In this write-up
These notes document the October 2026 CTF challenge. Commands, addresses and outputs refer to the supplied challenge environment.
Challenge files#
ls
# bundle.zip postcards.pcap
We only need postcards.pcap.
1. Basic recon#
file postcards.pcap
capinfos postcards.pcap
This is a small pcap: 10 packets, about 9 seconds, Ethernet capture.
Then list DNS queries:
tshark -r postcards.pcap -Y dns -T fields \
-e frame.number -e ip.src -e ip.dst -e dns.qry.name -e dns.qry.type
Output:
1 192.0.2.10 192.0.2.53 001.obxxg5ddmfzgi427.dispatch.test 1
2 192.0.2.10 192.0.2.53 004.pu.dispatch.test 1
3 192.0.2.10 192.0.2.53 002.gqyggmrvmnsdsojx.dispatch.test 1
4 192.0.2.10 192.0.2.53 000.oruwezlcpnsg4427.dispatch.test 1
5 192.0.2.10 192.0.2.53 001.obxxg5ddmfzgi427.dispatch.test 1
6 192.0.2.10 192.0.2.53 assets.library.test 1
7 192.0.2.10 192.0.2.53 000.oruwezlcpnsg4427.dispatch.test 1
8 192.0.2.10 192.0.2.53 weather.library.test 1
9 192.0.2.10 192.0.2.53 003.gi2gkodcmu2dozrx.dispatch.test 1
10 192.0.2.10 192.0.2.53 time.library.test 1
2. Identify the real postcards#
The interesting queries are:
<number>.<data>.dispatch.test
The library.test queries are background noise:
assets.library.test
weather.library.test
time.library.test
Also notice duplicates:
000appears twice001appears twice
These are retries and must be deduplicated.
3. Extract unique numbered postcards#
Use tshark, awk, and sort:
tshark -r postcards.pcap -Y 'dns.qry.name contains ".dispatch.test"' \
-T fields -e dns.qry.name \
| awk -F. '{print $1, $2}' \
| sort -k1,1n -u
Output:
000 oruwezlcpnsg4427
001 obxxg5ddmfzgi427
002 gqyggmrvmnsdsojx
003 gi2gkodcmu2dozrx
004 pu
The first column is the postcard number.
The second column is the encoded chunk.
4. Reassemble and decode#
Concatenate the chunks in numeric order:
oruwezlcpnsg4427
obxxg5ddmfzgi427
gqyggmrvmnsdsojx
gi2gkodcmu2dozrx
pu
Combined:
oruwezlcpnsg4427obxxg5ddmfzgi427gqyggmrvmnsdsojxgi2gkodcmu2dozrxpu
This is Base32. It uses lowercase letters and digits 2-7.
Python’s Base32 decoder expects uppercase and correct padding.
Decode it:
python3 - <<'PY'
import base64
chunks = {
"000": "oruwezlcpnsg4427",
"001": "obxxg5ddmfzgi427",
"002": "gqyggmrvmnsdsojx",
"003": "gi2gkodcmu2dozrx",
"004": "pu",
}
s = ''.join(chunks[k] for k in sorted(chunks))
s += '=' * ((8 - len(s) % 8) % 8)
print(base64.b32decode(s.upper()).decode())
PY
Output:
tibeb{dns_postcards_40c25cd99724e8be47f7}
5. Fully automated one-liner#
This does everything from the pcap to the flag:
tshark -r postcards.pcap -Y 'dns.qry.name contains ".dispatch.test"' \
-T fields -e dns.qry.name \
| awk -F. '{print $1, $2}' \
| sort -k1,1n -u \
| awk '{printf "%s", $2}' \
| python3 -c 'import sys, base64; s=sys.stdin.read().strip(); s += "="*((8-len(s)%8)%8); print(base64.b32decode(s.upper()).decode())'
Output:
tibeb{dns_postcards_40c25cd99724e8be47f7}
6. Flag#
tibeb{dns_postcards_40c25cd99724e8be47f7}
Tricky / interesting parts#
-
Data is hidden in DNS query names
The query name itself carries the payload. No DNS response is needed. -
Retries are deliberate
000and001appear twice. You must deduplicate before decoding. -
Background traffic is deliberate
assets.library.test,weather.library.test, andtime.library.testare red herrings. Ignore them. -
Base32, not Base64
The alphabet usesA-Zand2-7. The digits4and7are valid Base32 characters. -
Lowercase Base32
Python’sbase64.b32decodewants uppercase, so use.upper()orcasefold=True. -
Padding matters
The concatenated string is 66 characters. Base32 needs a length multiple of 8, so add6padding=characters. -
Final chunk is short
The last chunk is onlypu. Do not assume all chunks have the same length. Concatenate everything first, then decode. -
Sequence numbers are zero-padded
000,001,002, etc. This makes sorting easy, but numeric sort is safer.