This is a fire door that turns into a command prompt when someone writes the right word on it
CVE-2013-2251 is the old Struts DefaultActionMapper bug behind security bulletin *S2-016*. In Apache Struts 2.0.0 through 2.3.15, parameters prefixed with action:, redirect:, or redirectAction: can be evaluated as OGNL, which lets an unauthenticated remote attacker turn an HTTP request into server-side code execution if the vulnerable Struts stack is reachable.
The vendor's *CRITICAL* label matches reality for any internet-reachable instance, even though the exposed population is narrower today because this only hits legacy Struts 2 builds. The main downward pressure is age and shrinking install base; the main upward pressure is stronger: no auth, low complexity, public exploit tooling, early exploitation reports, and current KEV status.
4 steps from start to impact.
Find a legacy Struts endpoint
httpx, nuclei, or basic manual browsing, because the target is just a web app that answers HTTP.- The application is reachable over the network, typically HTTP/HTTPS
- A vulnerable Struts 2 application is still deployed
- The attacker can hit a route processed by Struts
- Many Struts apps are now internal-only or retired
- Reverse proxies and generic banners make product fingerprinting noisy
- Not every Java app is Struts, and not every Struts app is this old
Deliver the prefixed-parameter payload
action:, redirect:, or redirectAction: parameter prefix to smuggle an OGNL expression into the value stack. Public weaponization exists in Packet Storm and Rapid7's Metasploit module exploit/multi/http/struts_default_action_mapper.- Request parameters reach Struts without being normalized or blocked
- The vulnerable
DefaultActionMapperpath is active - No WAF/IPS rule blocks obvious OGNL or prefix abuse
- Custom URL mappings can make the right endpoint less obvious
- Some WAFs and IPS signatures catch the classic payloads
- App-specific parameter filtering can break commodity exploit strings
action: or redirect: plus OGNL metacharacters. Scanner coverage exists, but exploitability checks can miss apps behind custom routing.Trigger OGNL evaluation inside Struts
- The deployed
struts2-coreversion is earlier than2.3.15.1 - The target request path is actually handled by vulnerable Struts code
- The Java process has enough permissions to make post-exploitation useful
- If the app runs with reduced OS permissions, immediate blast radius is smaller
- If the app is patched, backported, or fronted by filtering, the chain dies here
- Some legacy apps bundle Struts in non-obvious ways, complicating both exploit and defender validation
java spawning shells, script interpreters, or suspicious child processes is high-signal. Web logs may also show malformed OGNL syntax or failed probes.Convert web access into persistence or data access
- Successful code execution has already been achieved
- The app server can write files, spawn processes, or reach internal resources
- Outbound traffic or internal pivot paths are available
- EDR, egress controls, and non-root service accounts reduce follow-on options
- Containerized or locked-down app servers limit persistence
- Short-lived nodes may reduce durability unless the attacker gets app-level persistence
The supporting signals.
| In-the-wild status | Yes. CISA KEV lists it as exploited in the wild, and SANS ISC reported exploitation attempts starting 2013-07-17 with additional reports by 2013-08-12. |
|---|---|
| Public exploit availability | Mature and commoditized. Rapid7 ships exploit/multi/http/struts_default_action_mapper; Packet Storm and Exploit-DB also carry public exploit material. |
| EPSS | 0.94325 from the prompt, which is extremely high and consistent with a top-tier exploitation likelihood band. |
| KEV status | Listed in CISA KEV on 2022-03-25 with a federal due date of 2022-04-15. |
| CVSS vector readout | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H means unauthenticated remote code execution over the network with no user interaction and full CIA impact. |
| Affected versions | Apache Struts 2.0.0 through 2.3.15 are affected for *S2-016* / CVE-2013-2251. |
| Fixed versions | Baseline fix is 2.3.15.1. In practice, many estates should skip straight to a supported Struts line, because 2.3.x itself is long end-of-life. |
| Scanning and exposure reality | SANS ISC saw exploitation traffic within a day of the Apache announcement. Separately, Sonatype still reports heavy consumption of old Struts lines, which is a strong supply-chain signal that vulnerable legacy code remains in real environments even if precise live internet counts vary. |
| Disclosure timeline | Apache shipped the fix and announced *S2-016* on 2013-07-16; NVD publication followed on 2013-07-20. |
| Researcher / reporting trail | Apache tracked the fix under WW-4140; public exploit/module credits include Takeshi Terada, sinn3r, and juan vazquez in Rapid7's module history. |
noisgate verdict.
The decisive factor is attacker position: this is unauthenticated remote code execution against a web app, and KEV status proves the exploit path is not theoretical. The only real friction is exposure population—these are old Struts builds—but when they still exist they are usually exactly the kind of forgotten internet-facing apps attackers love.
Why this verdict
- No-auth remote RCE keeps the vendor baseline high: attacker position is unauthenticated remote, which implies this can be the *initial access* event, not merely post-compromise abuse.
- KEV and observed exploitation add upward pressure: CISA lists it as known exploited, and SANS saw exploit traffic almost immediately after disclosure, so this is a field-proven path.
- Low operational friction matters: public Metasploit support and long-lived PoC availability mean the skill floor is low once a vulnerable endpoint is found.
- Legacy-only exposure trims the score slightly: requiring Struts
<=2.3.15narrows the reachable population because many organizations have already retired or hidden these apps. - Blast radius is usually server-side and meaningful: successful OGNL execution lands in the app server context, which often exposes secrets, databases, and internal trust paths.
Why not higher?
The only reason this is not a perfect 10 is population friction. You need an actually exposed, actually old Struts deployment, and that is a smaller slice of the web than it was in 2013. Some modern controls also break commodity payloads before code execution.
Why not lower?
Dropping this to HIGH would underweight the two facts that matter most: no authentication required and known exploitation in the wild. This is not a post-auth admin bug or a lab-only edge case; if you have the exposure, attackers can use it as an entry point.
What to do — in priority order.
- Block external reachability — Take internet-facing vulnerable Struts applications off direct exposure or put them behind tightly filtered access paths within hours, because active-exploitation evidence overrides normal deadlines. If the app cannot be removed from exposure, restrict source IPs and remove public routing while patching is staged.
- Enable WAF or IPS signatures — Deploy rules that detect or block
action:,redirect:, andredirectAction:prefix abuse plus OGNL patterns within hours. This is a compensating control, not a fix, but it meaningfully raises attacker friction for commodity tooling. - Hunt for OGNL probe artifacts — Search reverse-proxy, WAF, and app logs for suspicious prefixed parameters and OGNL syntax within hours. Assume that internet-facing legacy Struts apps have already been probed if they were reachable.
- Constrain the app-server account — Reduce file-write, process-spawn, and outbound-network privileges for the Java service account within 3 days to shrink post-exploitation blast radius if a hit slips through.
- Inventory embedded Struts jars — Scan WARs, EARs, container images, and middleware bundles for
struts2-coreversions within 3 days. The hard part in real estates is not understanding the bug; it is finding the buried copy in an old app you forgot existed.
- MFA does nothing here because exploitation is unauthenticated and happens before any user login.
- Relying on EDR alone is too late; it may catch shelling or persistence, but it does not prevent the initial OGNL execution.
- Generic vulnerability scans without version-aware package inspection are not enough because Struts is often bundled inside application archives and middleware rather than installed as a neat OS package.
Crowdsourced verification payload.
Run this on the target host or on an auditor workstation with read access to deployed app directories, WARs, EARs, or container filesystems. Invoke it as python3 check_struts_cve_2013_2251.py /opt /srv/tomcat/webapps or point it at a mounted image path; no admin rights are required unless the directories need elevated read access.
#!/usr/bin/env python3
# check_struts_cve_2013_2251.py
# Detect vulnerable Apache Struts 2 versions for CVE-2013-2251 (S2-016)
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN, 3=USAGE
import os
import re
import sys
import zipfile
from pathlib import Path
TARGET_FIXED = (2, 3, 15, 1)
JAR_RE = re.compile(r"struts2-core-([0-9][0-9A-Za-z_.-]*)\.jar$", re.IGNORECASE)
POM_PATH_RE = re.compile(r"META-INF/maven/.*/struts2-core/pom.properties$")
def normalize_version(v):
v = v.strip()
parts = re.split(r"[._-]", v)
out = []
for p in parts:
if p.isdigit():
out.append(int(p))
else:
m = re.match(r"(\d+)", p)
if m:
out.append(int(m.group(1)))
else:
out.append(0)
while len(out) < 4:
out.append(0)
return tuple(out[:4])
def version_lt(a, b):
return a < b
def extract_version_from_jar_name(name):
m = JAR_RE.search(name)
if not m:
return None
return m.group(1)
def extract_version_from_zip(path):
try:
with zipfile.ZipFile(path, 'r') as zf:
for member in zf.namelist():
base = os.path.basename(member)
v = extract_version_from_jar_name(base)
if v:
return v
if POM_PATH_RE.search(member):
try:
data = zf.read(member).decode('utf-8', errors='ignore')
for line in data.splitlines():
if line.startswith('version='):
return line.split('=', 1)[1].strip()
except Exception:
pass
except Exception:
return None
return None
def inspect_path(root):
findings = []
root = Path(root)
if not root.exists():
return findings
for path in root.rglob('*'):
if not path.is_file():
continue
lower = path.name.lower()
if lower.endswith('.jar'):
v = extract_version_from_jar_name(path.name)
if v:
findings.append((str(path), v, 'direct-jar'))
continue
if lower.endswith(('.war', '.ear', '.jar')):
v = extract_version_from_zip(path)
if v:
findings.append((str(path), v, 'archive'))
return findings
def main():
if len(sys.argv) < 2:
print('UNKNOWN - usage: python3 check_struts_cve_2013_2251.py <path1> [path2 ...]')
sys.exit(3)
all_findings = []
for arg in sys.argv[1:]:
all_findings.extend(inspect_path(arg))
if not all_findings:
print('UNKNOWN - no struts2-core artifacts found in supplied paths')
sys.exit(2)
vulnerable = []
patched = []
unknown = []
for location, raw_ver, source in all_findings:
try:
norm = normalize_version(raw_ver)
if version_lt(norm, TARGET_FIXED):
vulnerable.append((location, raw_ver, source))
else:
patched.append((location, raw_ver, source))
except Exception:
unknown.append((location, raw_ver, source))
if vulnerable:
print('VULNERABLE')
for location, raw_ver, source in vulnerable:
print(f' {location} | version={raw_ver} | source={source}')
sys.exit(1)
if patched and not vulnerable:
print('PATCHED')
for location, raw_ver, source in patched:
print(f' {location} | version={raw_ver} | source={source}')
if unknown:
for location, raw_ver, source in unknown:
print(f' NOTE: unable to fully classify {location} | version={raw_ver} | source={source}')
sys.exit(0)
print('UNKNOWN - artifacts found but version classification failed')
for location, raw_ver, source in unknown:
print(f' {location} | version={raw_ver} | source={source}')
sys.exit(2)
if __name__ == '__main__':
main()
If you remember one thing.
2.0.0 through 2.3.15 as an emergency exposure: identify them today, remove public reachability or apply a reliable blocking control immediately, within hours, because KEV status overrides the normal noisgate mitigation SLA. Then complete actual remediation—upgrade at minimum past 2.3.15.1, and preferably off the dead 2.3.x line entirely—or retire the app under tracked exception by the noisgate remediation SLA of 90 days for a CRITICAL issue.Sources
- NVD CVE-2013-2251
- Apache Struts Announcements 2013
- Apache Struts Announcements 2021 security impact level update
- CISA Known Exploited Vulnerabilities Catalog entry
- SANS ISC diary on CVE-2013-2251 exploitation
- Rapid7 Metasploit module: struts_default_action_mapper
- GitLab advisory for CVE-2013-2251
- Apache JIRA WW-4140
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.