来源:嘶吼专业版

ID:Pro4hou

DevOps概念的流行跟近些年微服务架构的兴起有很大关系,DevOps是Dev(Development)和Ops(Operations)的结合,Dev负责开发,Ops负责部署上线,Docker出现之前,公司需要搭建一个数据库环境,有了Docker之后,只需在一些开源的基础镜像上构建出公司自己的镜像即可。

因此目前大多数DevOps设置都在CI管道中的某处设置了Docker,这就意味着你所看到的任何构建环境都将使用Docker等容器解决方案。由于这些构建环境需要接受不可信的用户提供的代码并进行执行,因此探讨如何将这些代码安全地装入容器就显得非常有意义。

在这篇文章中,我将探讨在构建环境中非常小的错误配置是如何产生严重的安全风险的。

需要注意的是,我并未在本文描述Heroku,Docker,AWS CodeBuild或容器中的任何固有漏洞,而是讨论了在查看基于Docker容器的多租户构建环境时发现的错误配置漏洞。在常规运行下,虽然Docker容器技术提供了非常稳定的安全默认设置,但是在特殊情况时,有时候小的错误配置就会导致严重安全风险。

特殊的构建环境

可能的特殊构建环境可以具有以下架构:

1.具有完全托管的生成服务,可编译源代码、运行测试以及生成可供部署的软件包——AWS CodeBuild;

2.Docker构建服务中的Docker容器;

Docker容器可以通过Dind(Docker-in-Docker,是让你可以在Docker容器里面运行Docker的一种方式)创建,因此,从理论上来说,你最终得到两个攻击者需要逃脱的容器。使用CodeBuild可进一步最小化攻击面,因为你拥有AWS提供的一次性容器,而且租户不会与对方的构建过程互动。

 

攻击者是如何控制构建过程的?

在大多数构建或CI管道中要做的第一件事就是创建一个包含你想要构建和部署的代码的Git仓库。然后这些代码将被打包并转移到构建环境,最后应用到docker构建过程。

通过查看构建服务,你通常可以通过两种方式配置容器,即通过Dockerfile或config.yml,这两种方法都与源代码有关。

其中有一个CI配置文件,我称之为config-ci.yml,如下图所示。

在其它的构建过程开始之前,该文件将在构建过程中被转换为Dockerfile。如果你有明确指定要使用的Dockerfile环境,可以将config-ci.yml更改为以下内容。

Dockerfile_Web和Dockerfile_Worker是源代码存储库中Dockerfiles的相对路径和名称,既然现在我已经提供了完整的构建信息,就可以开始构建了。构建通常是通过原始仓库上的代码上传来启动的。启动时,你会看到如下所示的输出内容。

正如你所看到的,输出的内容有docker build -f Dockerfile。这些内容即对调试过程有用,又对于发现可能出现的攻击有用。

 对预构建过程进行攻击

在进入docker构建之前,我首先想到的是尝试并中断构建过程,或者,我可以尝试将来自CodeBuild环境的文件链接到我的Docker构建的上下文中。

由于我已经控制了config-ci.yml文件的内容,更具体地说,我控制的是“要使用的Dockerfile的相对路径”,所以我可以尝试用一种老式攻击方法——目录遍历攻击。

第一个尝试就是试着改变构建的目录:

一旦构建过程开始,我就会立即得到以下错误信息。

有趣的是,该错误是我造成的,并导致了路径泄漏,如果我尝试“读取”文件会发生什么?

可以看出,我解析了Docker守护进程的错误。不幸的是,这只针对我系统上的第一行文件。尽管如此,这也是一个有趣的开始。

其实,我这么做的另一个想法是想尝试使用符号链接将文件包含到我的构建中。不过,Docker阻止了我这么做,因为它不会将构建目录之外的文件包含到构建上下文中。

攻击构建过程,以发现漏洞

让我们先回到实际的构建过程,看看可以对什么进行攻击?由于构建过程发生在dind Docker容器中,该容器在一次性CodeBuild实例中运行。为了进一步寻找攻击,docker构建过程会在一次性Docker容器中运行所有命令。Docker的容器是把应用程序和环境打包在一起的,所以是一次构建,多处发布。举个例子,以前你开发完程序后,测试人员和运维人员都需要去部署,通过docker只需要一个run命令即可。因此docker最大的好处就是标准化了应用交互,同时支持多个不同平台和多个不同的云服务商,只要机器能装docker,就能无差别的运行程序。

所以Docker构建的每一步实际上都是一个新的Docker容器,这从构建过程的输出中就可以看出。

在上述情况下,在新的Docker容器e7e10023b1fc中执行上面输出代码段中的Step 2/9。因此,即使用户决定在Dockerfile中插入一些恶意代码,它们也应该在一次性隔离容器中运行,而不会造成任何损害,如下所示。

在发布Docker命令时,这些命令实际上被传递给负责创建/运行/管理Docker镜像的dockerd守护进程。为了继续实现dind,dind需要运行自己的Docker守护进程。然而,由于实现dind的方式是使用主机系统的docker实例(dockerd instance),以允许主机和后台共享Docker镜像,并从Docker的所有缓存中受益。

如果Dind使用下面的包装脚本启动会发生什么结果:

/usr/local/bin/dind是一个使Docker在容器中运行的包装脚本,该包装脚本确保来自主机的Docker套接字在容器内部可用,因此,此特定配置会引入安全漏洞。

通常Docker构建过程将无法与Docker守护进程交互,但是,在这种情况下,却可以实现交互。敏锐的观察者可能会注意到,dockerd守护进程的TCP端口也是通过--host=tcp://0.0.0.0:2375进行映射的。通过这种错误配置设置的Docker守护进程会监控容器上的所有接口。因此,这就成了Docker网络功能中的一个漏洞。除非另有说明,否则所有容器都将会被放入相同的默认Docker网络中。这意味着每个容器都能够与其他容器进行通信,而不会受到阻碍。

现在,我的临时构建容器(执行用户代码的那个容器)已经能够向托管它的dind容器发出网络请求。由于dind容器只是重复使用了主机系统的Docker守护进程,所以我实际上是直接向主机系统AWS CodeBuild发出命令。

 实施Dockerfiles攻击

为了测试Dockerfiles攻击,我可以将下面的Dockerfile提供给构建系统,这样我就能够交互访问正在构建的容器。需要说明的是,我这么做只是为了加速寻找漏洞的过程,而不是为了减少等待构建过程的时间。

可以看出,反向shell可以通过很多不同的方式完成。

这个Dockerfile会安装一些依赖项,即docker和netcat。然后它们会将我的源代码目录中的文件复制到构建容器中。这将在后来的步骤中用到,除此之外,这么做还可以更容易地将我的完整漏洞快速传输到系统。由于mknod指令会创建一个文件系统节点,以允许通过文件重定向stdin和stdout。使用netcat可以打开一个反向shell,除此之外,我还需要在我使用公共IP地址控制的系统上为此反向shell设置监控器。

这样,当构建发生时,我将收到一个反向连接。

现在通过远程交互式访问,我就可以检查是否能对Docker守护进程进行访问。

我会使用-H 172.18.0.1来指定远程主机,由于我发现Docker使用的网络范围是172.18.0.0/16,因此使用了此地址。为了找到这个远程主机,我的交互式shell被用来充作ip addr和ip route,以获得分配给我的构建容器的网络。请注意,默认情况下,所有Docker容器都将被放入同一个网络,默认网关将是运行Docker守护进程的实例。

这样漏洞就会被成功发现,此时我可以从正在构建的容器中访问Docker,以便在下一步启动一个具有额外特权的新容器。

 进行栈处理

此时,我已有一个shell,不过它还是位于一次性的构建容器中,作用不是很大。另外,我也可以访问Docker守护进程。于是我就想,把这两者结合起来会怎么样?为此,我引入了第二个Dockerfile,它会在构建和运行时创建一个反向shell。以下就是我启动第二个监控器来捕获的新的shell。

这将作为Dockerfile2保存在源代码目录中,现在,当源代码文件被复制到构建容器中时,我可以直接访问它了。

当我重新运行构建过程时,我将在端口4445上获得我的第一个反向shell,这样我就可以留在构建容器中。现在我可以构建Dockerfile2,它被复制到COPY * /files/中的构建容器中。

现在我可以使用主机Docker守护进程并构建一个新的可用Docker映像,我只需要运行它即可。不过这里有个小的技巧,就是我需要通将根目录映射到新的Docker容器,这可以通过-v/:/vhost完成。

以下是我得到的第一个反向shell:

现在,一个新的反向shell就会连接到攻击系统上的4446端口。这样我就将处于一个新的容器中,并直接访问底层CodeBuild主机的文件系统和网络。这首先是因为--net=host将通过主机网络映射,而不是将容器保存在一个独立的隔离网络中。其次,因为Docker守护进程正在主机系统上运行,所以当使用-v /:/vhost的文件映射完成时,主机系统的文件系统将被映射。

这样在新的反向shell中,我现在就可以探索底层的主机文件系统了。通过检查以下两个之间的区别,我就可以证明我在与此文件系统交互时不在Docker中。

在/vhost中我还发现有一个新的目录,它可以清楚地表明我在CodeBuild实例文件系统中,而不是在任何Docker容器中。

这样在codebuild里,就会出现一个神奇的结果。这是AWS Codebuild用来控制构建环境的内容,快速浏览一下可以看到一些有趣的数据。

此时,我通常会尝试提取AWS凭证和数据透视表,为此,我需要使用AWS_CONTAINER_CREDENTIALS_RELATIVE_URI。

根据与该IAM相关的权限,现在应该有机会绕过AWS环境。

上述步骤可自动化实现,并且只需要一个反向shell即可完成,但是,请记住,你需要保持正常的构建环境。请注意,大多数构建环境会在30-60分钟后自动删除。

 缓解措施

 

在这种情况下,修复非常简单,永远不要将Docker守护进程绑定到所有接口上。从包装脚本中删除--host=tcp://0.0.0.0:2375 行也可以来修复这个漏洞。另外,不需要绑定到TCP端口,因为unix套接字已经通过了--host=unix:///var/run/docker.sock映射。

总结

虽然容器策略提供了一个很好的机制,来让你创建运行不受信任代码的安全环境,而不需要额外的虚拟化过程。然而,这些容器的安全性与它们的配置一样。默认情况下,它们非常安全,但只需一个小的配置错误就可以让整个安全环境崩塌。

Docker容器构建过程的安全性分析的更多相关文章

  1. 原lnmp环境服务器升级为mysql+nginx+php单个docker容器构建的lnmp环境

    时间:2018年2月 一.项目背景 我单位现web服务架构为lnmp环境,服务器软件.硬件升级部署难:同时开源软件日新月异,考虑到技术升级,领导决定服务器架构整体升级为容器架构,维护性.移植性强. 二 ...

  2. Docker笔记一:基于Docker容器构建并运行 nginx + php + mysql ( mariadb ) 服务环境

    首先为什么要自己编写Dockerfile来构建 nginx.php.mariadb这三个镜像呢?一是希望更深入了解Dockerfile的使用,也就能初步了解docker镜像是如何被构建的:二是希望将来 ...

  3. Docker容器化部署Python应用

    1. 简介 Docker是目前主流IT公司广泛接受和使用的,用于构建.管理和保护它们应用程序的工具. 容器,例如Docker允许开发人员在单个操作系统上隔离和运行多个应用程序,而不是为服务器上的每个应 ...

  4. vue 源码学习(一) 目录结构和构建过程简介

    Flow vue框架使用了Flow作为类型检查,来保证项目的可读性和维护性.vue.js的主目录下有Flow的配置.flowconfig文件,还有flow目录,指定了各种自定义类型. 在学习源码前可以 ...

  5. 关于docker-环境部署及拉取镜像创建容器的过程记录

    背景:因安全部门要求对特定几台应用主机的进行漏洞及脆弱性扫描,使用的工具需要基于docker环境,即他们提供镜像,让我们创建成容器,于是将整个环境安装及创建docker容器的过程记录于此 1.还是先得 ...

  6. Docker容器学习梳理 - Dockerfile构建镜像

    在Docker的运用中,从下载镜像,启动容器,在容器中输入命令来运行程序,这些命令都是手工一条条往里输入的,无法重复利用,而且效率很低.所以就需要一 种文件或脚本,我们把想执行的操作以命令的方式写入其 ...

  7. docker容器网络通信原理分析

    概述 自从docker容器出现以来,容器的网络通信就一直是大家关注的焦点,也是生产环境的迫切需求.而容器的网络通信又可以分为两大方面:单主机容器上的相互通信和跨主机的容器相互通信.而本文将分别针对这两 ...

  8. docker容器网络通信原理分析(转)

    概述 自从docker容器出现以来,容器的网络通信就一直是大家关注的焦点,也是生产环境的迫切需求.而容器的网络通信又可以分为两大方面:单主机容器上的相互通信和跨主机的容器相互通信.而本文将分别针对这两 ...

  9. 如何提高docker容器的安全性

    一. 概述 Docker 容器一直是开发人员工具箱的重要组成部分,使开发人员能够以标准化的方式构建.分发和部署他们的应用程序.毫无疑问,这种吸引力的增加伴随着容器化技术的相关安全问题.他们可以很容易地 ...

随机推荐

  1. java--Hibernate添加数据save

    添加按钮跳转到add表单页面 <a href="${pageContext.request.contextPath }/department_saveUI.action"&g ...

  2. 【JVM】java方法区

    java方法区[名词解析]        --->和java堆一样,方法区是一块所有线程共享的内存区域.        --->保存系统的类信息,比如,类的字段,方法,常量池等.      ...

  3. 【JVM】java棧

    java棧和函数调用的关系图 [名词解释]--->java棧是一块线程的私有空间--->java的棧是先进后出的数据结构.函数返回,则该函数的棧帧被弹出.--->一个函数对应一个棧帧 ...

  4. Bootstrap日期/日历插件Datepicker 时间加标记

    由于工作需要,项目中使用了Bootstrap日期/日历插件Datepicker,根据需求需要在其中添加日期标记,实现效果图如下: 特此记录此次解决方案: 1.首先分析了功能的DOM元素(如下图),可以 ...

  5. 浅谈Huffman树

    所谓Huffman树,就是叶子结点带权的\(K\)叉树,假设每个叶子的权值为\(v\),到根的距离为\(dep\),那么最小化\(\sum v_i*dep_i\)就是\(Huffman\)树的拿手好戏 ...

  6. POJ2080:Calendar(计算日期)

    Calendar Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 12842   Accepted: 4641 Descrip ...

  7. 机器学习:Jupyter Notebook中numpy的使用

    一.Jupyter Notebook的魔法命令 # 模块/方法 + ?或者help(模块/方法):查看模块/方法的解释文档: 1)%run # 机械学习中主要应用两个魔法命令:%run.%timeit ...

  8. TModJS:目录

    ylbtech-TModJS:目录 1.返回顶部 1. https://github.com/aui/tmodjs 2. https://www.npmjs.com/package/tmodjs 3. ...

  9. Java探索之旅(7)——对象的思考

    1.知识要点 ❶不可变类:一旦创建,其内容不能改变的类称之为不可变类.满足:⑴所有数据域私有,⑵没有修改器,⑶没有访问器方法,其返回一个指向可变数据域的引用.(这样通过引用就能修改私有数据域).比如, ...

  10. Assembly.GetManifestResourceNames()获取不到资源文件

    Assembly.GetManifestResourceNames()获取到的是嵌入的资源文件 右键资源文件属性 将生成操作改为嵌入的资源就OK咯