漏洞演示

使用 root 账号创建一个临时文件 a.txt:

1
2
3
[root@localhost ~]# echo "aaa" > /root/tmp/a.txt
[root@localhost ~]# cat /root/tmp/a.txt 
aaa

确认 work 账号没有 a.txt 的权限:

1
2
[work@localhost ~]$ cat /root/tmp/a.txt
cat: /root/tmp/a.txt: Permission denied

使用下面命令启动一个redis容器并挂载数据卷 /root/tmp :

1
2
3
# 注意这里是work账号
[work@localhost ~]$ docker run -d -v /root/tmp:/root/tmp  redis:6.0.10
f709b9d2ef3acd8edd525ba6a47246976715d720c047c868bea0631d51f4ab54docker run -d -v /root/tmp:/root/tmp  redis:6.0.10

进入刚才创建的 redis 容器并修改 a.txt 文件:

1
2
3
4
5
6
# 注意这里也是work账号
[work@localhost ~]$ docker exec -it f70 bash
root@f709b9d2ef3a:/data# echo "bbb" >> /root/tmp/a.txt 
root@f709b9d2ef3a:/data# cat /root/tmp/a.txt 
aaa
bbb

回到宿主机上,使用 root 账号确认 /root/tmp/a.txt 文件内容:

1
2
3
[root@localhost ~]# cat /root/tmp/a.txt 
aaa
bbb

可以看到一个普通的 work 账号,通过docker容器提权操作了一个没有权限的文档。

原理

使用work账号进入刚才的redis容器然后保持:

1
2
3
[work@localhost ~]$ docker exec -it f70 bash
root@f709b9d2ef3a:/data# id
uid=0(root) gid=0(root) groups=0(root)

在宿主机使用root账号查看一下容器的bash进程

1
2
3
4
[root@localhost ~]# docker top f70
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
systemd+            13516               13497               0                   04:18               ?                   00:00:03            redis-server *:6379
root                13679               13497               0                   04:32               pts/0               00:00:00            bash

在宿主机确认13679进程的user是root

1
2
[root@localhost ~]# ps -ef | grep 13679
root     13679 13497  0 04:32 pts/0    00:00:00 bash

可以看到work账号进入容器bash程序的时候,切换成root账号,所以权限得到提升。如果绑定了root的目录,则就可以对root目录进行操作。

这是因为我们的docker服务是使用root账号启动的:

1
2
[root@localhost ~]# ps -ef | grep dockerd
root     12694     1  0 03:59 ?        00:00:01 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

容器用户

在宿主机查看redis进程, 可以知道宿主机上redis进程的username是 polkitd :

1
2
$ ps -ef | grep redis
polkitd  12876 12857  0 14:17 ?        00:00:00 redis-server *:6379

也可以使用 docker top 查看:

1
2
3
$ docker top dca
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
polkitd             12876               12857               1                   14:17               ?                   00:00:00            redis-server *:6379

查看宿主机上polkitd账号的uid:

1
2
$ id polkitd
uid=999(polkitd) gid=998(polkitd) 组=998(polkitd)

进入容器查看容器内用户:

1
2
3
root@5be8ce78244c:/data# gosu redis bash  # 使用gosu切换到redis账号
redis@5be8ce78244c:/data$ id
uid=999(redis) gid=999(redis) groups=999(redis)

这里可以发现容器内redis和宿主机polkitd的uid是一致的。

redis:6.0.10镜像没有安装 ps, 可以进入容器后执行 apt-get update && apt-get install procps 安装。然后查看redis进程的uid:

1
2
3
4
5
root@6c54c348686d:/data# ps -ef
UID         PID   PPID  C STIME TTY          TIME CMD
redis         1      0  0 08:08 ?        00:00:00 redis-server *:6379
root         26      0  0 08:09 pts/0    00:00:00 bash
root        350     26  0 08:15 pts/0    00:00:00 ps -ef

确认容器内是使用redis账号启动的服务,服务进程号是 1。还可以在宿主机上确认redis账号不存在:

1
2
$ id redis
id: redis: no such user

我们可以得出结论1: 容器用户uid和宿主机用户的uid相同,名称可以不同

指定容器用户

容器可以指定用户。编写 Dockerfile

1
2
FROM redis:6.0.10
RUN groupadd -r test1 && useradd -r -g test1 test1  # 创建一下test1用户

编译镜像

1
docker build -t redis:redis_test .

运行自定义的redis镜像, 运行时需要指定用户

1
docker run -d --user test1 redis:redis_test

docker run -u 参数帮助: –user string Username or UID (format: <name|uid>[:<group|gid>])

在宿主机上查看redis进程信息, 发现是systemd-bus-proxy创建的进程:

1
2
$ ps -axo user:20,pid,pcpu,pmem,vsz,rss,tty,stat,start,time,comm | grep redis
systemd-bus-proxy    15429  0.1  0.0  52796 11916 ?        Ssl  16:24:01 00:00:01 redis-server

ps -ef 中如果username超过8个字符,会显示成 systemd+, 所以上面命令要使用 use:20 格式化一下长度

看看systemd-bus-proxy的uid

1
2
$ id systemd-bus-proxy
uid=998(systemd-bus-proxy) gid=996(systemd-bus-proxy) groups=996(systemd-bus-proxy)

进入容器查看:

1
2
3
$ docker exec -it 680 bash
test1@680ccf686e5f:/data$ id  # 注意当前用户变成了test1,uid=998
uid=998(test1) gid=998(test1) groups=998(test1)

使用test1运行的容器没有权限安装包,使用下面方式确认进程的uid=998

1
2
3
4
5
6
7
8
$ cat /proc/1/status | grep id
Tgid:	1
Ngid:	0
Pid:	1
PPid:	0
TracerPid:	0
Uid:	998	998	998	998
Gid:	998	998	998	998

docker的机制是容器的id=1的进程是主进程。

我们可以得出结论2: 容器用户可以在镜像中创建,在container运行时使用--user参数指定

gosu切换用户

容器可以指定用户,自然涉及到切换用户的问题,宿主机上使用 su , 容器内建议 gosu,下面是Docker — 从入门到实践中的内容:

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立 好的用户来运行某个服务进程,不要使用 su 或者 sudo ,这些都需要比较麻烦 的配置,而且在 TTY 缺失的环境下经常出错。

redis:6.0.10的 Dockerfile 源码中可以看到创建了redis=999的账号:

1
2
# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added
RUN groupadd -r -g 999 redis && useradd -r -g redis -u 999 redis

重点是 docker-entrypoint.sh 部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
root@6c54c348686d:/data# whereis docker-entrypoint.sh
docker-entrypoint: /usr/local/bin/docker-entrypoint.sh
root@6c54c348686d:/data# cat /usr/local/bin/docker-entrypoint.sh
#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
	set -- redis-server "$@"
fi

# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
	find . \! -user redis -exec chown redis '{}' +
	exec gosu redis "$0" "$@"  # 使用redis账号启动redis-server
fi

exec "$@"

所以默认的redis:6.0.10显示uid=999。

下面是gosu官方的日志:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ docker run -it --rm ubuntu:trusty su -c 'exec ps aux'
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  46636  2688 ?        Ss+  02:22   0:00 su -c exec ps a
root         6  0.0  0.0  15576  2220 ?        Rs   02:22   0:00 ps aux
$ docker run -it --rm ubuntu:trusty sudo ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  3.0  0.0  46020  3144 ?        Ss+  02:22   0:00 sudo ps aux
root         7  0.0  0.0  15576  2172 ?        R+   02:22   0:00 ps aux
$ docker run -it --rm -v $PWD/gosu-amd64:/usr/local/bin/gosu:ro ubuntu:trusty gosu root ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   7140   768 ?        Rs+  02:22   0:00 ps aux

sudo会作为被授权的命令的父进程一直存在,直到该命令退出,gosu则没有这个问题。

版本

本次使用使用的相关版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
NAME="CentOS Linux"
VERSION="7 (Core)"
...

Client: Docker Engine - Community
 Version:           20.10.7
 API version:       1.41
...

Server: Docker Engine - Community
 Engine:
  Version:          20.10.7
  API version:      1.41 (minimum version 1.12)
  ...

清理

可以使用下面命令清理实验环境

1
2
3
4
5
docker ps -aq | xargs docker  stop  # 关闭容器
docker container prune  # 清理容器

docker rmi redis:redis_test  # 删除镜像
docker image prune  # 清理镜像

参考链接