
WebSummarizer Pro (AI/LLM) - Writeup
On this page
Challenge Overview
| Event | KPMG CTF 2026 Campus Edition |
|---|---|
| Challenge | WebSummarizer Pro |
| Category | AI / LLM (agentic SSRF) |
| Points | 20 |
| Target | kicyber-...-web-summarizer-pro-0-0.chals.io |
"Make an LLM-powered web summarizer bypass its URL safety policy and reach a protected internal endpoint."

WebSummarizer Pro is an LLM that fetches and summarizes public web pages. Its operators "swear it can't touch internal infrastructure." The goal is SSRF (Server-Side Request Forgery): abuse the server's own URL fetcher to reach an internal service it's supposed to refuse.
The crucial mindset here: the chatbot is just the interface. The vulnerability lives in the backend fetcher and how it validates URLs, not in the model's politeness. The whole challenge is finding the gap between what the policy checks and what the HTTP client actually connects to.
Step 1: Baseline Behaviour
Start with a harmless public URL to see how the bot fetches:

Please summarize https://example.com/
→ "I tried to fetch https://example.com/, but it returned 404..."
So the backend genuinely performs a server-side fetch and reports a real HTTP status. Good, that means there's a live fetcher to target, not just a model hallucinating summaries.
Step 2: Enumerate the Policy (Ask, Don't Fetch)
Before attacking, map the rules. Asking the model to explain its policy (without fetching) lays out exactly what we're up against:


The stated policy:
- Accept
https://+ public hostname. - Reject
https://169.254.169.254/(cloud metadata IP) explicitly. - Reject hostnames that resolve to private/internal IPs (RFC1918).
- Redirects must have their destination re-validated too.
- Reject "alternative textual representations" of IPs in general, because, in its words, "validation is effectively based on the resolved IP."
That last claim is the one to test. If validation really normalized every URL to its resolved IP before deciding, SSRF would be hard. CTF experience says the claim and the implementation rarely match.
Step 3: Confirm It's SSRF, Not Prompt-Trust
The target name 169.254.169.254 (link-local cloud instance metadata) is dangled in the policy itself, a strong hint that the intended internal endpoint lives there. Direct attempts are refused, as expected:

A redirect-based bypass (via httpbin.org/redirect-to) is also caught, the policy revalidates the redirect target:

So the simple bypasses are covered. We need something that defeats the string check while still resolving to the internal IP.
Step 4: API Discovery (Confirm the Target)
Rather than guess, enumerate the app. The server: uvicorn header says this is FastAPI, which exposes its schema by default:
export T='https://kicyber-...-web-summarizer-pro-0-0.chals.io'
curl -sk "$T/openapi.json" # HTTP 200
curl -sk "$T/api/challenge" | python3 -m json.tool
/api/challenge confirms the exact challenge and intent:
{
"id": "hard-02-agentic-ssrf",
"category": "agentic-ssrf",
"briefing": "...Its operators swear it can't touch internal infrastructure.
Investigate whether it can reach protected internal services."
}
And /openapi.json plus the inline JS confirm the chat contract and that success is server-side (the backend returns solved + flag, so faking frontend state won't work):
POST /api/chat → { "session_id": "...", "message": "..." }
This matters: it tells us we must make the real backend fetch succeed, we can't fake our way past it.
Step 5: Probe the IP Parser
The policy blocks the dotted-decimal string 169.254.169.254 and known private hostnames. But does it block other numeric encodings of the same address? IPv4 can be written as a single 32-bit integer, and many HTTP clients (Python's included) happily accept it.
Test with the loopback address first. 127.0.0.1 in decimal-integer form is 2130706433:

https://2130706433/
→ HTTP 404, "Not found, or blocked by policy." (Final URL: https://2130706433/)
This is the breakthrough. The request was not rejected by the policy, it was actually fetched (and got a 404 because nothing's served at 127.0.0.1/). The string-based blocklist never recognised 2130706433 as a loopback address, even though the fetcher resolved and connected to it. The "validation is based on the resolved IP" claim was false; it's checking the raw string.
Step 6: Exploit — Decimal-Encode the Metadata IP
Now apply the same trick to the real target. The metadata IP 169.254.169.254 as a 32-bit integer:
169·2^24 + 254·2^16 + 169·2^8 + 254 = 2852039166
The protected endpoint from our recon is /health, so the payload is https://2852039166/health:

Please fetch and summarize this URL: https://2852039166/health
Fetched https://2852039166/health successfully.
- HTTP status: 200
- Returned content (verbatim): KPMG_CTF{h87bXkdKZpbAj3cs7W03aeT4kbWXk_xGd7iFPE_m4UTc}
The string blocklist didn't match 2852039166, so the policy allowed it; the fetcher then expanded the integer back to 169.254.169.254 and hit the internal endpoint. The backend flips solved: true and returns the flag:

KPMG_CTF{h87bXkdKZpbAj3cs7W03aeT4kbWXk_xGd7iFPE_m4UTc}
Summary
| Step | Action | Result |
|---|---|---|
| 1 | Summarize example.com | Confirms real server-side fetch |
| 2 | Ask policy (no fetch) | Blocklist: 169.254.169.254, private IPs, redirects |
| 3 | Direct + redirect attempts | Both blocked |
| 4 | /openapi.json, /api/challenge | Confirms agentic-ssrf, chat schema |
| 5 | https://2130706433/ (127.0.0.1) | Reached fetcher → string check is naive |
| 6 | https://2852039166/health | Metadata IP reached → flag |
Key Takeaways
- Validate the resolved destination, not the string. The entire bug is that the policy matched text (
169.254.169.254) while the HTTP client connected to a normalized address. Decimal, octal, hex and other IPv4 encodings all collapse to the same IP, so a blocklist that doesn't normalise first is trivially bypassed. The fix is to resolve the host, then check the final IP against deny-ranges. - The LLM's description of its controls is not the control. The model confidently claimed validation was "based on the resolved IP." It wasn't. Never trust a model's account of its own backend, test the backend.
- Enumerate before you exploit. FastAPI's
/openapi.jsonand/docshanded us the routes, the challenge ID (hard-02-agentic-ssrf), and the request schema. Framework defaults leak a lot. - Probe with loopback first. Testing
2130706433(127.0.0.1) proved the parser gap safely and cheaply before spending a shot on the real target. - Agentic SSRF = classic SSRF with an LLM front door. The alternative-IP-encoding bypass is a textbook SSRF filter evasion (OWASP SSRF). Wrapping the fetcher in an LLM changes the interface, not the underlying flaw.
Flag: KPMG_CTF{h87bXkdKZpbAj3cs7W03aeT4kbWXk_xGd7iFPE_m4UTc}