
Usando Struts2 y PowerShell para recrear la vulnerabilidad de inyección OGNL CVE-2017-5638.
Este repositorio forma parte del proyecto del curso SC3010 Computer Security.
Recrea CVE-2017-5638, una vulnerabilidad crítica de ejecución remota de código en Apache Struts 2 (versiones 2.3.5–2.3.31 y 2.5–2.5.10), famosa por haber sido explotada en la brecha de datos de Equifax de 2017 para exfiltrar los registros personales de aproximadamente 147 millones de personas.
La vulnerabilidad surge de un único fallo de diseño: cuando Apache Struts no logra analizar una solicitud de carga multipart/form-data, construye un mensaje de error incrustando el encabezado Content-Type sin procesar —sin sanitizarlo— en una cadena que luego se pasa por el evaluador de expresiones OGNL. Por lo tanto, un atacante que coloca una expresión OGNL dentro del encabezado Content-Type consigue que se ejecute en el servidor como el usuario del proceso Tomcat.
Este repositorio contiene:
SC3010-Computer-Security/
├── simulation/
│ ├── backend/ # Vulnerable Apache Struts2 2.3.28 server (Java/Maven)
│ └── attack-script/ # Exploit script for CVE-2017-5638
│ └── exploit_cve_2017_5638.ps1 # PowerShell (cross-platform)
├── struts-src-code/ # Apache Struts2 reference source + legal notices
│ ├── licenses/ # LICENSE, NOTICE, and component licenses
│ └── src/
│ ├── struts2-core/ # Request pipeline classes + vulnerable JakartaMultiPartRequest
│ ├── xwork2/ # ActionContext.java, OgnlUtil.java
│ └── ognl/ # OgnlContext.java
├── diagrams/ # State-machine + sequence diagrams, annotated OGNL payload
└── _notes/ # Background reading
El siguiente diagrama muestra cada rama de decisión en el pipeline de solicitudes de Struts2. La ruta roja es la que el atacante fuerza; las ramas grises → ... son rutas seguras que no aplican durante la explotación.
flowchart TD
Start(["Attacker sends HTTP POST /upload.action<br/>Content-Type: %{OGNL_PAYLOAD}.multipart/form-data"])
Start --> PIPE
PIPE["<b>Struts2 filter → wrap → parse pipeline</b><br/>doFilter() → PrepareOperations.wrapRequest()<br/>→ Dispatcher.wrapRequest() → new MultiPartRequestWrapper()"]
PIPE --> D_CT{"Content-Type contains<br/>multipart/form-data?"}
D_CT -- "No → normal request ..." --> OUT_NORM([non-upload path ...])
D_CT -- "Yes (attacker appends<br/>.multipart/form-data to payload)" --> PARSE
PARSE[/"<b>JakartaMultiPartRequest.parse()</b><br/>→ Commons FileUpload.parseRequest()"/]
PARSE --> D_VALID{"FileUpload can parse<br/>Content-Type?"}
D_VALID -- "Yes → normal file upload ..." --> OUT_OK([files extracted, action executes ...])
D_VALID -- "No — malformed Content-Type<br/>throws InvalidContentTypeException<br/>(message = raw Content-Type string)" --> CATCH
CATCH["parse() catch block<br/>calls buildErrorMessage(e, {})"]
CATCH --> FIND
FIND[/"<b>buildErrorMessage()</b><br/>→ LocalizedTextUtil.findText(class, errorKey, locale, e.getMessage(), args)"/]
FIND --> D_KEY{"Resource bundle has key<br/>struts.messages.upload.error.*?"}
D_KEY -- "Yes → safe localised message ..." --> OUT_SAFE([error displayed safely ...])
D_KEY -- "No — falls back to<br/>e.getMessage() as the default<br/>(attacker's raw Content-Type)" --> TRANSLATE
TRANSLATE["TextParseUtil.translateVariables()<br/>scans string for %{...} expressions"]
TRANSLATE --> D_OGNL{"String contains<br/>%{...} OGNL expression?"}
D_OGNL -- "No → plain error string ..." --> OUT_PLAIN([safe error message ...])
D_OGNL -- "Yes — evaluates OGNL<br/>inside attacker content" --> SANDBOX
SANDBOX[/"<b>OGNL sandbox bypass</b><br/>#ognlUtil.getExcludedClasses().clear()<br/>#ognlUtil.getExcludedPackageNames().clear()"/]
SANDBOX --> ACCESS
ACCESS[/"<b>Unrestricted reflection</b><br/>#context.setMemberAccess(DEFAULT_MEMBER_ACCESS)"/]
ACCESS --> RCE
RCE[/"<b>RCE</b><br/>Runtime.getRuntime().exec(cmd)"/]
RCE --> Done([Command executed as Tomcat process user])
style Start fill:#d32f2f,color:#fff
style Done fill:#d32f2f,color:#fff
style OUT_NORM fill:#9e9e9e,color:#fff
style OUT_OK fill:#9e9e9e,color:#fff
style OUT_SAFE fill:#9e9e9e,color:#fff
style OUT_PLAIN fill:#9e9e9e,color:#fff
style SANDBOX fill:#b71c1c,color:#fff
style ACCESS fill:#b71c1c,color:#fff
style RCE fill:#b71c1c,color:#fff
style CATCH fill:#e65100,color:#fff
style TRANSLATE fill:#e65100,color:#fff
Para consultar la máquina de estados completa con explicaciones de las ramas, véase diagrams/cve-2017-5638-state-machine.md.
Para una vista de diagrama de secuencia y un desglose anotado del payload OGNL, véase diagrams/cve-2017-5638-attack-chain.md.
Este repositorio reproduce partes del código fuente de Apache Struts 2.3.28 con fines de investigación académica en seguridad.
Apache Struts es Copyright © 2000–2016 The Apache Software Foundation.
Licenciado bajo la Apache License, Version 2.0.
Una copia de la licencia está disponible enstruts-src-code/licenses/LICENSE.txt.
El aviso de atribución completo exigido por la Apache License se encuentra enstruts-src-code/licenses/NOTICE.txt.
Apache Struts 2 incluye componentes adicionales de terceros, cada uno regido por su propia licencia:
| Componente | Archivo de licencia |
|---|---|
| OGNL (Object-Graph Navigation Library) | struts-src-code/licenses/OGNL-LICENSE.txt |
| XWork | struts-src-code/licenses/XWORK-LICENSE.txt |
| FreeMarker | struts-src-code/licenses/FREEMARKER-LICENSE.txt |