
Utilisation de Struts2 et PowerShell pour recréer la vulnérabilité d'injection OGNL CVE-2017-5638.
Ce dépôt fait partie du projet de cours SC3010 Computer Security.
Il recrée CVE-2017-5638, une vulnérabilité critique d'exécution de code à distance dans Apache Struts 2 (versions 2.3.5–2.3.31 et 2.5–2.5.10), tristement célèbre pour avoir été exploitée lors de la violation de données d'Equifax en 2017 afin d'exfiltrer les dossiers personnels d'environ 147 millions de personnes.
La vulnérabilité découle d'un unique défaut de conception : lorsque Apache Struts échoue à analyser une requête d'upload multipart/form-data, il construit un message d'erreur en intégrant l'en-tête Content-Type brut — sans le nettoyer — dans une chaîne qui est ensuite transmise à l'évaluateur d'expressions OGNL. Un attaquant qui place une expression OGNL dans l'en-tête Content-Type obtient donc son exécution côté serveur sous l'utilisateur du processus Tomcat.
Ce dépôt contient :
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
Le diagramme ci-dessous montre chaque branche de décision dans le pipeline de requêtes Struts2. Le chemin rouge est celui que l'attaquant force ; les branches grises → ... sont des chemins sûrs qui ne s'appliquent pas pendant l'exploit.
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
Pour la machine à états complète avec les explications des branches, voir diagrams/cve-2017-5638-state-machine.md.
Pour une vue en diagramme de séquence et une décomposition annotée de la charge utile OGNL, voir diagrams/cve-2017-5638-attack-chain.md.
Ce dépôt reproduit des portions du code source d'Apache Struts 2.3.28 à des fins de recherche académique en sécurité.
Apache Struts est Copyright © 2000–2016 The Apache Software Foundation.
Sous licence Apache License, Version 2.0.
Une copie de la licence est disponible à l'adressestruts-src-code/licenses/LICENSE.txt.
L'avis d'attribution complet requis par l'Apache License se trouve dansstruts-src-code/licenses/NOTICE.txt.
Apache Struts 2 intègre des composants tiers supplémentaires, chacun régi par sa propre licence :
| Composant | Fichier de licence |
|---|---|
| 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 |