https://pwn.college/system-security/sandboxing/ 并不一定适用于所有发行版,有些发行版可能已经修复这些漏洞了(实测ubuntu24已经修复chroot工作目录的漏洞了,但是fd漏洞可以用;ubuntu20都可以用)

0x00 参考文章(前置知识)

超细节的 Namespace 机制讲解

超详解的Linux内核进程描述符task_struct结构体

Linux nsenter命令全面解析

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
2
3
4
5
6
/* file system info */
int link_count, total_link_count;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;

其中fs_struct结构体长这样:

1
2
3
4
5
6
7
struct fs_struct {
atomic_t count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};

root:根目录的目录项
pwd:当前工作目录的目录项
altroot:模拟根目录的目录项

files_struct长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct files_struct {
atomic_t count;
struct fdtable *fdt;
struct fdtable fdtab;

spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;
struct embedded_fd_set close_on_exec_init;
struct embedded_fd_set open_fds_init;
struct file * fd_array[NR_OPEN_DEFAULT];
};
struct fdtable {
unsigned int max_fds;
int max_fdset;
struct file ** fd; /* current fd array */
fd_set *close_on_exec;
fd_set *open_fds;
struct rcu_head rcu;
struct files_struct *free_files;
struct fdtable *next;
};
#define NR_OPEN_DEFAULT BITS_PER_LONG
#define BITS_PER_LONG 32 /* asm-i386 */

而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
2
int chdir(const char *path);
int fchdir(int fd);

同理,如果设置了工作目录在jail内,那么chdir也没办法改变工作目录,但是fd则不受限制。

如果只是想打开文件,那么也可以考虑使用openat,如果pathname是一个相对路径,那么相对的就是fd对应的目录:

1
2
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
openat(3,"flag",0,0) // 记得一定不要加上/

如果openat被禁用了,也可以考虑使用linkat,创建指向现有文件的新链接,支持相对路径和绝对路径的处理(假如已打开的目录是/,fd为3):

1
2
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flags);
linkat(3,"flag",4,"/flag",0)

这样就可以将原本根目录下的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() — 使某个进程脱离某个 namespace
  • setns(int fd, int nstype) — 把某进程加入到某个 namespace
1
2
int clone(int (*func)(void *), void *child_stack, int flags, void *func_arg, ...);
int container_id = clone(container_main, container_stack + STACK_SIZE, CLONE_NEWNS | SIGCHLD, NULL);

如上面所示,表示使用clone从父进程建立一个子进程,并且使用mount隔离,子进程执行container_main这个函数。

mount隔离下,/bin文件夹可读可写

尽管挂载是独立的,但是在子命名空间中对文件的修改是会影响到父命名空间的(共享挂载的情况下)。那么我们可以通过子进程中的root权限使得/bin/cat(或者其他命令)获得suid,然后退出后,就可以cat root权限的flag了。

没有隔离pid的情况下,利用nsenter命令逃逸

nsenter命令允许用户从一个命名空间切换到另一个命名空间,执行命令或启动一个新的shell

如果没有隔离pid,并且/proc被挂在到了子命名空间内,bin文件夹不可写的情况下,可以使用以下语句将用户切换至父命名空间的shell。pid1是众所周知的init进程,所以一定是在最初始的命名空间中的。

1
2
nsenter --mount=/proc/1/ns/mnt /bin/bash
cat /flag

但是既然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_NEWCGROUPfd 必须指向一个 cgroup 的 namespace;
  • CLONE_NEWIPCfd 必须指向一个 IPC 的 namespace;
  • CLONE_NEWNETfd 必须指向一个 network 的 namespace;
  • CLONE_NEWNSfd 必须指向一个 mount 的 namespace;
  • CLONE_NEWPIDfd 必须指向一个 pid 的 namespace;
  • CLONE_NEWUSERfd 必须指向一个 user 的 namespace;
  • CLONE_NEWUTSfd 必须指向一个 UTS 的 namespace;

如果能访问到/proc/[pid]/ns文件夹,那么可以先open想要加入的namespace,获得目录的fd,然后setns(3,0),即可加入到相应的命名空间中实现逃逸。但此时权限依然是子进程的用户权限,如果是root,就可以以此来实现提权。

⬆︎TOP