CVE-2017-5638 is a critical remote code execution flaw in the Jakarta Multipart parser of Apache Struts 2.3.5 to 2.3.31 and 2.5 to 2.5.10. A crafted HTTP header lets an unauthenticated attacker run commands with the privileges of the web server process. Fix it by upgrading struts2-core to a current 6.x or 7.x release (2.3.32 or 2.5.10.1 at minimum) and redeploying.
Both affected release lines are end of life, so a finding today usually points to an application nobody has rebuilt in years. If it faces the internet, plan for the upgrade and for a compromise check.
What the scanner is actually detecting
| Finding title | Scanner | How it is detected |
|---|---|---|
| Apache Struts 2.3.5 – 2.3.31 / 2.5.x < 2.5.10.1 Jakarta Multipart Parser RCE (remote) | Nessus 97610 (CGI abuses family) | Remote. Nessus tries to exploit the flaw against URLs its web crawler (webmirror) discovered |
| Apache Struts 2.3.5 < 2.3.32 / 2.5.x < 2.5.10.1 RCE (S2-045 / S2-046) | Tenable WAS | Component Vulnerability check during a web application scan |
Apache published the flaw as two bulletins under the same CVE. S2-045 covers a malicious Content-Type header; S2-046 covers a malicious Content-Disposition value or an improper Content-Length header. In both cases an invalid value makes the parser throw an exception, and Struts uses that exception to build the error message shown to the user, which is the path to code execution.
Tenable marks 97610 as exploited by Nessus, so a hit reflects how the application actually answered, not a version banner. Tenable rates it CVSS v3 9.8, CVSS v2 10.0 and VPR 9.2, and lists public exploits in Metasploit, CANVAS and Core Impact. Tenable also publishes a separate local, authenticated check for Windows and Unix hosts, which Tenable’s 2017 post recommended because it does not depend on crawling.
Real-world risk
The attack needs no credentials, no user interaction and a single HTTP request. Commands run as the servlet container’s service account, so a Tomcat running as root or LocalSystem hands over the whole host.
This is not theoretical. The GAO report on the 2017 Equifax breach describes attackers scanning, on March 10, 2017, for an Apache Struts command execution flaw that US-CERT had flagged two days earlier, then using it to reach the online dispute portal. The same report notes that Equifax’s own scan a week later did not detect the vulnerability on that portal. CVE-2017-5638 is also listed in the CISA Known Exploited Vulnerabilities catalog.
The limits: only applications that load an affected struts2-core and use the Jakarta parser (the default) are exploitable. A stray jar in a backup folder is an inventory problem, not an open door.
How to confirm it on the host
Find every struts2-core jar on Linux
# Loose jars (exploded webapps, lib folders)
sudo find / -name 'struts2-core*.jar' 2>/dev/null
# Jars packaged inside WAR files, plus WARs nested inside EAR files
sudo find / ( -name '*.war' -o -name '*.ear' ) 2>/dev/null |
while read -r f; do
unzip -l "$f" 2>/dev/null | grep -E 'struts2-core.*.jar|.war$' | sed "s|^|$f: |"
done
A WAR listed inside an EAR has to be extracted and checked on its own. File names can be renamed, so read the real version from the Maven metadata in the jar:
unzip -p struts2-core-2.3.31.jar META-INF/maven/org.apache.struts/struts2-core/pom.properties
Find it on Windows
Add-Type -AssemblyName System.IO.Compression.FileSystem
Get-ChildItem C: -Recurse -Include 'struts2-core*.jar' -ErrorAction SilentlyContinue | Select-Object FullName
Get-ChildItem C: -Recurse -Include '*.war','*.ear' -ErrorAction SilentlyContinue | ForEach-Object {
$archive = $_.FullName
$zip = [System.IO.Compression.ZipFile]::OpenRead($archive)
$zip.Entries | Where-Object { $_.Name -like 'struts2-core*.jar' -or $_.Name -like '*.war' } |
ForEach-Object { "{0}: {1}" -f $archive, $_.FullName }
$zip.Dispose()
}
Check the build and the parser setting
mvn dependency:tree -Dincludes=org.apache.struts
./gradlew dependencyInsight --dependency struts2-core --configuration runtimeClasspath
grep -rn 'struts.multipart.parser' src/main/resources WEB-INF/classes 2>/dev/null
No struts.multipart.parser constant means the default jakarta parser is in use. Dependency scanning in the build (SCA) finds Struts that a crawler never reaches; see how SAST, DAST and SCA differ for why both views matter here.
How to fix CVE-2017-5638
Pick the target version
| Current line | Minimum that closes this CVE | Recommended target |
|---|---|---|
| 2.3.5 to 2.3.31 | 2.3.32 | Current 6.x or 7.x (2.3.x reached end of life on 12 September 2019) |
| 2.5 to 2.5.10 | 2.5.10.1 | Current 6.x or 7.x (2.5.x reached end of life on 30 October 2023) |
A patched 2.3.x or 2.5.x build clears this scanner finding, but Apache no longer ships security fixes for those lines, so it only trades one finding for later ones. Take the version number from the Struts download page on the day you build.
Update a Maven or Gradle build
<properties>
<struts2.version>CURRENT_6_OR_7_RELEASE</struts2.version>
</properties>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>${struts2.version}</version>
</dependency>
Use the same property for every org.apache.struts plugin artifact, then run mvn clean package and repeat the dependency tree check so no transitive copy of an old core survives. Gradle projects change the version in the dependency declaration and rerun dependencyInsight.
Redeploy cleanly (Tomcat example)
sudo systemctl stop tomcat
sudo cp /opt/tomcat/webapps/myapp.war /root/myapp.war.pre-upgrade
sudo rm -rf /opt/tomcat/webapps/myapp /opt/tomcat/work/Catalina/localhost/myapp
sudo cp target/myapp.war /opt/tomcat/webapps/
sudo systemctl start tomcat
Service names and paths vary (tomcat9, a separate CATALINA_BASE). Removing the exploded folder and the work directory stops Tomcat from serving old classes and compiled JSPs, which Apache’s 2.3 to 2.5 migration notes also advise.
Vendor products that bundle Struts
Tenable maps this CVE to Oracle WebLogic Server, MySQL Enterprise Monitor, Oracle WebCenter Sites and Siebel CRM through their 2017 Critical Patch Updates. For those, apply the vendor patch instead of swapping jars inside the product.
Stopgaps while you rebuild
- Restrict exposure. Put the application behind a VPN or an allow list until the new build ships.
- Header filtering. S2-045 suggests a servlet filter (or an equivalent WAF rule) that drops requests whose Content-Type does not match multipart/form-data. It does not cover the S2-046 headers, so treat it as partial.
- Different parser or no upload interceptor. S2-045 and S2-046 describe switching to an alternative multipart parser, or, on 2.5.8 to 2.5.10 only, a custom default interceptor stack without the File Upload Interceptor. Copy the exact configuration from the bulletins.
Check exposed hosts for compromise
- List child processes of the JVM. Shells spawned by Tomcat are a red flag: ps –ppid $(pgrep -f catalina | head -1) -o pid,user,etime,cmd, or on Windows Get-CimInstance Win32_Process | Where-Object ParentProcessId -eq <java_pid>.
- Compare JSP and class files in the deployed webapp against the build artifact to spot web shells.
- Default access logs do not record request headers. Add %{Content-Type}i to the AccessLogValve pattern (or log it at the proxy) so future attempts are visible.
How to verify the fix and rescan
- Rerun the jar search and the pom.properties check against the deployed webapp, not only the build output.
- Confirm the application starts and a real upload form still works.
- Rescan with a Nessus policy that includes 97610 and has web application tests enabled. If the crawler missed the Struts URLs the first time, add them to the crawl start list (Tenable’s post notes multiple URLs are separated by colons).
- Rerun the Tenable WAS scan against the same target so both findings close.
What can break and how to roll back
| Upgrade path | What may break |
|---|---|
| 2.3 to 2.5 | Filter class moves from org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter to org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter in web.xml; DTD moves to 2.5; several tags use var instead of id; Dojo, Codebehind, JSF and Struts1 plugins are gone; only Tiles 3 is supported |
| 2.5 to 6.x | Java 8 and a Servlet 3.1 container required; DTD 6.0; XWork constants renamed; OGNL cannot call static methods and expressions are capped at 256 characters by default |
| 6.x to 7.x | Java 17 minimum; Jakarta EE namespace, so the container must support jakarta.servlet (the Struts 7 build targets Jakarta EE 10, which for Tomcat means 10.1 or later, not 9); com.opensymphony.xwork2 classes move to org.apache.struts2; the old FileUploadInterceptor is removed in favor of ActionFileUploadInterceptor |
Rollback is the saved WAR: stop the container, restore the old file, clear the exploded and work folders, start again. Rolling back reopens CVE-2017-5638, so pair it with the stopgaps above and keep the window short.
Common false positive reasons
- Wrong asset. The IP belongs to a reverse proxy or load balancer, so the finding belongs to a backend server you have not looked at.
- Stale results. The scan predates the redeploy, or an old exploded folder was still being served.
- Dead jars. Local and SCA checks flag struts2-core copies in backups or unused folders. Remove them anyway so the inventory stays clean.
- Bundled products. A vendor product patched through its own update can still trip version-based checks; confirm with the vendor’s patch level.
The opposite problem is more common: a clean remote scan often only means the crawler never found a Struts action.
FAQ
Our app has no upload form. Is it still vulnerable?
Assume yes. The attack is carried in request headers that the multipart parser processes, Jakarta is the default parser, and Nessus 97610 tests ordinary crawled URLs rather than upload pages.
Is a WAF rule on Content-Type enough?
No. S2-046 describes the same flaw through Content-Disposition and Content-Length, and header filtering is a stopgap even when it covers all three.
Does upgrading to 2.3.32 close the finding?
It closes this CVE, but the 2.3 line has been end of life since 2019 and misses later Struts fixes. Move to 6.x or 7.x.
Is CVE-2017-5638 actively exploited?
Yes. It is in the CISA KEV catalog and public exploit modules have existed since 2017.
Tracking this finding across many hosts
When the same Struts app is deployed on several servers, the hard part is proving each copy was rebuilt. SITEY, a self-hosted vulnerability management platform, imports Nessus results from an uploaded .nessus export, adds CISA KEV and FIRST EPSS context, and can re-test an individual Nessus finding such as 97610 after the redeploy. Duplicates are merged per scanner, so a host reported by Nessus and by a second scanner still appears as two findings.