Server-Side Request Forgery (SSRF)
Prerequisites¶
- INSECURE-threat-intel-app Docker stack running; signed in at http://localhost:8081.
- Burp Suite (Burp basics).
- A text editor to craft a small SVG file (the payload).
Likeliness meter¶
Likely — Apps that accept image uploads and process them server-side (thumbnailing, optimizing, rendering SVG/PDF) routinely follow resource references inside the file. Crafted SVGs are a classic SSRF and XXE source, and they frequently sit one hop from cloud metadata.
Overview¶
Server-Side Request Forgery (SSRF) forces the application server — not the victim’s browser — to issue HTTP (or other protocol) requests to a destination the attacker chooses. From the network’s point of view, the request often comes from a trusted internal IP (the app container or cloud instance), so attackers reach:
- Loopback services (
http://localhost:4321/) - Cloud metadata (
http://169.254.169.254/latest/meta-data/on AWS) - Internal RFC1918 hosts not exposed to the internet
This lab’s SSRF is hidden inside an uploaded image. The CTI reports feature lets analysts publish Cyber Threat Intelligence reports to a shared team feed, each with a cover image. When you upload an image, the server “processes” it and resolves external resources referenced inside the file — exactly what real renderers (ImageMagick, librsvg, headless browsers) do with an SVG <image href="…">, <use href>, xlink:href, or CSS url(…).
That resolver is the SSRF sink: it calls fetch() on whatever URL is embedded in your SVG, with no host or scheme validation. A simulated cloud metadata service (IMDS-style) is reachable from inside the container at http://localhost:4321/latest/meta-data/iam/security-credentials/ and hands out AWS-style credentials.
There is no URL field in this form — the only input is a file. The trick is that an SVG is text/XML, so the attacker-controlled URL travels inside the image.
Learning objectives¶
- Recognise that an image upload can trigger server-side fetches when the file is processed.
- Craft an SVG whose embedded reference points at an internal target.
- Pivot the embedded reference from a public URL to cloud metadata.
- Exfiltrate simulated AWS keys and document impact in your lab report.
Attacker methodology¶
Step 1 — Recon: which uploads get processed server-side?¶
Walk the authenticated app. In Burp Site map / HTTP history, ask: “Does any feature take a file and do something to it on the server?”
| Feature | SSRF candidate? |
|---|---|
| Dashboard panels | Pre-seeded synthetic data — no upload |
Documents (?id=) |
Reads local records — IDOR surface, not SSRF |
| CTI reports → Publish report → cover image | Yes — the upload is processed (SVG resolved) server-side |
Open http://localhost:8081/cti. The Publish report form accepts a cover image (PNG, JPG, or SVG) and notes that “vector images are processed to resolve embedded assets.” That hint is the attack surface.
Note for your report: “Upload feature parses SVG/XML server-side and resolves embedded references — candidate SSRF.”
Step 2 — Baseline: a benign image upload (the control)¶
Upload a normal raster image (e.g. sample-cti-cover.png) and publish. It appears in the Team feed as the report’s avatar with no “Embedded resources resolved” block and no SSRF banner — because a PNG carries no external references. This is your “before” control.
Step 3 — Craft an SVG with an external reference¶
SVG is XML, so it can reference external resources the renderer will fetch. Save this as poc.svg:
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
<image href="https://example.com/" />
</svg>
Upload poc.svg as the cover image and publish. The report card now shows an “Embedded resources resolved while processing image” block with the HTTP status, content-type, and the fetched body of https://example.com.
That proves the server — not your browser — fetched the URL embedded in your file. (With Burp Pro you would use a unique Collaborator host and watch for a hit from the server IP.)
Step 4 — First internal probe¶
Edit the SVG to aim the reference at loopback, then re-upload:
You may get a connection error, a redirect to /login, or a generic page — that is normal. Document it, then aim at a path that matters.
Loopback naming: use
localhostas the internal address in this lab — it is the most reliable loopback name across the app's runtime.http://127.0.0.1:4321/and the container DNS namehttp://app:4321/are equivalent targets to try iflocalhostis filtered.
Step 5 — Reach the cloud metadata service (impact)¶
Real cloud instances expose an Instance Metadata Service (IMDS) at http://169.254.169.254/. This lab simulates it on loopback so you can practice the exact request shape. First, list the attached IAM role by embedding this href and uploading:
The resolved-resource block returns a role name:
Now point the embedded reference at that role to pull its credentials:
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
<image href="http://localhost:4321/latest/meta-data/iam/security-credentials/cti-uploader-role" />
</svg>
Upload it. Expected: the resolved-resource block contains JSON with AWS-style keys, and the “Found SSRF” banner appears:
{
"Code": "Success",
"Type": "AWS-HMAC",
"Role": "cti-uploader-role",
"AccessKeyId": "AKIALABEXAMPLE",
"SecretAccessKey": "lab-secret-not-real-aws-key",
"Token": "lab-session-token-not-real"
}
You just made the server read its own cloud credentials by hiding the request inside a cover image. A ready-made payload lives at exercise-files/web-attacks-101/INSECURE-threat-intel-app/sample-cti-cover-ssrf.svg.
Cloud equivalent: on a real EC2 host the same URL is
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>. The lab maps that role path onto localhost so no real metadata service is needed.
Step 6 — Burp workflow¶
- With Intercept off, upload an SVG and find the
POST /ctirequest in HTTP history. - Send to Repeater. The multipart body contains the
cover_imagefile part — the SVG text is right there in the request. - Edit the
href="…"inside the SVG part and resend to iterate on targets without re-saving files. Keep one Repeater tab per target URL.
Step 7 — Understand blast radius (write this down)¶
| If this were production | Lab equivalent |
|---|---|
| Steal cloud role credentials from IMDS | AWS-style keys in JSON — treat as critical impact class |
| Use stolen keys against the cloud API | Keys are fake — note the capability in your report |
Read local files via file:// in SVG/XXE |
Out of scope here; note as a related vector |
| Scan internal ports via timing/errors | Embed http://localhost:5432/ and observe errors vs successes |
One strong internal read (the credentials document) is enough for the report.
Step 8 — Report checklist¶
| Field | Example |
|---|---|
| URL | /cti |
| Parameter | cover_image (uploaded SVG) |
| Vector | SVG <image href> resolved server-side during image processing |
| External PoC | SVG referencing https://example.com |
| Internal PoC | SVG referencing http://localhost:4321/latest/meta-data/iam/security-credentials/cti-uploader-role |
| Impact | Server-side read of simulated cloud metadata → AWS-style credentials disclosed |
| Remediation | Don’t resolve external references in uploads; strip/sandbox SVG; allowlist egress; block link-local/private IPs |
Save the Repeater response (and the report card) showing the credentials JSON.
Remediation¶
- Do not resolve external references in uploaded files. Disable external entity/resource loading in your image/SVG processor (no network fetches during render).
- Sanitise SVGs (e.g. strip
<image>,<use>,href/xlink:href, scripts) or rasterise them in an isolated, network-less sandbox. Prefer raster formats for avatars. - If a fetch is unavoidable, allowlist destination hosts, validate after DNS resolution, block link-local (
169.254.0.0/16) and private ranges, and setredirect: "error". - Run processors in a segment with no route to metadata; in AWS, enforce IMDSv2.