半夜两点,某单位网络管理员给我打电话,说内网一台监控服务器CPU飙到100%,Web日志里全是奇怪请求。远程看了一眼,Cacti的poller日志里躺着一串明显不是正常主机名的命令执行痕迹。那台服务器部署Cacti已经六年,中间一次版本都没升过。那一刻我脑海里只有一个词:RCE。
Cacti这个老牌网络监控工具,部署量极大,从校园网到企业机房都能见到。过去一段时间,围绕它的RCE(远程代码执行)漏洞在安全工作圈被反复公开、反复利用,很多团队已经把"Cacti是否中招"列为了高危排查项。这篇文章不是简单贴一条复现命令就完事,而是把整个链条讲清楚:漏洞出在哪、为什么出现、怎么复现验证、怎么判断自己是否暴露、又该怎么干净利落地完成修复和加固。无论你是安全运维、网络管理员还是做应急响应的蓝队同学,这篇文章都值得认真读完。
1. 漏洞全景:Cacti RCE影响面与漏洞形态
1.1 Cacti在监控栈里的位置与暴露面
Cacti通过SNMP协议从交换机、路由器、服务器上采集设备数据,用RRDtool把数据画成曲线图,并配合模板、阈值做告警。典型部署形态是经典的LAMP架构:Linux操作系统加Apache/Nginx做Web服务,MySQL/MariaDB存配置,PHP处理业务逻辑。采集进程由cron周期性拉起poller.php,拉取数据后写入数据库和RRD文件。
部署位置通常在企业内网的核心区域。由于运维团队经常需要远程查看流量图,很多企业会把Cacti的Web端口用端口映射或反向代理暴露出来。这就给了攻击者一个非常理想的突破口:一个极少更新、功能复杂、权限边界又相对松散的老牌开源应用。从攻击者的视角看,内网监控服务器的价值极高——掌握全网设备清单、SNMP团体字、网络拓扑;运维习惯又使它常驻高权限账号、能联动其他系统;防护等级却往往不如核心业务数据库,很少进WAF和全量日志审计。可以说,在安全建设不完善的网络里,Cacti这类监控工具就是事实上的内网跳板机。
1.2 这个RCE的典型形态与影响版本
这次说的RCE,从公开漏洞资料来看,是组合链而不是单点漏洞。核心构成是认证绕过加命令注入。攻击者不需要账号密码,直接在请求头上做文章,绕过访问控制后,再在自动化采集的参数里种下恶意命令,最终以Web服务进程权限执行任意命令。
受影响版本相对清楚:1.2.22之前的版本基本都在影响区间。1.2.22及之后的版本才从根上修复了链路中的两个关键缺陷。安全研究人员在补丁发布后已经做了大量技术拆解,漏洞细节早已公开,不存在所谓"0day"信息差的问题。
这里要特别提醒一点:不要只盯着版本号。部分发行版并没有及时跟进上游代码,而是自己做了小幅修改,版本号虽然低于1.2.22,却未必受同一条链影响;反之,也有打了部分补丁但没打全的情况。所以版本号只能作为初步筛选,最终还要结合代码文件与行为特征来判断。
1.3 漏洞为什么值得被反复研究
一个RCE漏洞被公开后,常规做法是等补丁、给资产打补丁、观察是否被利用。但Cacti这种系统有个特点:很多部署已经运行多年,升级成本不低,不少管理员抱着"能跑就别动"的心态一直拖着。于是漏洞公开后很长一段时间里,互联网上仍然存在大量可被触达的目标。对防守方来说,深入理解漏洞的触发原理,不是为了去攻击别人,而是为了在最短时间内识别风险、给出有效的临时缓解方案,以及在日志里发现那些细微的入侵痕迹。
2. 根因拆解:两个致命点如何串成一条RCE链
2.1 认证绕过:信任了不该信任的请求头
先看第一环:访问控制逻辑。
Cacti的某些自动化脚本(主要分布在远程采集和自动化轮询模块),为了让请求能穿透反向代理,直接读取了X-Forwarded-For请求头的内容,拿它里面的IP做白名单校验。问题就在这里:代理层在转发请求时添加X-Forwarded-For,本意是让后端知道真实客户端IP。但应用层直接把这个请求头当作可信来源,而攻击者又能直接构造原始HTTP请求——那么伪造一个头部就可以把自己伪装成被信任的地址。
很多系统把应用放到Nginx或HAProxy后面时,都容易犯这个毛病:应用里还在用请求头做来源判断,代理却没有覆盖或过滤客户端传入的同名头部。如果Nginx配置里用了类似proxy_set_header X-Forwarded-For $http_x_forwarded_for这样的写法,那就是把客户端传进来的值原封不动往后端送,后端再拿它做校验,等于门禁完全形同虚设。
我拿生活里的场景类比一下:小区门禁系统,保安本来应该刷卡或者人脸识别来判断你是否有权限,结果保安直接问你"你是不是住户",你说"我是"就放你进去了。X-Forwarded-For校验就是这种"你说你是住户"的简化版——门禁本身不校验任何密码或签名,只凭一句自我声明放行。这个缺陷能成为RCE的第一环,是因为它直接让后续需要权限才能调用的功能,变成了完全公开的接口。
2.2 命令注入:未经干净的参数进入shell
第二环是真正的执行点。在认证绕过的后面,某个自动化采集逻辑需要把主机名或设备标识传给系统命令去执行。代码里用了字符串拼接方式调用shell,而且没有对特殊字符做严格过滤。
这里需要理解一个常见误区:很多人以为做黑名单过滤就够了,把;、|、>等字符屏蔽掉就安全了。实际上过滤的重点不在于列黑名单,而在于责任边界。凡是准备交给shell执行的字符串,都应视为完全不可信的数据。管道符、分号、美元符号、反引号、换行,在Unix shell里都各自有特殊含义。只要拼接进去,即便后面跟着的看似只是普通参数,也很可能被解释成新的命令。
举例来说,假设正常逻辑是这样一条命令:
ping -c 1 主机名如果攻击者把"主机名"参数构造为:
主机名; whoami分号后面的whoami就会被当作新命令执行。类似的分隔符还有&&、|、换行符等。实际场景里攻击者当然不会只跑whoami,而是会利用任意命令执行能力去下载恶意载荷、反弹会话或者植入后门。
命令注入的根本教训是:永远不要在拼接命令时信任用户传入的参数。要么用参数数组方式调用,避免经过shell解释;要么做双向的严格白名单校验,只允许主机名字符、数字、点和短横线。任何复杂度带来的便利,都不值得用代码执行风险去换。
2.3 两条链串起来之后的真实价值
把两步连起来看,攻击者连账号密码都不需要,只要目标机器上的Cacti Web端口可访问,就能完成从网络请求到命令执行的全流程。这也正是这个漏洞被列到高危清单的原因——利用成本极低、影响面广、一旦成功就是服务器完全控制权。更麻烦的是,Web服务进程如果恰好以root运行,那么命令执行权限直接就是管理员,攻击者拿到的是整台主机的完整控制权。
对防守方来说,理解完整链路特别重要。单纯堵住认证绕过,虽然进不了这一个入口,但同类的信任错位可能还存在于其他脚本里;单纯堵住命令注入,认证绕过也可能让其他未修复的入口继续暴露。两个缺陷必须一起修,这正是官方在1.2.22版本里同时处理两处代码的原因。漏洞修复从来不是打一个补丁那么简单,而是要审视代码的信任边界和命令拼接规范。
3. 搭建验证环境与复现路径(仅限授权测试)
3.1 用Docker快速复现目标版本环境
复现RCE必须在隔离环境里做,授权边界必须清晰,这一点没有商量余地。我自己做验证时,首选Docker。原因是Cacti依赖的PHP版本、MySQL版本、RRDTool、SNMP组件比较多,手工装环境既慢又容易污染宿主系统。用Docker可以拉起一个和目标场景高度一致的靶机,验证完直接销毁,不留下任何残留。
基本步骤是这样的:
- 准备一台能运行Docker的Linux主机,建议至少2核CPU、4GB内存;
- 拉起一个带旧版本Cacti的容器环境,把版本固定在1.2.2x区间内;
- 同时启动一个MySQL容器作为数据库后端,配置好网络互通;
- 完成Cacti的Web安装向导,设置管理员密码和数据库连接信息;
- 启动poller进程,确认基础监控能正常拉取数据。
我建议在Docker里单独创建一个隔离网络,只让宿主机和靶机互通,不要和办公网、生产网有任何路由关系。验证环境的网络隔离,本身就是授权测试的基本素养。
3.2 分步验证:先认证绕过,再命令注入
刚开始接触这个漏洞的同学,习惯一上来就找一个完整脚本直接跑一遍。这种"黑盒一键打穿"虽然很爽,但对理解漏洞帮助不大。我更推荐分层验证的方式。
第一步,验证认证绕过。用curl构造请求,在请求头里添加一个伪造的X-Forwarded-For字段,值设置为代码中白名单网段内的地址,然后尝试访问需要认证的自动化脚本接口。如果返回结果与不带该请求头时有明显差异,比如从拒绝访问变成正常响应,就说明认证绕过已经成立。
第二步,验证命令注入。在确认能触达接口后,往主机名参数里附加一个无害的命令分隔符和无害命令,观察返回内容。我个人建议使用id或者echo一段随机字符串做验证,不要直接反弹shell,更不要在调试阶段使用破坏性命令。这一步的目的是确认命令是否进入了shell并被执行,而不是追求所谓的"利用效果"。
第三步,观察执行结果与日志。命令注入是否成功,不一定要靠回显,也可以在poller日志中观察有没有对应的进程记录。Cacti执行采集命令时会留日志,如果日志里出现了不属于正常主机名格式的内容,就可以确认命令确实进入了shell并且被解释执行。结合Web访问日志和poller日志做交叉验证,能更准确地判断漏洞是否真实可利用。
3.3 复现过程中的常见坑
我复现时踩过的坑不少,挑几个典型的分享一下。
第一,PHP版本不对。旧版Cacti对PHP 7.x和8.x的兼容性差别很大,有的代码在PHP 7.4下正常,在8.1下直接报语法错误。如果镜像里的PHP版本太新,复现时接口可能根本无法正常工作,容易被误判为"漏洞不存在"。
第二,poller没有跑起来。很多同学环境装好之后,忘了配置cron,也没有手动启动poller进程,导致采集模块根本没进入工作状态。而这个RCE链路的命令注入点就藏在poller调度路径上。poller不运行,后面的验证自然做不通。
第三,目录权限问题。Cacti需要写日志、写rra文件、写缓存。容器里如果挂载了宿主目录而没有做好权限映射,接口会静默失败,返回结果看起来像是被修复了,实际上根本没有走到注入代码。遇到接口异常时,先看Cacti目录下有没有正常生成日志文件,再判断是权限问题还是代码问题。
第四,忽略数据库初始化。有些Cacti镜像为了控制体积,裁掉了部分初始化数据,导致登录后页面报错,部分接口虽然暴露但无法正常理解输出。针对这种情况,最好用官方标准安装流程完成初始化,不要贪图镜像的"精简"。
4. 风险排查:快速判断你的Cacti是否暴露
4.1 版本自查与补丁核对
有资产台账就先看版本。Cacti的版本信息一般在include/version.php里,命令行可以直接查看:
grep const version /var/www/html/include/version.php也可以在登录Cacti后,在页面底部的About选项卡里看到版本号。对照官方安全公告,1.2.22是最低安全版本。低于这个版本号的,默认视为高风险,进入紧急处置流程。
但这里要小心两点。一是发行版二次打包,可能改了代码而没有升版本号,或者升了版本号而没有改代码。二是老版本树上打了commit级别的安全补丁,版本号依然显示为1.2.22以下。所以版本号判断只是第一步,随后要配合代码审计和补丁核对。重点确认修复补丁是否同时覆盖了认证绕过和命令注入两个部分。只修一半的版本,仍然可能被攻击者利用另外一环完成突破。
4.2 日志与流量特征检测
如果已经怀疑被攻击,或者正在被扫描,重点看Web访问日志。以下几条特征值得高度关注:
- 大量请求集中在
remote_agent.php、poller_automation.php这类自动化接口; - 请求头中出现多个不同的X-Forwarded-For值,且值集中在常见的内网网段范围内;
- 请求参数中带有shell特殊字符,比如分号、竖线、反引号,或者出现
bash、curl、wget等关键字; - 同一来源IP在短时间内的请求频率远超正常监控轮询频率。
在poller日志方面,cacti.log里如果出现异常命令拼接痕迹,尤其是主机名字段后面跟着一串明显不像主机名的字符,也需要立即溯源。常用检测命令类似:
grep -iE 'remote_agent|poller_automation' /var/log/httpd/access_log有条件的话,建议把这些检测规则接入SIEM或日志平台,用告警代替人工盯屏。特征再明显,没有留痕和告警,对应急响应来说都是空谈。
4.3 三种高风险部署形态
即便版本安全,糟糕的部署形态也会让整体风险被放大。我在这里列出三种最常见的高危形态。
第一种,Web端口直接暴露公网。严格来讲,Cacti这类运维工具的管理端口不应该直接出现在公网上。远程运维应当通过带身份验证的安全接入方式访问内网基础设施。如果因为历史原因已经暴露了,应当尽快收敛。
第二种,Web服务进程以root身份运行。一旦RCE发生,命令执行权限直接是root,攻击者直接获得整台服务器的控制权。正确做法是在系统层面创建低权限专用账号来运行Web和PHP-FPM。
第三种,存在横向可达的宽泛网络。如果Cacti所在主机与整个内网没有网络隔离,一台监控服务器沦陷,基本等于整个内网沦陷。建议至少把监控服务器放入独立区域,通过防火墙严格放行必要端口。
5. 修复与加固实践
5.1 升级到安全版本的标准流程
升级前要做的第一件事不是下载安装包,而是完整备份。备份内容至少包括三部分:Cacti的数据库、Web根目录下的源码目录、以及配置文件(主要是config.php)。备份之后先验证备份文件可恢复,再开始操作。
操作顺序建议如下:
- 停止cron中所有与Cacti相关的任务,避免升级过程中poller再次执行写入数据;
- 停止Web服务,或至少让Cacti进入维护状态;
- 把新版源码解压覆盖或替换旧目录,注意保留
config.php; - 访问升级页面,执行数据库结构更新脚本;
- 清理缓存目录,恢复cron任务;
- 验证登录、验证poller数据写入、验证绘图是否正常渲染。
升级过程中最容易出错的是数据库连接配置。升级页面会尝试连接数据库并执行结构变更,如果老版本里用了独立数据库账号或自定义socket路径,升级脚本可能找不到配置。建议在升级前把config.php里的数据库配置完整核对一遍,最好做成参数化,避免代码覆盖时被误改。
5.2 无法立即升级时的临时缓解措施
不是所有团队都有条件立刻升级,特别是在业务高峰期或运维窗口紧张时。这时可以先用临时措施把风险按住。
第一,在Nginx或Apache层剥离客户端传入的X-Forwarded-For。在反向代理层级强制覆盖这个请求头,只保留代理自己添加的值,后端一律忽略客户端原始传入值。这个做法能在一定程度上瓦解认证绕过的前提。
第二,对自动化脚本接口做来源白名单。只允许监控服务器所在网段或合法运维跳板地址访问remote_agent.php和poller_automation.php,其他来源一律拒绝。
第三,在Web服务器层启用HTTP Basic Auth。也就是在应用之外再加一层账号密码认证,攻击者即使绕过了应用自身的访问控制,还要再过Web服务器的认证。
第四,在防火墙层面对公网关闭Cacti的Web端口,只保留内网访问。
临时措施一定要有明确的时限和责任人。这类措施一旦上了生产,很容易被遗忘。在临时措施生效期间,应当持续关注安全版本发布情况,具备条件后马上走正式升级流程。
5.3 纵深防御:安全不是一次升级的事
最后说点实打实的经验。安全加固不是升级完就结束,而是要把功夫花在平时。
- Web服务运行账号尽量用独立低权限用户,不赋予sudo权限;
- 给Cacti用的数据库账号单独创建,只授权Cacti库的权限,不要顺手给全局超级权限账号;
- 数据库主机不要和Web混用一台机器;如果必须混用,至少开启本地socket认证并禁用远程root登录;
- 对Cacti管理入口启用额外的认证手段,或者至少开启HTTP Basic Auth;
- 定期做漏洞扫描,把Cacti这类开源监控工具纳入资产巡检清单,而不是等出事了才想起来看版本;
- 监控平台自身的日志和系统日志要集中外发,防止攻击者拿到权限后清空本地日志。日志外发性越高,应急响应的证据链就越完整。
实际上,抛开Cacti这个具体对象,上面这套做法做扎实了,可以覆盖很多同类Web管理工具的加固需求。安全是工程问题,不是一个补丁问题。复现只是手段,把RCE链路里的每一次绕过、每一处拼接、每一条日志都看清楚,防守的时候才能知道该在哪里设卡。
最后分享一点我个人的体会。完整复现这个RCE的过程,比最终拿到执行结果更让我印象深刻。因为只有当你亲眼看到那个被轻信的请求头、看到被拼接进shell的命令、看到日志里留下的痕迹,才会真正记住"不可信数据"这四个字的分量。在真实巡检里,我发现大量Cacti部署都存在同样的问题:版本老旧、公网暴露、配置宽松。好在这个漏洞的修复方式比较清晰,升级到安全版本后基本可以挡住已知链路。如果你发现自己管理的网络里也有类似的监控系统,建议今天就做一次排查。别等到半夜两点的电话再响起来。