← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2013-2251 · CWE-74 · Disclosed 2013-07-20

Apache Struts 2

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

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.

"KEV-listed unauthenticated Struts RCE on exposed legacy apps is still a stop-everything problem."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a legacy Struts endpoint

An attacker fingerprints a Java web app and looks for Struts-backed actions or routes that accept request parameters. Common tooling is httpx, nuclei, or basic manual browsing, because the target is just a web app that answers HTTP.
Conditions required:
  • 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
Where this breaks in practice:
  • 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
Detection/coverage: External attack-surface tools can find the hostname, but product fingerprinting is imperfect. ASM/EASM coverage is good for exposure, weaker for exact vulnerable versioning.
STEP 02

Deliver the prefixed-parameter payload

The attacker sends a crafted request using the 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.
Conditions required:
  • Request parameters reach Struts without being normalized or blocked
  • The vulnerable DefaultActionMapper path is active
  • No WAF/IPS rule blocks obvious OGNL or prefix abuse
Where this breaks in practice:
  • 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
Detection/coverage: WAF, reverse-proxy, and app logs can catch suspicious parameter names containing action: or redirect: plus OGNL metacharacters. Scanner coverage exists, but exploitability checks can miss apps behind custom routing.
STEP 03

Trigger OGNL evaluation inside Struts

On vulnerable versions, the prefix content is not sanitized before OGNL evaluation, so the framework evaluates attacker-controlled server-side expressions. That converts a single HTTP request into arbitrary code execution in the security context of the Java web process.
Conditions required:
  • The deployed struts2-core version is earlier than 2.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
Where this breaks in practice:
  • 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
Detection/coverage: This is where EDR helps: java spawning shells, script interpreters, or suspicious child processes is high-signal. Web logs may also show malformed OGNL syntax or failed probes.
STEP 04

Convert web access into persistence or data access

After code execution, the attacker typically drops a webshell, runs OS commands, or pivots into app secrets and backend systems. Commodity post-exploitation uses the usual toolchain: shell one-liners, file writes, credential theft, and outbound callbacks.
Conditions required:
  • 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
Where this breaks in practice:
  • 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
Detection/coverage: EDR, process creation telemetry, file integrity monitoring, and unusual outbound network connections are the best catches here. Vulnerability scanners do not help once exploitation has moved to post-compromise behavior.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusYes. 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 availabilityMature and commoditized. Rapid7 ships exploit/multi/http/struts_default_action_mapper; Packet Storm and Exploit-DB also carry public exploit material.
EPSS0.94325 from the prompt, which is extremely high and consistent with a top-tier exploitation likelihood band.
KEV statusListed in CISA KEV on 2022-03-25 with a federal due date of 2022-04-15.
CVSS vector readoutCVSS: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 versionsApache Struts 2.0.0 through 2.3.15 are affected for *S2-016* / CVE-2013-2251.
Fixed versionsBaseline 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 realitySANS 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 timelineApache shipped the fix and announced *S2-016* on 2013-07-16; NVD publication followed on 2013-07-20.
Researcher / reporting trailApache tracked the fix under WW-4140; public exploit/module credits include Takeshi Terada, sinn3r, and juan vazquez in Rapid7's module history.
04 · The Call

noisgate verdict.

Final Verdict
= UNCHANGED to CRITICAL (9.5/10)

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.

HIGH Exploitability of an exposed vulnerable instance
HIGH KEV / historical exploitation evidence
MEDIUM How many vulnerable instances remain externally reachable in a given enterprise

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.15 narrows 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.

05 · Compensating Control

What to do — in priority order.

  1. 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.
  2. Enable WAF or IPS signatures — Deploy rules that detect or block action:, redirect:, and redirectAction: prefix abuse plus OGNL patterns within hours. This is a compensating control, not a fix, but it meaningfully raises attacker friction for commodity tooling.
  3. 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.
  4. 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.
  5. Inventory embedded Struts jars — Scan WARs, EARs, container images, and middleware bundles for struts2-core versions 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.
What doesn't work
  • 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.
06 · Verification

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.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/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()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, treat every internet-facing Struts 2 app on versions 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

  1. NVD CVE-2013-2251
  2. Apache Struts Announcements 2013
  3. Apache Struts Announcements 2021 security impact level update
  4. CISA Known Exploited Vulnerabilities Catalog entry
  5. SANS ISC diary on CVE-2013-2251 exploitation
  6. Rapid7 Metasploit module: struts_default_action_mapper
  7. GitLab advisory for CVE-2013-2251
  8. Apache JIRA WW-4140
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.