Rogue Tower picoCTF 2026 Solution

Published: March 20, 2026

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.

bash
tshark -r rogue_tower.pcap -Y http -T fields -e http.host -e http.user_agent -e http.request.uri
bash
wireshark rogue_tower.pcap

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Pull the IMSI from the HTTP User-Agent
    Observation
    I 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.
    bash
    tshark -r rogue_tower.pcap -Y 'http.user_agent contains "IMSI"' -T fields -e http.user_agent
    bash
    # -> 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.

  2. Step 2
    Reassemble the base64 exfil from the request bodies
    Observation
    I 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.
    bash
    tshark -r rogue_tower.pcap -Y 'http.request.method == "POST"' -T fields -e http.file_data
    bash
    # concatenate the base64 chunks in order
    What 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.

  3. Step 3
    XOR-decrypt with the IMSI tail (known-plaintext check)
    Observation
    I 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).
    python
    python3 - <<'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{...}
    PY

    The 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 = key at 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.

Key takeaway

Repeating-key XOR is trivially broken whenever any stretch of plaintext is known. XORing ciphertext bytes against the corresponding known-plaintext bytes directly reveals the key bytes at those positions, so a flag-format prefix like picoCTF{ gives eight characters of free known-plaintext in every picoCTF challenge. In malware analysis and network forensics, recognizing that a payload is XOR-obfuscated and recovering the key from a known header (file magic bytes, HTTP signatures, protocol prefixes) is a standard first step before deeper analysis.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for Forensics

What to try next