Skip to content

SOAP Basics

Once you have the WSDL, manual SOAP requests are mechanical. The minimum payload structure for any SOAP request is the same; only the operation name and parameter list change per call.

# Template - replace OPERATION and PARAMS for each operation
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://tempuri.org/">
<soap:Body>
<OPERATIONRequest xmlns="http://tempuri.org/">
<param1>value1</param1>
<param2>value2</param2>
</OPERATIONRequest>
</soap:Body>
</soap:Envelope>
# Send via curl
curl -X POST http://target:3002/wsdl \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "Login"' \
--data @login.xml
# Or via Python
requests.post(url, data=payload, headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"Login"',
})

Success indicator: a 200 response with a <OPERATIONResponse> body element containing the operation’s output. A <soap:Fault> response means the request reached the parser but failed validation.

A SOAP request needs three pieces:

  1. Envelope - XML wrapper that declares the SOAP namespace
  2. Body - contains the operation name and parameters
  3. HTTP headers - Content-Type: text/xml (or application/soap+xml for SOAP 1.2) and SOAPAction: "<operation>"

Everything else (Header block, Fault block) is optional. The minimum working request:

<?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>password</password>
</LoginRequest>
</soap:Body>
</soap:Envelope>

Sent as:

POST /wsdl HTTP/1.1
Host: target:3002
Content-Type: text/xml; charset=utf-8
SOAPAction: "Login"
Content-Length: 198
<?xml version="1.0" encoding="utf-8"?>...

That’s the whole protocol. The XML can be on one line or pretty-printed; both work.

Given this WSDL fragment from Discovery:

<wsdl:types>
<s:element name="LoginRequest">
<s:complexType>
<s:sequence>
<s:element minOccurs="1" maxOccurs="1" name="username" type="s:string"/>
<s:element minOccurs="1" maxOccurs="1" name="password" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>
</wsdl:types>
<wsdl:binding name="ExampleServiceSoapBinding" type="tns:ExampleSoapPort">
<wsdl:operation name="Login">
<soap:operation soapAction="Login" style="document"/>
...
</wsdl:operation>
</wsdl:binding>

The mapping to a request:

WSDL element Where it appears in the request
<s:element name="LoginRequest"> Root element inside <soap:Body>
targetNamespace="http://tempuri.org/" xmlns="..." attribute on the root operation element
<s:element name="username" type="s:string"/> <username>VALUE</username> child of the operation element
<s:element name="password" type="s:string"/> <password>VALUE</password> child of the operation element
soapAction="Login" (from binding) SOAPAction: "Login" HTTP header

So the full request becomes:

<?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>password</password>
</LoginRequest>
</soap:Body>
</soap:Envelope>

The xmlns="http://tempuri.org/" on <LoginRequest> is critical - without it the server may not recognize the operation. This namespace comes from the WSDL’s targetNamespace attribute.

You’ll see different namespace styles in the wild. All three of these are equivalent:

<!-- Style 1: default namespace on operation element -->
<soap:Body>
<LoginRequest xmlns="http://tempuri.org/">
<username>admin</username>
</LoginRequest>
</soap:Body>
<!-- Style 2: namespace prefix declared upfront, used on every element -->
<soap:Envelope xmlns:soap="..." xmlns:tns="http://tempuri.org/">
<soap:Body>
<tns:LoginRequest>
<tns:username>admin</tns:username>
</tns:LoginRequest>
</soap:Body>
</soap:Envelope>
<!-- Style 3: explicit prefix only on operation, default on children -->
<soap:Body>
<tns:LoginRequest xmlns:tns="http://tempuri.org/">
<username>admin</username>
</tns:LoginRequest>
</soap:Body>

When testing, start with style 1 (simplest) and only complicate if the server rejects. Some servers are strict about namespace matching - if a request fails with “operation not found,” try moving the namespace declaration or adding a tns: prefix to the operation element.

Terminal window
# 1. Save the envelope to a file
$ cat > /tmp/login.xml <<'EOF'
<?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>P@ssw0rd</password>
</LoginRequest>
</soap:Body>
</soap:Envelope>
EOF
# 2. Send
$ curl -X POST http://target:3002/wsdl \
-H 'Content-Type: text/xml; charset=utf-8' \
-H 'SOAPAction: "Login"' \
--data @/tmp/login.xml
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<LoginResponse xmlns="http://tempuri.org/">
<success>true</success>
<result>Welcome, admin</result>
</LoginResponse>
</soap:Body>
</soap:Envelope>

Three points:

  • Content-Type must be text/xml or application/soap+xml. Without it, many servers return 415 Unsupported Media Type.
  • SOAPAction value is quoted - SOAPAction: "Login" not SOAPAction: Login. Most servers tolerate the unquoted form, but the spec requires quotes.
  • --data @file.xml reads from a file. For inline --data '<?xml...>', escape internal quotes carefully or use --data-binary @file.
Error Cause
415 Unsupported Media Type Missing or wrong Content-Type header
400 Bad Request Malformed XML - usually missing namespace declaration
500 Internal Server Error + <soap:Fault> Operation reached parser but parameters invalid or auth failed
Empty response Sometimes a SOAP server returns 200 with empty body when the operation succeeded but produced no output - check status code, not body length
404 Wrong endpoint URL (not the WSDL location); check <soap:address location="..."/>

For automation, scripting against multiple operations, or when you need to manipulate the payload programmatically:

import requests
payload = '''<?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>P@ssw0rd</password>
</LoginRequest>
</soap:Body>
</soap:Envelope>'''
response = requests.post(
'http://target:3002/wsdl',
data=payload,
headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"Login"',
},
)
print(response.text)
import requests
URL = 'http://target:3002/wsdl'
NAMESPACE = 'http://tempuri.org/'
def soap_call(operation, params, soap_action=None):
"""Make a SOAP call. operation is the element name; params is a dict."""
soap_action = soap_action or operation
body_inner = ''.join(f'<{k}>{v}</{k}>' for k, v in params.items())
payload = f'''<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<{operation}Request xmlns="{NAMESPACE}">
{body_inner}
</{operation}Request>
</soap:Body>
</soap:Envelope>'''
return requests.post(URL, data=payload, headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': f'"{soap_action}"',
})
# Call each operation from the WSDL
print(soap_call('Login', {'username': 'admin', 'password': 'P@ssw0rd'}).text)
print(soap_call('ExecuteCommand', {'cmd': 'whoami'}).text)

This pattern is useful when you’ve discovered many operations and want to systematically test each one.

For command-injection / RCE-style SOAP services, an interactive shell is convenient:

import requests
URL = 'http://target:3002/wsdl'
while True:
try:
cmd = input('$ ')
except (EOFError, KeyboardInterrupt):
break
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/">
<cmd>{cmd}</cmd>
</LoginRequest>
</soap:Body>
</soap:Envelope>'''
r = requests.post(URL, data=payload, headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': '"ExecuteCommand"',
})
print(r.text)

Reads commands from stdin in a loop, fires each as a SOAP request. Useful for the SOAPAction-spoofing scenario where the body operation differs from the SOAPAction header - see SOAPAction spoofing.

soapUI is a GUI tool for SOAP testing. When manual curl gets tedious:

Terminal window
# Install (Linux)
$ sudo apt install soapui
# Or download from soapui.org for Windows/Mac

Workflow:

  1. New SOAP Project → paste WSDL URL or upload .wsdl file
  2. soapUI auto-generates request templates for every operation
  3. Edit the parameters and click “Submit Request”
  4. Response renders in a parallel panel

soapUI strengths:

  • Automatic envelope construction from WSDL
  • Schema validation before sending
  • Built-in WS-Security support (for signed/encrypted SOAP)
  • Saved test suites for reuse

For penetration testing specifically, soapUI is comfortable for exploration but Burp Suite (intercepting your hand-crafted curl requests) is usually faster for adversarial probing - you have more direct control over the bytes.

A common workflow:

  1. Use soapUI to generate the request template from the WSDL
  2. Copy the request into Burp Repeater
  3. From Burp, modify, fuzz, scan, etc.

soapUI does the boilerplate, Burp does the attack tooling.

When a request fails inside the SOAP layer (rather than at the HTTP layer), the server returns a <soap:Fault> element with diagnostic information:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>Object reference not set to an instance of an object.</faultstring>
<detail>
<ExceptionType>System.NullReferenceException</ExceptionType>
<StackTrace>at MyService.Authenticate(String username, String password)</StackTrace>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>

<faultcode> is one of:

Code Meaning
soap:VersionMismatch SOAP envelope namespace wrong
soap:MustUnderstand Required <soap:Header> block missing
soap:Client Request was malformed or invalid
soap:Server Server-side error processing the request

<faultstring> is human-readable. <detail> (optional) often leaks information valuable to the attacker: exception types, stack traces, internal class names, file paths.

When a fault response contains a stack trace, it’s the SOAP equivalent of a development-mode error page - read it carefully, it often reveals the back-end framework (.NET WCF, Java JAX-WS, PHP NuSOAP), the database (System.Data.SqlClient.SqlException), and the actual SQL or operation that failed.

Particularly useful: SQL errors that surface through SOAP faults give you a clear “your injection worked syntactically, but here’s the error from the DB” signal:

<faultstring>
SQLite error: near "'": syntax error
</faultstring>

That’s a SQLite injection-positive signal - see Capstone chain for the full SQL injection through SOAP walkthrough.

Two versions of the SOAP spec exist. Quick distinguishing features:

Feature SOAP 1.1 SOAP 1.2
Envelope namespace http://schemas.xmlsoap.org/soap/envelope/ http://www.w3.org/2003/05/soap-envelope
Content-Type text/xml application/soap+xml
SOAPAction header Required Replaced by action parameter in Content-Type
Fault structure <faultcode>, <faultstring> <Code>, <Reason>, etc.

Almost everything you encounter in the wild is SOAP 1.1. WCF services sometimes expose both via different endpoints (?wsdl vs ?singleWsdl). For 1.2:

POST /wsdl HTTP/1.1
Content-Type: application/soap+xml; charset=utf-8; action="Login"
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
...
</soap:Envelope>

Note: no separate SOAPAction: header in 1.2 - the action moved into Content-Type. Otherwise the payload structure is similar.

Task Pattern
Minimum envelope <soap:Envelope xmlns:soap="..."><soap:Body><OPRequest xmlns="..."><param>v</param></OPRequest></soap:Body></soap:Envelope>
SOAP namespace 1.1 http://schemas.xmlsoap.org/soap/envelope/
SOAP namespace 1.2 http://www.w3.org/2003/05/soap-envelope
Content-Type 1.1 text/xml; charset=utf-8
Content-Type 1.2 application/soap+xml; charset=utf-8; action="OP"
SOAPAction header SOAPAction: "OperationName" (quoted)
Send via curl curl -X POST URL -H 'Content-Type: text/xml' -H 'SOAPAction: "OP"' --data @file.xml
Python client requests.post(URL, data=payload, headers={...})
GUI client soapUI (load WSDL, click submit)
Fault response indicates Request reached parser; check <faultstring> for details
415 response Wrong Content-Type
400 response Malformed XML
500 + Fault Server-side error, often informative
Generate templates from WSDL soapUI auto-generation, or wsdl2py/wsdl2java/svcutil.exe

For the canonical SOAP-specific bypass attack, continue to SOAPAction spoofing. For the end-to-end capstone chain combining SOAPAction spoofing with SQL injection, see Capstone chain.