ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

K8s容器逃逸后攻击者如何隐藏持久化?攻防视角拆解与防守策略

K8s容器逃逸后攻击者如何隐藏持久化?攻防视角拆解与防守策略 我讲一个场景各位应该不陌生一次授权攻防演练里业务方把整套微服务跑在K8s上安全团队花了大价钱买了WAF、HIDS、容器安全平台告警规则配了几百条。结果红队从一个不怎么起眼的Web漏洞打进Pod容器逃逸之后在宿主机上蹲了整整三天期间还通过K8s控制面把自己“复活”了两次最后直到演练结束防守方都没发现主机已经被长期驻留。这是我复盘了多个攻防项目之后最深的体会在K8s环境里容器逃逸往往不是终点而是起点。攻击者真正想要的是在主机上站稳脚跟并且让所有告警都“看不见他”。这篇内容不教大家去做坏事而是从红队攻击路径的视角把逃逸之后“怎么藏、怎么活、怎么不被发现”的完整逻辑拆开来讲让做防守的同学清楚攻击者每一步在想什么、踩在哪里、又在躲什么。只有知道敌人在暗处如何行动防守方的告警规则才不是摆设。适合K8s运维、安全运营、蓝队工程师在授权攻防项目中作为参考。1. 为什么K8s会成为持久化攻击的“天然跳板”1.1 从一次授权攻防演练说起的真实案例有一次我们做红队模拟初始入口是一个业务系统的RCE漏洞代码执行发生在Pod里。当时所有人的注意力都在“如何从Pod逃逸到宿主机”但真正让我意外的不是逃逸本身而是逃逸之后防守方的反应。逃逸成功后我在宿主机上做了常规的权限维持操作然后故意留下了一些比较明显的痕迹比如修改了/etc/crontab、新建了一个伪装成系统服务的systemd unit。结果连续两天防守方没有任何动静。后来复盘时发现防守方确实收到了告警但告警到了SOC平台之后被“降噪”规则直接吞掉了。因为当时容器逃逸产生的行为特征和某些正常运维操作长得很像——毕竟很多运维同学也喜欢手动改crontab、手动创建systemd服务。这类告警每天产生几百条安全人员已经看得麻木了。这个案例说明一个很关键的问题K8s环境下的持久化难度不在于“能不能实现”而在于“能不能长得像正常运维”。攻击者真正花心思的地方是让自己的一切行为淹没在日常操作的噪声里。1.2 容器环境的隐蔽优势为什么攻击者偏爱这里传统物理机或虚拟机上的持久化攻击者通常要考虑杀软、EDR、主机加固策略。但在K8s环境里这些防线被大大削弱了。原因有三层第一容器本身就是“一次性”的。很多组织默认Pod会被频繁重建所以安全团队对容器内的异常进程、异常文件没那么敏感总觉得“反正是无状态的重启就干净了”。这个认知恰恰给了攻击者空间——他可以在容器里放一些临时脚本利用工作负载的自动伸缩和调度让恶意行为分散在多个Pod里执行每次都是“新容器”每次都不像长期驻留。第二K8s控制面的权限模型非常复杂。ServiceAccount、RBAC、NetworkPolicy、PodSecurityPolicy这些机制一旦配置疏漏攻击者拿到的权限往往超出预期。很多集群管理员对Pod内的应用权限没有收敛导致攻击者从容器逃逸后甚至能直接访问K8s API Server创建高权限Workload。第三多云和混合云架构让主机的归属变得模糊。很多团队对“这个节点属于哪个集群、上面跑了哪些业务”都说不清楚就更别提对节点上的异常行为做精确判断了。简单说K8s把传统的“一台主机一个边界”拆成了“一堆容器N个边界”边界变多盲区变多攻击者找到的可乘之机自然也就多了。1.3 告警为什么总在逃逸瞬间“失明”我在很多项目里发现一个共性现象容器逃逸的过程本身其实是会触发告警的比如内核态异常、敏感挂载点访问、特权容器启动这些都有现成的检测规则。但告警发出来之后往往并没有形成有效响应。问题出在两个环节。第一个环节是告警“只报不查”。告警事件只告诉运营人员“有异常”但没有上下文——它不会告诉你这个异常进程是从哪个Pod里出来的不会告诉你它和之前的什么操作有关联也不会告诉你这个进程接下来访问了哪些资源。运营人员看到一条孤零零的告警根本无法判断严重程度。第二个环节是“告警洪峰”淹没“关键告警”。在攻防演练期间防守方往往会在同一时间收到大量误报和低优先级告警比如容器镜像扫描的漏洞list、Pod频繁重启的warning事件。这些噪声占据了运营人员的注意力真正关键的逃逸告警反而排在后面等人看到时攻击者早就完成了持久化操作。所以从攻击者的视角来看逃逸之后的黄金时间窗口非常宝贵——告警可能已经发出但真正的响应还没开始。他要做的就是在这个窗口内快速收集信息、落地持久化、清理痕迹然后“隐身”。2. 逃逸落地之后攻击者第一时间的侦察动作2.1 侦察优先级内核、权限、挂载点与网络我见过不少新手在拿到容器权限后第一件事就是急着传工具、弹Shell结果很快被防守方抓住。实际上一个有经验的红队操作者逃逸成功后会先花几分钟做侦察搞清楚自己落到了一台什么样的主机上。按优先级排列攻击者至少要看这四类信息侦察对象典型命令/方法攻击者关心的核心问题内核版本与内核模块uname -a, cat /proc/version是否存在已知的内核逃逸漏洞宿主机内核是否较老进程与容器身份cat /proc/1/cgroup, hostname, cat /etc/hostname自己到底在容器里还是宿主机上是否为特权模式挂载点与Docker Socketmount, ls -la /var/run/docker.sock, cat /proc/mounts是否有敏感目录挂载是否能直接控制宿主机Docker守护进程网络与云元数据ip addr, cat /proc/net/route, curl云厂商元数据地址主机的网络位置、是否在云上、能否访问内部元数据服务这里我多说一句攻击者侦察时用的基本都是系统自带命令不会触发“恶意工具落地”的检测规则。很多防守方只盯可疑文件、可疑进程名却忽略了“正常命令的异常组合”才是更危险的信号。2.2 如何判断自己有没有资格触碰K8s API主机站稳后攻击者通常会尝试和K8s API Server建立连接。这一步能不能成取决于容器和节点有没有可用凭据。最值钱的是这三种第一种是容器内置的ServiceAccount。每个Pod在创建时都会自动挂载一个服务账户Token路径在/var/run/secrets/kubernetes.io/serviceaccount/token。如果这个ServiceAccount被赋予了较高的RBAC权限攻击者不需要任何额外操作就能直接用这个Token调K8s API。第二种是节点上的kubelet证书和Admin Kubeconfig。如果攻击者拿到了节点的root权限而节点的/etc/kubernetes目录权限配置不当就能读到kubelet的客户端证书甚至直接找到admin.conf。这类凭据的权限通常比ServiceAccount高得多。第三种是云厂商托管的集群凭据。不少团队会把K8s集群的Kubeconfig文件、云访问密钥保存在节点的配置目录、CI脚本或环境变量里。攻击者只要在主机的Shell配置或容器环境变量里翻一翻经常能有意想不到的收获。判断自己“有没有资格”碰K8s API攻击者会先跑一下kubectl auth can-i --list之类的检查确认自己能对哪些资源做什么操作。这一步非常安静不产生恶意流量但对后续的持久化路径选择至关重要。2.3 从Pod到集群的权限扩展路径拿到K8s API访问权限之后攻击者就从一个“单点容器失陷”升级到了“控制面失陷”的层面。这时候他的权限扩展路径大体是这样的如果ServiceAccount的RBAC允许创建Deployment或DaemonSet攻击者就可以直接部署一个新的恶意工作负载指定它调度到某个目标节点上并且挂载宿主机的根目录。这种操作在K8s里叫“恶意工作负载”是攻击者最喜欢的手法因为一切操作都通过API完成传统的EDR和主机Agent根本看不到这个“容器”在做什么。如果RBAC权限不够攻击者可以尝试创建高权限Pod利用HostPID、HostNetwork、特权模式等配置来扩大影响面。纵使无法直接控制整个集群也至少能在节点层面获得一个高权限容器持续探测其他通道。更隐蔽的做法是修改现有工作负载的定义。攻击者不会新建一个看起来就很可疑的Deployment而是在已有的名字、已有的镜像基础之上注入一个sidecar容器或者修改启动命令。这样云原生安全平台虽然能看到“工作负载有变化”但很难判断变化是否是版本发布导致的。3. 三类“低噪音”持久化手法攻击者的真实选择3.1 文件层无文件落地与隐藏路径复用传统持久化喜欢写文件比如往/usr/bin下面丢一个后门文件。但在K8s和容器场景里写文件的风险很高因为文件扫描、镜像一致性校验都能发现问题。所以攻击者更偏爱“无文件落地”的方式。一种常见做法是使用memfd_create直接在内存里创建匿名文件把恶意代码映射到内存中执行磁盘上不留痕迹。很多容器镜像本身是只读文件系统攻击者也会刻意避免写磁盘把脚本和二进制压缩包放在内存临时文件系统里。另一种做法是“隐藏路径复用”。在宿主机上/tmp、/var/tmp、/dev/shm这类目录通常不会被常规文件扫描重点关注。攻击者会把这些目录改成看似正常的名字比如/tmp/.X11-unix/、/var/tmp/.update-manager/然后在里面存放持久化脚本。有些攻击者甚至会把恶意文件隐藏在原系统文件的扩展属性或日志文件的空洞中但这些手法对磁盘空间和清理时机要求太高不是首选。我特别提醒防守同学一句别只盯着进程和网络容器层和宿主机层那些“几乎没人看的目录”才是攻击者藏东西的首选。3.2 进程层名字、资源与调度器的伪装进程是HIDS检测的核心对象攻击者必须在进程层面做好伪装。最粗暴的方式是起一个和系统进程完全同名的进程比如把后门进程命名为systemd、kubelet、containerd。这一招在传统主机上挺有用但在容器环境里很容易露出马脚因为HIDS一旦发现同名进程数量异常或进程路径不对就会告警。更高级的做法是“资源限制伪装”。攻击者会给自己植入的后门进程设置合理的环境变量和资源限制让它的CPU、内存占用看起来像某个正常业务服务的指标。比如原本业务服务占用内存2GB后门进程就把自己的内存占用控制在200MB以内避免触发资源使用的异常告警。还有一类手法是与正常进程“同生共死”。攻击者会把自己的恶意代码注入到业务进程运行时环境中或者利用LD_PRELOAD加载恶意共享库让恶意代码“寄生”在正常进程里。这样即使SOC拿着进程列表一一排查看到的还是那个合法的Java进程或Nginx进程很难发现内部已经被劫持。3.3 控制面层借K8s控制器实现自动复活这部分是云原生环境下最有特色的持久化手法也是很多传统安全人员容易忽略的地方。攻击者如果拿到了K8s API的写权限他不会只依赖主机上的cron或systemd因为一旦Pod被重新调度、节点被重置宿主机上的持久化就断了。他会直接把“驻留点”部署到K8s控制面上让集群自己帮他维持存在感。思路是这样的创建一个Deployment或DaemonSet让它始终维持一定数量的副本数。即使某个Pod被删除控制器会自动新建一个Pod来补位。这个“自动复活”机制是K8s的正常行为从事件流上看只是普通的Pod重建如果Pod名称带上了业务的随机后缀安全团队很难把它和攻击联系起来。更隐蔽的变体是利用CronJob。攻击者定时创建新的Job来执行一轮侦察或维持通信的任务任务执行完Job就结束看起来像是一次性任务不会在集群里留下常驻进程。配上比较长的间隔比如每隔23小时执行一次这类规律性不强的小任务很难被时序检测发现。4. 在主机上站稳脚跟隐匿驻留的时间艺术家4.1 systemd与cron传统持久化在云原生下的变体云原生环境底层也是Linux主机所以传统的主机持久化手段依然有效只是需要调整姿势。systemd服务是攻击者比较喜欢的方式。攻击者会在/etc/systemd/system/下面新建一个服务文件服务名通常模仿现有服务比如systemd-networkd-update.service。但单纯新建服务还是太明显比较好的伪装是修改已有服务的ExecStart把恶意命令追加进去让它在系统服务重启时被一并启动。cron是另一个经典手段但在容器场景里要注意很多容器镜像默认不安装cron宿主机上则一般都有。攻击者会把恶意脚本写到/var/spool/cron/root或其他用户的crontab里配上随机化的延时让任务看起来像是正常的系统维护。因为没有在init脚本和常见启动路径中留下痕迹这类定时任务往往在主机上存活很久。还有一个细节值得防守方注意攻击者会花时间把文件的时间戳改成和旁边文件一致。如果你在排查时发现某个cron关联的脚本时间戳和/etc目录里其他文件完全一样反而可能是刻意对齐的结果。4.2 通信形态的隐匿复用云平台与K8s的合法通道持久化只解决“活下来”的问题还要解决“怎么通信”。如果恶意程序在主机上拼命外连陌生IP再隐蔽的持久化也会立刻暴露。所以攻击者会想方设法把自己的流量混进合法通道。在K8s环境里最大的合法通道就是K8s API Server本身。如果攻击者能拿到一个有权限的ServiceAccount Token他完全不需要反向Shell直接通过HTTP请求以一定时间间隔向API Server查询资源状态即可。这种流量的目的地是内网已知IP而且是标准的HTTPS请求很多安全设备都会直接放行。云平台上还有一类合法通道是对象存储和消息队列。攻击者可以把恶意脚本需要的数据上传到云上的对象存储然后用畸形的请求头或特定路径来传递控制指令。这类流量在云环境里太常见了SOC平台不会对“访问对象存储”这个动作单独告警。我见过比较极端的做法是攻击者把恶意域名解析到CDN节点上再通过CDN回源到自己的服务器让外联流量看起来只是用户访问了某个云厂商的静态资源域名。这类通信从网络层看很难和正常业务流量区分开。4.3 时间与频率控制让告警降噪帮攻击者“降噪”很多告警规则是基于频率和周期设计的——某个行为如果在一段时间内频繁发生就会被标记为异常。攻击者很清楚这一点所以会刻意控制自己的行为频率。比如需要每隔几分钟执行一次的信息收集攻击者会改成每小时执行一次甚至每天只在凌晨执行几次。这样在SOC的时间线视图里这些行为稀疏得像无关紧要的杂音很难聚合成一条完整的攻击链。再比如文件访问类的行为攻击者会把本来可以在几秒内完成的批量文件遍历拆成几百个小步骤分散在一整天里完成。这样IDS和审计系统看到的单次事件都不超过阈值不会触发聚合告警。做防守的同学要理解一件事攻击者永远比你更有耐心。如果告警规则只盯着“高频”“集中”“大流量”那么一个足够慢的攻击就能毫发无伤地从你眼皮底下经过。这也是为什么我后面会强调必须把“时间维度”纳入行为建模而不仅仅是依赖单点特征。5. 从攻击视角复盘点告警系统最常见的五处破绽5.1 特征检测的失效未知威胁不会遵循签名很多团队建的告警规则还是老一套匹配恶意域名、匹配已知漏洞利用特征、匹配文件hash。这套逻辑在传统Web攻击里还有效但在K8s环境的攻击链里效果非常有限。原因很简单攻击者使用的工具很多是系统自带的比如curl、wget、kubectl、python甚至直接使用K8s API的SDK。没有恶意文件名没有恶意hash没有明显的漏洞利用特征所有操作看起来都是“正常运维能做的事”。特征规则库对这种行为基本无能为力。我并不是说特征检测完全没用而是它的定位应该是“兜底”不应该是“主力”。如果防守方主要依赖特征匹配那遇到一次真正有经验的攻击可能全程都不会收到有价值的告警。5.2 时序关联的空窗逃逸与驻留之间的检测断层更麻烦的问题在于告警之间缺乏关联。即使防守方同时看到了“容器逃逸”告警和“宿主机出现新systemd服务”告警如果这两条告警没有在时间上、主机上、账号上被关联起来运营人员依然无法意识到这是同一次攻击的两步。攻击者最擅长的就是利用这个空窗——他逃逸后不会马上做持久化而是先等一等让逃逸告警的注意力过去或者先做点低风险操作给安全团队制造“这可能是误报”的错觉。等运营人员把告警关闭再在几个小时后去落地驻留脚本。所以时序关联能力比单点告警重要得多。发现逃逸事件后至少应该在24小时内持续关注该节点的异常行为而不是把逃逸当作一个孤立事件来处理。5.3 日志采集的盲区被刻意清空的取证线索攻击者站稳脚跟之后通常会做一件事清理历史命令记录和执行痕迹。他会把~/.bash_history删掉或者干脆设置一个不记录历史的Shell环境。更彻底的是查看并篡改/var/log下的审计日志和系统日志把和自己相关的行删除。这给防守方提出了一个硬性要求日志必须集中外发。如果日志只存在本地攻击者拿到root权限后可以随意抹除事后连溯源都做不了。很多K8s集群的容器日志默认只存在节点本地Pod一旦被删除日志文件随之被回收。安全团队如果依赖容器日志做检测等于主动把证据交给攻击者销毁。真正有效的做法是至少把K8s审计日志、kubelet日志、关键节点的主机日志实时转发到独立的日志中心并且对日志写入做权限隔离让容器里的进程没有权限删除远端日志。5.4 告警降噪的副作用高基数误报掩盖真实攻击我之前提到过的组织里的“告警降噪”问题这里再展开讲一下。不少团队为了让告警数量看起来“好看”配置了大量规则把某些事件直接抑制掉或者把多个事件聚合成一条告警。常见的降噪规则包括“同一账号重复触发N次后静默”“同一IP触发规则后24小时内不再告警”“仅严重级别为高危才推送到群”等等。这些规则在削减噪声的同时也给攻击者提供了“豁免”机会。攻击者可以在真正动手前故意触发几次低危告警把某个IP、某个账号或某个文件路径“喂”进降噪名单里。一旦降噪规则生效他再利用同一个入口做高危险动作告警就被自动吞掉了。做降噪不是不行但绝不能做“一刀切”式的静默。推荐的做法是降噪只合并重复事件不丢弃不同维度的告警同一来源的告警可以降频推送但要保留在完整事件流里供事后追溯。5.5 云原生安全平台自身的盲区很多组织采购了容器安全平台以为装上就万事大吉。实际从攻击者的角度来看这类平台有几个很常见的盲区。第一镜像扫描只负责“进集群前”那道关。攻击者如果在运行时往容器里写入恶意脚本扫描器不会每次都重新扫描运行中的容器文件系统。第二运行时防护策略如果配得太严会导致大量业务容器被误杀所以很多团队会把策略调成“仅监控”。也就是说平台看到了异常但不会阻断只会记录。攻击者只要确认当前集群处于“监控模式”就可以放心大胆地执行后续操作。第三平台自身的Agent在容器里通常以Sidecar形式运行权限受限有时候连读取宿主机关键目录都做不到。攻击者只要避开Agent可见的进程和文件就能保持在盲区里活动。6. 防守方反制在攻击者必走的路上埋下六颗雷6.1 强制开启的审计对象清单说了这么多攻击者的思路最后落到防守方怎么干。第一步不是加规则而是把该看的日志全部看全。我建议至少覆盖以下审计对象Kubernetes API Server审计日志记录所有对API的请求包括谁在什么时候创建了什么资源。这是发现恶意工作负载的基石。kubelet日志连接节点和容器的桥梁容器逃逸前后kubelet会留下大量有价值的线索。关键节点的安全日志auth.log、syslog覆盖SSH登录、sudo执行、cron运行等行为。Docker/containerd的event日志记录容器的创建、销毁、挂载等操作。云平台的操作审计CloudTrail/ActionTrail记录所有云上控制台和API的调用。这几份日志必须实时外传到独立存储并且设置至少180天的保留周期。只要日志是完整的攻击者抹掉本地痕迹也没用。6.2 行为基线与异常建模告别裸奔式特征告警特征规则要保留但行为基线才是K8s环境检测的核心。具体做法是先用一段时间记录集群里的正常行为再基于这些数据建立基线。例如每个命名空间下通常有哪些工作负载、每个工作负载的进程列表是什么、节点上什么时候会批量拉取镜像、哪个账号经常在什么时间调用API。基线建立后凡是不符合基线的行为无论有没有匹配恶意特征都值得关注。举个例子某个Deployment平时从来不访问云元数据服务突然某个Pod开始频繁请求元数据接口哪怕流量不大也应该引发告警。再比如某个ServiceAccount平时只读Pod突然尝试创建Deployment这就是明显的权限异常。行为基线不需要太复杂的算法从CPU、内存、进程、网络连接、API调用几个维度做统计建模就能覆盖80%的异常。重点是把“偏离正常”作为告警触发条件而不是只依赖已知威胁情报。6.3 运行时阻断与最小权限的落地优先级光检测不阻断攻击者照样能把日子过下去。运行时阻断策略应该优先覆盖这几个高风险动作防护动作推荐策略优先级禁止特权容器PodSecurity Admission启用restricted级别最高禁止挂载Docker Socket在Pod创建阶段校验hostPath最高禁止容器内新增高权限systemd服务运行时检测主机侧unit文件变化高禁止容器访问云元数据NetworkPolicy阻断169.254.x.x高禁止非白名单进程写入cron主机Agent监控crontab文件变化中同时RBAC权限要做到最小化。每个ServiceAccount只授权它真正需要访问的资源Pod的automountServiceAccountToken尽量设为false从源头上减少攻击者拿到K8s API凭据的可能性。一个比较容易忽略的点是PodSecurityPolicy已经废弃现在应该用Pod Security Admission或OPA/Gatekeeper这类策略引擎来强制约束。策略最好在CI/CD阶段就嵌入而不是只靠集群运行时拦截。6.4 定期“敌情演练”用攻击视角检验防御体系再完善的告警规则不经过实战检验也无法证明有效。我非常建议每个K8s集群的维护团队定期做一次“敌情演练”也就是红队蓝队对抗。演练不需要搞得很复杂可以只是内部模拟几个典型场景模拟容器逃逸后新建systemd服务、模拟攻击者用ServiceAccount创建恶意Deployment、模拟攻击者清理本地日志。看这些动作能否在可接受的时间内被检测到、响应流程是否能跑通。每次演练结束把防守方的盲区和响应延迟记录成问题清单逐条修复。这样反复几轮告警体系的实战能力会明显提升也远比临到攻防演练时手忙脚乱要好。我在多支团队里推行过这种演练方式最大的收获是告警规则不是上线就完事而是要在一次次攻防中不断校正。很多规则配置之初看起来合理但只在理论推演里有效真正模拟攻击时才发现日志采集缺了一环。写在最后攻防视角是防守方最好的老师在我做攻防项目的这些年里一个反复验证的经验是防守方如果不了解攻击者的真实路径配置再多告警规则也像是在黑屋子里找猫。与其买更多工具不如先把K8s环境里的这条攻击链从头到尾走一遍——想清楚数据库和代码里最值钱的资产会放在哪、攻击者最可能从哪里进来、他进来之后会碰哪些东西、又会在哪里长期躲藏。这篇文章里的内容全部来自授权攻防演练和真实安全事件复盘后的总结。任何技术都像一把工具用对了方向才能创造价值。希望读到这里的同学尤其是正在负责K8s集群安全的同行能把文章里的攻击路径当作“敌情报告”来研究顺着这些思路去检查自己的审计日志、RBAC权限和运行时策略。防守和攻击从来不是孤立的事情真正有效的云原生安全体系永远是站在对方的角度思考出来的。
返回列表