chroot&namespace逃逸总结
0x00 参考文章(前置知识)
超详解的Linux内核进程描述符task_struct结构体
0x01 chroot
功能简述
chroot 系统调用执行时需要 privilege,一般情况下需要 root 权限。被执行的命令或程序,需要在被限制的目录下,如sudo chroot /tmp /bin/bash
,这种情况下,在 /tmp/bin 目录中需要有bash文件。
简单来说,chroot的功能是改变/
对于当前进程的含义。例如执行了chroot /tmp/jail /bin/bash
,那么你的bash的根目录就会被视作是从/tmp/jail
开始的,如果在jail中输入/
,在宿主机中相当于会跳转到/tmp/jail
。chroot的功能有且仅有改变根目录的作用,其他的一律没有做隔离,所以chroot是非常脆弱的,如果被错误配置(例如jail拥有root权限、未设置工作目录等),并且运行的服务有被rce的可能,是能够结合在一起做到逃逸的。
从内核视角看chroot
Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。在task_struct中和文件相关的成员是这些:
1 | /* file system info */ |
其中fs_struct
结构体长这样:
1 | struct fs_struct { |
root:根目录的目录项
pwd:当前工作目录的目录项
altroot:模拟根目录的目录项
files_struct长这样:
1 | struct files_struct { |
而chroot仅改变了fs_struct下的root成员。
未显性设置工作目录到jail中(ubuntu24不适用)
从根目录开始展开的文件路径叫绝对路径,那么在jail中绝对路径一定会从/tmp/jail
开始,没法逃逸,那么相对路径呢?相对路径是相对于工作目录开始的,然而chroot只改变了进程的根目录,并不会改变工作目录。换句话来说,如果并没有在chroot之后,人为地调用chdir("/")
,那么我们的工作目录可能依然在jail之外,那么就可以通过相对路径来获取到jail外的文件信息,甚至rce。
例子:/flag需要root权限才能读取,有一个被运行于/tmp/jail中的低版本busybox(拥有suid权限),工作目录在/tmp,那么可以在jail中使用cat ../flag
利用程度的suid权限读取到jail外的flag。
如果工作目录设置了在jail内,那么相对路径无论再怎么穿越目录也只会停留在/tmp/jail中
在进程被jail之前有打开且可用的目录fd
正如一开始所说,chroot只改变了根目录,其他东西一律没有隔离,fd也不例外。假如某个程序在开启chroot之前先行打开了某个jail外的目录,fd为3,那么后续我们可以利用fchdir系统调用来将工作目录跳转到刚刚打开的那个目录。
1 | int chdir(const char *path); |
同理,如果设置了工作目录在jail内,那么chdir也没办法改变工作目录,但是fd则不受限制。
如果只是想打开文件,那么也可以考虑使用openat,如果pathname是一个相对路径,那么相对的就是fd对应的目录:
1 | int openat(int dirfd, const char *pathname, int flags, mode_t mode); |
如果openat被禁用了,也可以考虑使用linkat,创建指向现有文件的新链接,支持相对路径和绝对路径的处理(假如已打开的目录是/,fd为3):
1 | int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flags); |
这样就可以将原本根目录下的flag链接到/tmp/jail/flag
,后续就可以通过/flag来打开源文件了
嵌套chroot逃逸(ubuntu24不适用)
前面讲到task_struct结构体用来管理进程,如果在jail中可以使用chroot(有这个程序并且有root权限),那么在jail中执行一个新的chroot,就会覆盖掉task_struct结构体原本的root路径,然后不设置工作目录,这样一来,工作目录又存在于jail之外了,又可以利用相对路径来访问jail外的文件了。这是最经典的chroot逃逸方法。
利用root权限kill进程
如果chroot没设置权限,默认root权限进入jail的情况下,在jail中使用kill -9 <pid>
来强制kill掉chroot进程。当然如果有守护进程或者没有root权限,那这个办法就无效了。ubuntu24下实测有效。
关于这个方法,网上更多的说法是另开一个shell来kill,但我想不到这个做法的意义,既然能够另外开一个shell在jail外而且有权限kill那为什么还要……
0x02 namespace
命名空间是一个较为现代化的内核级别环境隔离的方法,而且提供了较多的隔离选项,包含了对 UTS、IPC、Mount、PID、Network、User 等的隔离机制,不像chroot只能隔离根目录。namespace同时也是容器技术的隔离底层实现。
在这些隔离选项当中,mount是出现最早、应用最广泛的隔离选项。
namespace 有三个系统调用可以使用:
clone()
— 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。unshare()
— 使某个进程脱离某个 namespacesetns(int fd, int nstype)
— 把某进程加入到某个 namespace
1 | int clone(int (*func)(void *), void *child_stack, int flags, void *func_arg, ...); |
如上面所示,表示使用clone从父进程建立一个子进程,并且使用mount隔离,子进程执行container_main这个函数。
mount隔离下,/bin文件夹可读可写
尽管挂载是独立的,但是在子命名空间中对文件的修改是会影响到父命名空间的(共享挂载的情况下)。那么我们可以通过子进程中的root权限使得/bin/cat(或者其他命令)获得suid,然后退出后,就可以cat root权限的flag了。
没有隔离pid的情况下,利用nsenter命令逃逸
nsenter
命令允许用户从一个命名空间切换到另一个命名空间,执行命令或启动一个新的shell
如果没有隔离pid,并且/proc被挂在到了子命名空间内,bin文件夹不可写的情况下,可以使用以下语句将用户切换至父命名空间的shell。pid1是众所周知的init进程,所以一定是在最初始的命名空间中的。
1 | nsenter --mount=/proc/1/ns/mnt /bin/bash |
但是既然proc也绑定到了子进程中的话,还可以利用/proc/fd/root访问到原本fs中的文件cat /proc/1/root/flag
。
在进程被jail之前有打开且可用的目录fd
和chroot同理,利用openat、linkat一类的系统调用可以实现读取jail外的文件
利用setns系统调用逃逸
1 | int setns(int fd, int nstype); |
fd
:要加入的 namespace 的文件描述符,一般为 /proc/[pid]/ns
下某个对应类型 namespace 的软链接;
nstype
:调用进程想要加入的 namesapce 的类型,其类型对应上文的表格中的 7 种 Namespace 类型:
0
:允许加入任何类型的 namespace;CLONE_NEWCGROUP
:fd
必须指向一个 cgroup 的 namespace;CLONE_NEWIPC
:fd
必须指向一个 IPC 的 namespace;CLONE_NEWNET
:fd
必须指向一个 network 的 namespace;CLONE_NEWNS
:fd
必须指向一个 mount 的 namespace;CLONE_NEWPID
:fd
必须指向一个 pid 的 namespace;CLONE_NEWUSER
:fd
必须指向一个 user 的 namespace;CLONE_NEWUTS
:fd
必须指向一个 UTS 的 namespace;
如果能访问到/proc/[pid]/ns文件夹,那么可以先open想要加入的namespace,获得目录的fd,然后setns(3,0)
,即可加入到相应的命名空间中实现逃逸。但此时权限依然是子进程的用户权限,如果是root,就可以以此来实现提权。