Description
A suspicious cell tower has been detected. Despite the cellular framing, the capture is ordinary HTTP traffic: a compromised handset beacons to a rogue 'tower' over HTTP, leaking its IMSI in a User-Agent header and exfiltrating the flag as base64 in the request bodies. The flag is XOR-encrypted with a key derived from that IMSI, recovered with a known-plaintext attack.
Open the PCAP in Wireshark. Filter on UDP first: look for a broadcast packet advertising an unauthorized test network. That packet names the rogue tower's cell ID.
Once you know the rogue cell ID, filter on http and find the device whose User-Agent shows that same cell ID - confirming it connected to the rogue tower.
Follow the HTTP conversation between the victim device and the rogue tower.
tshark -r rogue_tower.pcap -Y http -T fields -e http.host -e http.user_agent -e http.request.uriwireshark rogue_tower.pcapSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Pull the IMSI from the HTTP User-AgentObservationI noticed the challenge description said the capture is plain HTTP traffic and that the IMSI appears in a User-Agent header, which suggested filtering on http.user_agent rather than any GSM or NAS layer to extract the device identity and key seed.Filter on http and read the victim's requests. The device identifies itself in a User-Agent like 'MobileDevice/1.0 (IMSI:310410187936156; CELL:91043)'. Save that IMSI; the last 8 digits are the XOR key.bashtshark -r rogue_tower.pcap -Y 'http.user_agent contains "IMSI"' -T fields -e http.user_agentbash# -> MobileDevice/1.0 (IMSI:310410187936156; CELL:91043)Expected output
MobileDevice/1.0 (IMSI:310410187936156; CELL:91043)
What didn't work first
Tried: Filtering on GSMTAP or UDP layers looking for NAS or MCC/MNC fields to extract the IMSI from real cellular signaling
The capture has no GSMTAP encapsulation or GSM radio frames. Wireshark shows no NAS dissector output because the traffic is plain HTTP. The IMSI only appears as a literal string inside the HTTP User-Agent header, so any cellular-layer filter returns zero packets.
Tried: Using tshark with -e frame.coloring_rule.name or -e wlan.ta to find the rogue cell identity before switching to http filters
The pcap contains no 802.11 or radio-layer frames, so those fields are empty. The rogue tower's cell ID (CELL:91043) appears only inside the User-Agent string of HTTP requests, not in any packet metadata. The correct approach is to filter on http.user_agent directly.
Learn more
Do not chase real cellular signaling. The challenge is dressed up as an IMSI-catcher scenario, but the capture is plain HTTP. There is no GSMTAP, no NAS, no MCC/MNC to filter on. The only cellular artifact is the IMSI string the malware embeds in its User-Agent, which is both the device fingerprint and the seed of the decryption key.
Step 2
Reassemble the base64 exfil from the request bodiesObservationI noticed the rogue tower receives multiple HTTP POST requests from the victim device and each body ends with base64 padding (==), which suggested the flag was exfiltrated in chunks that must be concatenated in request order before decoding.The victim sends several HTTP POSTs to the rogue tower, each body carrying a base64 segment (the trailing == is the tell). Follow the HTTP streams and concatenate the segments in request order into one base64 blob.bashtshark -r rogue_tower.pcap -Y 'http.request.method == "POST"' -T fields -e http.file_databash# concatenate the base64 chunks in orderWhat didn't work first
Tried: Base64-decoding the body from a single HTTP POST and treating the result as the complete flag payload
Each POST body carries only one segment of the full exfiltrated data. Decoding a single chunk gives a short binary fragment that cannot be decrypted to a recognizable flag. You must follow all POST streams and concatenate the segments in request order before decoding.
Tried: Using tshark -e http.response.body instead of http.file_data to extract the POST contents
The exfiltrated data is in the request bodies sent by the victim, not in server responses. The field http.file_data captures request body bytes for POST packets. Using http.response.body yields the server's (empty or irrelevant) reply and misses the base64 payload entirely.
Learn more
Why base64 then XOR. Naively base64-decoding the blob yields binary garbage, because the plaintext was XOR-encrypted before encoding. The base64 is just transport. You need the key before the bytes mean anything, which is what the next step recovers.
Step 3
XOR-decrypt with the IMSI tail (known-plaintext check)ObservationI noticed that base64-decoding the reassembled blob produced binary garbage rather than a readable flag, and the challenge confirmed XOR encryption with a key derived from the IMSI, which suggested applying known-plaintext XOR using the 'picoCTF{' prefix to identify the correct IMSI substring as the repeating key.First, try the full IMSI (310410187936156) as the XOR key directly - it produces output but no recognizable flag. Next, apply known-plaintext: XOR the first 8 ciphertext bytes against 'picoCTF' to extract the actual repeating key bytes. Observe that those bytes match the last 8 digits of the IMSI (87936156). Finally, use that derived key to decrypt the full blob. CyberChef: From Base64 -> XOR(87936156, UTF8).pythonpython3 - <<'PY' import base64 blob = base64.b64decode("<concatenated base64 from the POST bodies>") key = b"87936156" # last 8 digits of IMSI 310410187936156 flag = bytes(b ^ key[i % len(key)] for i, b in enumerate(blob)) print(flag.decode()) # picoCTF{...} PYThe known-plaintext step is the safety net: if XORing the first 8 ciphertext bytes against
picoCTF{does not give a run of identical digits, you concatenated the base64 chunks in the wrong order or grabbed the wrong IMSI substring.What didn't work first
Tried: Using the full 15-digit IMSI (310410187936156) as the XOR key directly, treating it as a UTF-8 byte string
XORing with all 15 digits produces output but no readable flag prefix, because the actual key is only the last 8 digits (87936156). The known-plaintext check immediately reveals the mismatch: XORing the first 8 ciphertext bytes against 'picoCTF{' does not reproduce 87936156 when the wrong key length is assumed.
Tried: Applying XOR in CyberChef with key type set to Hex instead of UTF8 for the 8-digit key
The key bytes are the ASCII characters '8', '7', '9', '3', '6', '1', '5', '6' (0x38 0x37 0x39 0x33 0x36 0x31 0x35 0x36), not the raw hex value 0x87936156. Selecting Hex mode in CyberChef interprets the string as two 4-byte hex integers and produces garbage output. The correct CyberChef setting is UTF8 (or Latin1) so each digit character is treated as one key byte.
Learn more
Known-plaintext XOR. For a repeating-key XOR,
ciphertext XOR plaintext = keyat every position. Knowing any stretch of plaintext (the flag prefix) directly exposes that many key bytes. Here it both recovers and confirms the key derivation (the IMSI tail), so you are not guessing. See Wireshark and PCAP for CTF for the stream-following workflow.
Interactive tools
- Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
- Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
Flag
Reveal flag
picoCTF{r0gu3_c3ll_t0w3r_...}
Not real cellular forensics: the pcap is HTTP. The victim leaks its IMSI in the User-Agent (310410187936156) and exfiltrates the flag as base64 in POST bodies. Concatenate the base64, then XOR with the last 8 IMSI digits (87936156), confirming with the picoCTF{ known plaintext.