NDIAS Automotive/IoT CTF 2026 — Gate to the Past Writeup

An old IoT device SSHv2 session captured in a pcap, encrypted with AES-128-CBC. The 2007-era Debian OpenSSL weak PRNG (CVE-2008-0166) makes the Diffie-Hellman private exponent recoverable, so the whole session can be decrypted and the flag read off the wire.

4 min read
ibrahimsql
608 words

NDIAS Automotive/IoT CTF 2026 — Gate to the Past

A network-forensics challenge built around a 20-year-old SSH device and the famous Debian OpenSSL disaster.

  • Event: NDIAS Automotive/IoT CTF 2026
  • Category: Network
  • Artifact: dist-gate_to_the_past.zip → challenge.pcap
  • Flag format: FLAG{...}

1. Reading the handshake

The capture is a single TCP flow, client:50398 → server:22. Both sides open with the plaintext identification string:

SSH-2.0-OpenSSH_4.3p2 Debian-9etch3

That string is the giveaway. Debian-9etch3 dates the build to Debian Etch (2007), which is before 2008-05-13 — the window of CVE-2008-0166, the Debian OpenSSL PRNG catastrophe. On those systems RAND_bytes() had effectively no entropy beyond the process ID, so every "random" value OpenSSL produced was drawn from a tiny, enumerable set.

From the plaintext KEXINITs:

  • KEX: diffie-hellman-group-exchange-sha1
  • Cipher: aes128-cbc
  • MAC: hmac-md5
  • Compression: none

Then the GEX exchange exposes everything we need:

Text
min/n/max   1024 1024 8192
p bits      1024   g 2     (p is a safe prime)
client public  e = g^x mod p
server public  f = g^y mod p
host key       ssh-rsa, 2048-bit

2. Recovering the client DH private exponent

x is generated with RAND_bytes through OpenSSL's BN_rand path — i.e. from the broken PRNG. Enumerating the PRNG state (the seeding PID plus a small number of bytes consumed from the generator before the exponent draw) yields exactly one candidate x that reproduces the client's public value. Verifying it against the capture:

x = 0xbd886dabb7951ae7495f31444753098d07db09b9357d8df2795f88012158a351
pow(2, x, p) == e   # -> True

Once x is known the shared secret falls out immediately:

K = pow(f, x, p)

3. The exchange hash — mind the padding

The SSH exchange hash for group-exchange is built from the identification strings, the two SSH_MSG_KEXINIT payloads, the host key, the GEX parameters and the DH values:

H = SHA1( str(V_C) + str(V_S) + str(I_C) + str(I_S) + str(K_S)
        + u32(min) + u32(n) + u32(max)
        + mpint(p) + mpint(g) + mpint(e) + mpint(f) + mpint(K) )

The one non-obvious detail: the I_C / I_S values are the KEXINIT payloads, i.e. the packet minus its 4-byte length, 1-byte padding length and the trailing padding. Include the padding and the hash is wrong. With the correct boundaries:

H = 3800bc5ef9859a61c28bfcc4fdd43b5bc9bcfdca

This also validates against the server's RSA signature over H.

4. Key derivation and decryption

RFC 4253 key derivation with session_id = H:

IV_c2s  = SHA1(K || H || "A" || sid)      key_c2s = SHA1(K || H || "C" || sid)      mac_c2s = SHA1(K || H || "E" || sid)
IV_s2c  = SHA1(K || H || "B" || sid)      key_s2c = SHA1(K || H || "D" || sid)      mac_s2c = SHA1(K || H || "F" || sid)

AES-128-CBC takes the first 16 bytes, IVs 16 bytes, and the HMAC-MD5 keys 16 bytes. Decrypting both directions packet by packet and verifying every HMAC-MD5 over seq || plaintext gives a clean, fully authenticated transcript — no MIC failures.

One packaging detail worth catching: the server put its SSH_MSG_GEX_REPLY (704 bytes) and its tiny SSH_MSG_NEWKEYS (16 bytes) in one TCP segment, so a per-segment parser mis-counts the sequence and derails the CBC IV chain. Splitting the stream into SSH frames (not TCP frames) fixes it.

5. What the client actually ran

Text
c2s  #9  CHANNEL_REQUEST exec:
     echo "flag: FLAG{n3v3r_f0rg3t_13g4cy_bug5}"
 
s2c #10  CHANNEL_DATA:
     flag: FLAG{n3v3r_f0rg3t_13g4cy_bug5}

6. Flag

FLAG{n3v3r_f0rg3t_13g4cy_bug5}

The lesson is older than the challenge: a single broken RAND_bytes from 2008 turns a "strong" 1024-bit Diffie-Hellman exchange into a decryption key. The password on this device was never the secret — the PRNG was.

---
Share this post:

What do you think?

React to show your appreciation

Related Posts