Back to KPMG CTF 2026 Campus Edition
Locks of Confusion 1  (Infrastructure) - Writeup
KPMG CTF 2026 Campus EditionInfrastructure10 ptsMedium

Locks of Confusion 1 (Infrastructure) - Writeup

Oct 12, 2026 4 min readWriteup 10 of 22
On this page
  1. 1.Challenge Overview
  2. 2.Step 1: Recon — robots.txt and the README
  3. 3.Step 2: Follow the Paper Trail
  4. 4.Step 3: The /verify Gate
  5. 5.Step 4: Inspect the Archives
  6. 6.Step 5: Match the Key
  7. 7.Step 6: The Twist — Key Body = Zip Password
  8. 8.Summary
  9. 9.Key Takeaways

Challenge Overview

EventKPMG CTF 2026 Campus Edition
ChallengeLocks of Confusion – 1
CategoryInfra / Recon (weak-key cryptography)
Points10
Targetkicyber-3ec0b7cda978-locks-of-confusion1-0-0.chals.io

"Internal Ops just migrated an old database replica onto new hardware and swears everything was done 'by the book'. Somewhere sits a key that was decided long before it was ever generated."

Locks of Confusion challenge card

The target is an Internal Ops Portal — a read-only intranet of inventory pages, tickets, docs and backups. The riddle is the whole hint: a key "decided long before it was ever generated" points at a predictable-PRNG weak key (Debian OpenSSL, CVE-2008-0166). The challenge is a paper-trail recon exercise that ends in matching a public key against a pool of weak candidates and using the recovered private key to unlock a backup.

The crucial mindset: don't fuzz for files. Every step is explicitly linked from the one before it — the portal tells you where to go if you actually read the pages instead of grepping them down to one line.


Step 1: Recon — robots.txt and the README

Map the portal and check the obvious disclosure file:

export T='https://kicyber-3ec0b7cda978-locks-of-confusion1-0-0.chals.io'
curl -sk "$T/robots.txt"
Disallow: /internal-only/
Disallow: /old-admin/
Disallow: /.git/
Disallow: /archive/keyspace/README.txt
# you're near but still far, but don't leave the inventory just
# because you didn't find the SoUrCe

The last Disallow is the lead. Fetch and decode it (it's Base64):

curl -sk "$T/archive/keyspace/README.txt" | base64 -d
these got pulled off an old backup tape during one of the legacy-tier
audits... if you're looking at this because you found something worth
checking against 300 old keys, you probably already know what to do with them.

So the intended technique is confirmed: match something against 300 old keys. The odd capitalisation SoUrCe is a nudge to read page source, not just rendered text.


Step 2: Follow the Paper Trail

The /backups and /cron-reports pages both point at MIG-2011-004, and /inventory/legacy-03 links both the migration doc and a ticket:

legacy-03 inventory with Related links

  • /wiki/notes — R. Alvarez (2011) confirms the svc-legacydb keypair on legacy-03 is the original from Nov 2007 provisioning, carried through the 2011 P2V migration and never rotated.
  • /docs/migration-2011-004 — confirms SSH host keys + the svc-legacydb keypair were migrated without regeneration, and that a partial tape restore was left in cold storage.
  • /tickets/TICK-4471 — "SSH access broken for svc-legacydb after P2V migration." The resolution restored authorized_keys from the 2007 tape.

The real payload is in the HTML comments of TICK-4471 (view source):

<!-- TODO(infra): the tape-restore bundle from this incident never got
     cleaned up -- still sitting at
     /archive/provisioning/legacy-03-2007.tar.gz -->
<!-- TODO(infra): same for the old keyspace mirror -- clean this up too:
     /archive/keyspace/legacy-keys-2007.tar.gz -->

Two exact paths — the restore bundle (holds authorized_keys) and the 300-key mirror.


Step 3: The /verify Gate

Downloading either path directly returns a 60-byte decoy:

403 Forbidden -- complete the access check at /verify first.

/verify is a cookie-based access check — it asks you to click the legacy-03 node in a diagram, which sets a session cookie that unlocks /archive/ for a few minutes. curl runs in a separate session with no cookie, so either download in the browser (which carries the cookie automatically) or copy the session cookie from DevTools into curl -b.

Verify page — click legacy-03 to unlock

Then grab both tarballs and confirm they're real gzip (not another 60-byte 403):

file legacy-keys-2007.tar.gz legacy-03-2007.tar.gz   # gzip compressed data

Step 4: Inspect the Archives

tar -xzf legacy-03-2007.tar.gz -C prov
tar -xzf legacy-keys-2007.tar.gz -C keys

The provisioning bundle's var/log/provision.log is the smoking gun:

Nov 14 2007 ... apt: installing openssl (0.9.8c-4)
Nov 14 2007 ... provision: ssh-keygen -t rsa -b 2048 -f /home/svc-legacydb/.ssh/id_rsa -N ''

Debian etch + openssl 0.9.8c + RSA generated in 2007 = textbook CVE-2008-0166. The key was "decided" by a broken PRNG with only ~32,767 possible outputs — decided long before it was generated.

The keys mirror ships its own 300 candidate private keys (privkey_0000.pem … privkey_0299.pem) plus a README.txt — so no external Debian corpus is needed.


Step 5: Match the Key

The target public key lives in prov/legacy-03-2007/home/svc-legacydb/.ssh/authorized_keys. Derive each candidate's public key and compare:

import glob, base64
from cryptography.hazmat.primitives import serialization

tgt = base64.b64decode(open("authorized_keys").read().split()[1])
for p in sorted(glob.glob("keys/privkey_*.pem")):
    k = serialization.load_pem_private_key(open(p,"rb").read(), password=None)
    pub = k.public_key().public_bytes(
        serialization.Encoding.OpenSSH, serialization.PublicFormat.OpenSSH)
    if base64.b64decode(pub.split()[1]) == tgt:
        print("MATCH:", p); break
MATCH: keys/privkey_0137.pem

(Equivalent one-liner with ssh-keygen -y -f privkey_XXXX.pem and grep if you have openssh installed.)


Step 6: The Twist — Key Body = Zip Password

The keyspace README.txt reveals the final move:

there's also a leftover account-data backup:
    /archive/backups/e6f25638ae2ed6ba955cdae6a74c6798.zip
it's password-protected ... the password is the BODY of the matching .pem file
-- strip the BEGIN/END lines and every line break, take the first 64 characters.

So the whole key-matching exercise existed to produce this password. Extract it from the matched key:

grep -v -- '-----' keys/privkey_0137.pem | tr -d '\n' | head -c 64
# MIIEowIBAAKCAQEAui2yyUzRtrahcJ1ZpfIrOBvT+s9ZI4k0mXQMegwGWfeKQ4cA

Download the zip (behind /verify again — re-click to refresh the unlock) and extract:

unzip -P 'MIIEowIBAAKCAQEAui2yyUzRtrahcJ1ZpfIrOBvT+s9ZI4k0mXQMegwGWfeKQ4cA' \
      e6f25638ae2ed6ba955cdae6a74c6798.zip
cat user.txt
KPMG_CTF{J3cp4wRYSNX4dnNjqkuz2rxtMQcQEn4B_xyjG4bAUoBa}

Summary

StepActionResult
1robots.txt → Base64 READMEHint: "check against 300 old keys"
2Notes / MIG-2011-004 / TICK-4471 (view source)Two archive paths in HTML comments
3/verify node-clickCookie unlocks /archive/
4Unpack tarballs; read provision.logConfirms CVE-2008-0166 weak RSA key
5Match pubkey vs 300 candidatesprivkey_0137.pem
6Key body (first 64 b64 chars) → zip passworduser.txt → flag

Key Takeaways

  • "Decided before it was generated" = predictable PRNG. Debian OpenSSL (CVE-2008-0166) collapsed the key space to ~32k values per key type/size. Any RSA key born on an etch box in that window is recoverable by matching against the precomputed set — here, conveniently bundled.
  • Read the source, not the grep. The decisive clues (SoUrCe, the two archive paths) were in HTML comments and raw page bodies. Filtering output down to one line threw them away.
  • Follow the links the app gives you. Inventory → migration doc → ticket was a deliberate chain. Fuzzing /archive/keyspace/ with ffuf never would have found the comment-disclosed filenames.
  • Watch for stateful gates. /verify is cookie-based; curl and the browser are different sessions. The 60-byte "403" was a decoy served with HTTP 200, not a real status.
  • Never rotating keys is the whole vuln. A 2007 key carried verbatim through a 2011 migration (to avoid a fingerprint change) and never rotated is exactly how a long-dead weakness stays exploitable in 2026.

Flag: KPMG_CTF{J3cp4wRYSNX4dnNjqkuz2rxtMQcQEn4B_xyjG4bAUoBa}