mysql注入教程
php数据库基本操作
这个主要讲的是代码分析
数据库连接
首先看到简单的数据库连接操作
1 | |
其中重要的函数mysqli_connect解释如下
1 | |
其参数的位置是有严格要求的
sql执行
接下来就是sql执行操作
1 | |
一共有三个参数
1 | |

执行结果
储存执行结果的函数有很多
这里有一个表,可以清楚的表达这些函数的含义

这些函数的一般操作就是
1 | |
其中$row是mysqli_query的执行结果
报错
回显报错就很简单了
1 | |
OK了知道了这些就可以写一个简单的数据库查询代码了
1 | |
sql语句信息收集
在这里不讲sql 的语句,而是讲在注入的时候可能会需要使用的信心收集语句
1 | |
是不是如果有了一些信息,可能在后续的渗透过程中,攻击面可以大大增加
判断注入类型
对于拿到一个可能存在sql注入的网站,肯定是要先判断出注入的类型是数字型还是字符型
在构造pyload的时候,我们需要自己慢慢去判断写下目标服务器后端的sql处理逻辑
先来看到数字型注入判断
1 | |
已知服务器处理逻辑,pyload可得
1 | |
如果对方服务器是上面这种逻辑的话(为数字型)第二条语句逻辑错误,查询不到语句,放回空
所以:第一条返回正确,第二条返回空是数字型注入
现在看到字符型注入
思路还是跟数字型的一模一样
1 | |
字符型注入后端处理逻辑
1 | |
所以:第一条返回正确,第二条返回空是数字型注入
注意:如果将数字型的pyload注入的话,两条返回的都正常
注意:字符型注入,有双引号和单引号,这两个都需要去尝试
看报错信息
在数字型注入的时候,注入?id=1” and “1”=”1会出现错误

关键部分
1 | |
这个报错实际上说的是mysql不认识” and “1”=”1 LIMIT 0,1

所以知道了它是认识1的,即数字型注入
字符型注入也是同一个原理
主要还是看单引号里面的信息
联合注入
注入基本流程
现在正式开始sql注入了
我们可以看到存在联合注入的服务端的代码
1 | |
下面给出经常使用的payload
1 | |
在这里使用的方法很单一,比如这里使用的是group_concat函数包裹内容,其实还有很多的方法,可见后面的函数代替板块的内容或者是绕过板块
多select手法
其实我感觉这个方法很鸡肋,使用两个select执行sql,虽然很鸡肋,但是对于理解sql语句很有帮助
1 | |
一个小逻辑
一般来说服务端的写法长这样
1 | |
这很简单,但是如果服务端的代码写法长这样呢
1 | |
在这里使用了while虚幻处理$row的值
这里就是我所说的一个小逻辑
如果我们能把sql语句构造成这样呢
1 | |

可以知道如果服务端使用的是循环处理$row的值,我们可以这样构造注入
1 | |

布尔注入
联合注入告一段落,迎来的是布尔注入
特征:
查询语句没有输出结果,以及输入的数据正确是一个页面错误是另外一个页面
对于这种注入最重要的方式是你要会写脚本,写不出连贯的也可以只写一部分一部分的
先不管脚本啥的,先看看一般的注入方式
1 | |
其实不使用ascll也是可以的,适合burp,看你心情
下面的处理更加细致
1 | |
会写脚本之前要会用sqlmao
1 | |
对于有些会禁用一些函数的情况见后面的内容
下面是我写的脚本,可以作为参考
1 | |
时间注入
这个跟布尔注入很像但是也有不同
没有正常回显,没有报错回显或者是不管输入什么东西只有一种回显效果,无法判断是正确还是错误
但是同样的是对于会写脚本是很重要的
先看看服务端的代码逻辑
1 | |
这里介绍两种一般的注入方式
使用and配合ascii执行sleep
1 | |
还有一种手法
使用and配合if执行sleep
1 | |
依旧sqlmap的使用教程
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
接下来就是看我写的又长又丑的python脚本了
获得数据库名称
1 | |
获取每张表的名称
1 | |
获取重要表的字段数量及名称
1 | |
获取所有字段的键值对
1 | |
报错注入
报错是一个很重要的报错,因为使用起来很方便
下面会介绍很多的方法,有些方法会受到版本的限制
可以看看下面的这张图片

服务端的代码很简单时间,只需要有回显报错的代码即可
1 | |
xml报错注入(5.1.5~?)
这两个报错注入是最近常使用的
版本的范围也是很宽的
先将updatexml报错
报错原理如下
updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)
例如: select * from test where ide = 1 and (updatexml(1,0x7e,3)); 由于0x7e是~,不属于xpath语法格式,因此报出xpath语法错误。
看到updatexml的基本使用方法
1 | |
在这里我们先不讲如果显示不完整怎么办,在这个模块的后面会加上
接下来就是extractvalue报错注入了
原理差不多
extractvalue使用时当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)
select user,password from users where user_id=1 and (extractvalue(1,0x7e));
由于0x7e就是~不属于xpath语法格式,因此报出xpath语法错误。
下面是报错注入的基本使用方法
1 | |
这两个方法很像,但是使用方法还是有点不一样的
1 | |
所以使用起来是需要注意的
众所周知0x7e是报错的主要原因,如果0x7e被限制了,怎么办呢
当然还有很多备用的方案
1 | |
逻辑与命名类(mysql5.0~8.0)
NAME_CONST(鸡肋)
直接开门见山,这个报错注入的原理就是列名重复
基础的语法
1 | |
现在看到基础的使用
1 | |
可以知道这个函数的意思是生成临时表,第一个参数是列
这时候就可以想了,如果两个列的名字是一样的话,是不是就会发生错误
于是可以看到下面这个payload
1 | |
知道原理是要有两个重复的列名
但是有一个问题
1 | |
上面这个可以执行,但是查询不了databas()
不知道是为啥,所以我给这一小节取的名字是鸡肋
join using()注列名
对列名注入是一个很好的思路,mysql 对于重复的列名会报错
于是就有了一个新的方法
还是先从基础的来
众所周知派生表是需要别名的,这个知道吧
于是我们看到下面这个内容
1 | |
解释一下流程
1 | |
再看一个sql语句
1 | |
发现有4列
执行的过程如下
1 | |
那么执行
1 | |
必然是4x4十六列
1 | |
这里插亿句题外话
到这里是不是有一个感觉,是不是可以进行dos操作呢
没错感觉!对着呢!
如果把数字换成表,换成哪些行数多的表,是不是这样的执行的行数就会非常的大
1 | |
可以看到有上亿行数据,对了使用逗号隔开和使用join连接的效果是一样的
在后面再加一个join information_schema.columns D之后就能满足你的好奇心了
什么不满足好奇心??
注意有些版本的数据库做了优化,让这些系统表储存在内存里面,所以可以快速响应
1 | |
看看效果
1 | |
足足跑了一个小时多
总结一手
1 | |
好像讲歪了,我们不是讲报错注入吗
现在开始讲报错注入
1 | |
这个知识点前面我们讲过,将两个表拼接在一起,这是不会报错的
我们只需要将上面这个作为派生表即可,派生表的列名必须唯一,所以会出现报错注入
再看下面这个例子
1 | |
可以看到爆出了表的列名了
好奇怪怎么始终得不到数据库名字的报错注入
好像只能报错注入列名
在前面我们已经获得了id这个列名了,现在需要排除获得后面的列名
1 | |
继续这样操作
1 | |
主键重复floor(5.5~5.7)
在学习这个报错注入的时候需要一些基础知识
as基础
1 | |
group by基础
1 | |
floor方法基础
1 | |
rand()基础
1 | |
这里基础已经看的差不多了,接下来就是看payload了
看到最基础的payload
1 | |
这个是用来爆出数据库名字的
floor(rand(0)*2对于这个根据前面的知识可以知道返回的结果肯定是0或者是1,其实是固定的,构造语句执行可以知道
1 | |
可以拿到输出的结果是011011这些值是固定的
下面就是讲解漏洞产生的原因是什么了
在前面我们已经知道group by就是分组的作用,其实它还有一个特性就是会创建一个虚拟表用来分组
1 | |
是不是很好理解,这样floor报错注入的原理就讲完了
现在看看总的注入执行语句
1 | |
几何函数(mysql<=5.5)
注意这个版本限制
为啥这个可以报错注入
multipoint()、linestring()、polygon()、geometrycollection() 这些函数是 空间几何函数,它们期望接收符合 WKT(Well-Known Text)格式的几何数据
合法的WKT数据长这个样子
1 | |
在这里举一个下面马上要将的几何函数multipolygon
最简单的一个payload
1 | |
可以发现这个payload是需要嵌套三个select的
现在我们详细的解释一下这个payload
select version()执行危险的sql语句
然后
1 | |
把第一层的返回值当作一张临时表 a,然后用 SELECT * 取出这个表的所有内容
别名 a 必须加,MySQL 规定所有派生表必须有别名
第一次包装结束,下面开始第二次包装
1 | |
再把第二层的结果包装成临时表 b,继续用 SELECT * 取出
最后出发报错的函数出现
1 | |
然后需要使用一个select查询一下,是因为这样满足mysql的语法
然后有一个问题,为什么需要三层派生嵌套
因为
1 | |
然后这些几何函数的报错注入都差不多的
multipolygon
1 | |
最后综合的注入流程
1 | |
multipoint
1 | |
这个的完整注入方式与multipolygon一样的,只需要替换一下函数即可
polygon
1 | |
这个也是只需要替换一下函数名即可
eometrycollection
1 | |
这个也是只需要替换一下函数即可
linestring
1 | |
同理还是只需要将函数一换即可
multilinestring
1 | |
同理
至此几何函数已经结束了,接下来就是地理哈希类函数的报错注入
地理哈希类(mysql>=5.7.x)
注意版本
首先肯定是原理解释
这些地理哈希函数都有一个共同点:它们都是 MySQL 的空间地理函数,都期望接收合法的 GeoHash 编码字符串作为输入
这些函数能接受的字符有哪些?如下
1 | |
任何不在这个范围内的字符(比如 ~、@、大写字母)都会导致解析失败
所有原理感觉就好像xml报错注入
具体的函数有哪些呢?如下
ST_LongFromGeoHash()
1 | |
我们可以尝试一下完整的注入过程
1 | |
ST_LatFromGeoHash()
这个也是同理
1 | |
同理
ST_PointFromGeoHash()
1 | |
注意注意这个就不一样了,这个是有两个参数的
1 | |
到这里就结束了
接下来解释GTID函数的报错注入
GTID函数类(mysql>=5.6.x-显错<=200)
GTID(全局事务标识符)
还是先从原理出发,讲解一下
这两个函数都要求传入符合规范的 GTID集(一种特定格式的UUID和数字组合)
这个GTID集大概长这个样子
1 | |
就是一大串数字
如果我们输入的字符不是这个的话就会报错,实现报错注入
这里只有两个函数可以利用报错注入
gtid_subset
1 | |
可以看到这个是有两个参数的,所以写的时候是需要注意的
完整的注入流程
1 | |
GTID_SUBTRACT
1 | |
一样的,换一个函数即可
数值溢出类(mysql5.5.x)
取反减法溢出(< 5.5.53)
注意这个报错注入的版本
按位取反溢出有很多的函数,在这里我拿!感叹号取反来操作
依旧先从原理出发
看到这个简单的payload
1 | |
看到里面的子查询没有啥问题,都能看得懂
1 | |
可以看到输出的结果是一串字符
在mysql里面任何非零数字和非空字符串都是true
所以!(true)的结果是false,false在mysql里面就是0
所以我们做了这么多的功夫!(select * from (select version())x) 这一串就是0
接下来就是0其中是取反符号,0的二进制8个0,取反之后就是一个巨大的数字
1 | |
所以到了这一步,就是0-18446744073709551615得到-18446744073709551615
1 | |
可以发现报错了
因为这个负数,它远远小于 BIGINT UNSIGNED 类型能表示的最小值 0。MySQL 无法在无符号整数的范围内表示这个结果,因此就抛出了 BIGINT UNSIGNED value is out of range 这个错误
所以这个就是取反报错的原理
下面就是完整的报错注入语句
1 | |
字符取反溢出
原理还是差不多的
看到这个payload
1 | |
(select * from(select user())x)的含义是mysql对于不是数字的字符进行取反操作的时候会自动转化为0,所以这一坨东西就是0,是一个很大的整数最后加1造成了最后的报错注入
函数配合取反
这个的操作其实使用的原理还是上面哪些,只是多了一个函数,所以使用起来是很鸡肋的
1 | |
没有什么新的东西
有很多很多这种函数
可以使用burp工具fuzz一下
1 | |
在上面那个字典里面有些的使用方法不一定是SELECT xxx(~(SELECT * FROM(SELECT DATABASE())X)+1);
1 | |
exp报错注入(5.5.x)
基础
1 | |
报错注入原理
1 | |
利用
看到看懂下面这个执行语句即可
1 | |
cot(5.5.x)
这个差不多的
MySQL 尝试先把 root@localhost 转换成数字,再计算余切值
1 | |
uuid报错(8.0)
UUID_TO_BIN
简单讲一下原理
1 | |
于是我们可以这样利用
1 | |
bin_to_uuid
1 | |
后续的注入语句就很简单啦
报错注入显示不全
在使用报错注入的时候经常会有报错显示限制
所以我们要加上 limit 0,1
例如
1 | |
还可以使用mid函数
MID() 函数的主要功能是从一个字符串中截取指定长度的子字符串
1 | |
还可以使用substring函数
1 | |
登录注入
终于结束报错注入了,接下来就是最常见的场景,登录框注入
前面我们已经学习了联合注入,时间盲注,布尔注入,报错注入,接下来看看有什么利用场景
登录框
我们先看看使用php写登录逻辑怎么写的
1 | |
这个是一个有联合注入漏洞登录框
还是同一个道理,有没有报错回显,有没有登录成功的特征主要是看代码是怎么写的
在登录注入的时候需要对用户名和密码都要进行填写的话,这个时候就需要注意了
一般来说是在用户名那里输入万能密码,在密码那里注入
1 | |
这样是不是就很清晰了
如果是没有必须要输入账号和密码的话,则只需要在一个地方注入即可
事实上我们只需要这样注入了可以了
1 | |
这样登录框的注入逻辑就已经搞定了,
update型注入
常见于重置密码
里面涉及一些没有见过的函数
我们先看到下面这个代码(以sql-labs的第17关卡为例),这个是服务端的执行检查代码
1 | |
首先看到$value = substr($value,0,15);这个主要的作用是限制,输入的字符长度
然后就是这个函数get_magic_quotes_gpc(),这个函数不执行添加操作,仅用于检测 PHP 是否已自动对 GET、POST 和 COOKIE 数据中的单引号、双引号、反斜杠及空字符添加了反斜杠,所以说如果已经添加了反斜杠之后就返回1如果没有就返回0
然后就是stripslashes()这个函数的意思是去除变量里面的\,这一步主要是为了清洗数据
所以get_magic_quotes_gpc()和stripslashes()的配合是为了
1 | |
接下来就是ctype_digit()这个函数的意思是检查 $value 是否只包含数字 0-9(不接受负号、小数点、空格)
这样做就是使得如果输入的是数字的话就进入下一个if,所以这个if里面的就是字符
然后就是mysql_real_escape_string()函数转义 SQL 语句中使用的字符串中的特殊字符,
最后就是intval()这个函数的意思是将变量强制转换为整数类型
一旦转为整数,它就不再是字符串了。在 SQL 中,数字不需要加引号,因此彻底杜绝了 SQL 注入(因为注入通常需要单引号 ‘ 来闭合语句)
所以说这个check_input函数的是非常严格的
这个代码使用在$uname=check_input($_POST[‘uname’]);,所以我们得放弃对uname注入
于是对passwd进行注入
注意因为代理里面的逻辑就是必须要有username,我们肯定是知道username所以才能更改密码的
报错注入
要注入的语句如下
1 | |
username 已经没有必要注入了,过滤非常严格
于是我们呢在password里面注入
看看下面这个payload
1 | |
按照流程开始完整的注入方式
但是这会遇到一个问题
select group_concat(username) from users的时候是不能使用的
1 | |
报错内容是:您不能在 FROM 子句中指定要更新的目标表 ‘users’
属于是我们要更新users但是又要使用users了,所以会报错
我们可以用其他方法绕过 ,将表名users用(select username from users)a替换掉
使用(select username from users)a 查询出来的结果如下
1 | |
SQL 允许在 FROM 子句中使用子查询,并将其视为一个临时表。这个子查询必须有一个别名(如 a),以便在外部查询中引用。
SQL 标准允许在 FROM 子句中使用子查询,只要子查询被赋予一个别名,就可以被视为一个临时表。
于是注入语句如下
1 | |
于是完整的执行语句如下
1 | |
这里提一点报错注入有一个问题,字符显示问题,报错回显只能显示32个字符
处理也很简单
1 | |
或者
1 | |
时间盲注
面对update注入除了报错注入之外还有时间盲注
在面对的这个的时候
1 | |
怎么构造才能出发时间盲注呢
很简单
这样构造
1 | |
但是这样构造会遇到一个问题,这样会修改这个表里面所有的password的值,并且还有一个很重要的问题,如果有13行的话就会执行13次sleep,也就是说每一次update更新就会sleep一次
怎么解决每次都触发时间呢
答案很简单加上where限制
1 | |
时间盲注的完整注入方法就不写了,其实差不多的
有没有更加优雅的方式,当然有
1 | |
而且这个方法不需要使用注释符号和where限制
可以说这个方法是更好的方法,在这里我不多讲,详情剑insert型注入的时间盲注那里,先拜拜
布尔盲注
除了时间盲注之外还有他的亲兄弟布尔盲注
我们可以自己写一个页面理解一下
1 | |
注入语句如下
1 | |
剩下的布尔注入语句就是正常操作了
INSERT型注入
update型报错注入已经完成了,接下来的就是inset注入
看到insert服务端代码,这里我以sql-labs的less18关为例
1 | |
less18关的逻辑很简答,就是登录成功之后,执行插入语句,将
1 | |
这些信息插入到一个表里面
这个$_SERVER有非常多的参数,下面是这些参数的解释
1 | |
所以也就是说value的值是我们自己控制的
报错注入
看到一个简答的insert报错注入语句
1 | |
1 | |
insert跟update一样是不支持联合注入的
所以接下来就是时间盲注了
时间盲注
在前面的update时间盲注那里我们讲了一个更加优雅的方式,我们在这里实现
1 | |
解释一下payload
VALUES 括号里不一定要写死值,可以写表达式,MySQL 会先计算再插入
可以看一下执行过程
1 | |
原理已经清楚了,可以说这个是一个很巧妙的方法
1 | |
update的时间盲注的方法也是可以使用的
1 | |
这里不多讲
这个好像不能实现布尔盲注,因为插入语句,只要是字符或者数字就可以插入
二次注入
对于这个我会分两部分,一个是对于sql-labs中的less24的分析
二是分析我写的一个题目
less24分析
这一关更多的是代码审计
这个关的功能点有三个,一个是创建新用户,一个是登录,一个是登录之后的修改密码
注入点在修改密码那里
这个很精彩
这一关的利用过程很简单,利用过程一笔带过即可,之后的笔墨用在代码审计上面
首先新建一个用户,用户名为
1 | |
密码随便一个
然后登录进去,修改密码,即可,这样就可以把数据库里面原本的admin账户的密码修改为我们自己设置的
来到代码审计
我站在上帝视角看看,为什么会有sql注入
直接说为什么
看到pass_change.php这个文件

可以到一个很危险的行为,开发者考虑的很不周到
开发者对$pass和$curr_pass都做了过滤,这很好,但是我们可以明显的发现,开发这并没有对$username进行过滤,而是直接通过session里面的值进行数据更新sql语句执行的,而session里面的username的值是我们刚刚创建的新的用户的username,于是我们可以利用这个可以控制的参数和没有过滤的值进行注入
这就是漏洞利用的地方
下面这些文件是我对less24简化的代码,
index.php
1 | |
logged-in.php
1 | |
new_user.php
1 | |
config.php
1 | |
我只是简化了一下这个关卡的代码可以更加好的看到注入的逻辑,漏洞点还是logged-in.php
自制题目
我总感觉差点意思,然后在预处理的代码里面获得了思路,于是思路如下
logged-in.php的代码修改为如下代码,使用的是预处理
logged-in.php
1 | |
其他的代码没有修改,只修改了这个文件
注入点是UPDATE users SET password=? WHERE username=’$username’ AND password=?
可以看到$username 是我们可以自由控制的,于是就可以影响着注入语句,但是这个自制题目与less24的不一样的地方在于
注入的payload就需要多一个操作了
1 | |
也就是说我们需要不改变预处理的问号的个数,避免预处理报错
所以可以采用时间盲注,所以得到下面的payload
1 | |
对于这个题目,我写了一个粗糙的脚本(实力有限,自己写出来的效果不是很好),如下
主要的思路就是构造攻击链
1 | |
该题目已经开源到了我的仓库ctfbox,里面有详细的解释和题目docker镜像,可以一键导入
到这里二次注入就已经结束了
宽字节注入
接下来迎来的就是宽字节注入
宽字节注入是一种针对GBK 等宽字节编码数据库的 SQL 注入绕过技巧,核心原理是吃掉转义符反斜杠 \
GBK 是专门用来表示简体中文的一种双字节编码

简单说一下场景,就是当服务端将单引号进行转义,也就是说服务端的操作是如果匹配上攻击者输入的单引号就在单引号的前面加上转义字符\
面对这种情况,我们该怎么操作呢
如下
1 | |
在这里总结一下
1 | |
所以进行宽字节注入的条件是服务端的代码在使用mysqli连接的时候要配置支持gbk字符
1 | |
这就是告诉sql使用gbk编码
在sql-labs中对应的关卡是less-32/33/34/35/
1 | |
check_addslashes函数的作用是讲\和’和”转义为普通字符,让闭合失效
其功能类似于
1 | |
这两种检查方式是一样的
先给一个payload看看
1 | |
可以看到我们在’单引号的前面加了一个%df
执行过程是
1 | |
于是完整的payload如下
1 | |
当然肯定不止%df,还有很多。理论上
1 | |
0x81 ~ 0xFE这个范围的都可以拼接为汉字
下面列几个
1 | |
除了构造成中文之外还有两种
1 | |
编码转化
1 | |
土耳其语
1 | |
值得注意的是这些方法都比较老,现代 PHP 和 MySQL 在正确配置下已经很难利用了
如果是post注入的话,最好是使用工具注入,因为在浏览器注入会破坏%df
1 | |
温馨小提示:还有一个是需要注意的。后面我们在爆字段的时候表名是使用单引号包裹的
我们可以使用十六进制绕过对单引号的限制
1 | |
less36/37出现了一个新的函数

$string= mysql_real_escape_string($string);这个也是可以使用宽字节绕过的
但是也防不住宽字节注入
堆叠注入
在宽字节注入之后就是堆叠注入了,这个危害很大,所以操作空间会很大
下面开始正式的讲
这里有一个重要的函数mysqli_multi_query()这个是可以执行多条sql语句
我们可以自己写一个漏洞堆叠注入的代码
1 | |
可以看到与常规的注入代码不一样的地方在于
常规的注入代码如下
1 | |
而使用堆叠注入的代码可以这样写
1 | |
不同点在与这个需要用到mysqli_multi_query()这个函数
对于这个函数基本的概念如下
mysqli_multi_query() 函数执行一个或多个针对数据库的查询。多个查询用分号进行分隔,所以这就满足了堆叠注入的条件
常规的堆叠注入
其实这个主要就是会执行sql语句即可
insert插入数据
1 | |
update更新数据
1 | |
delete 删除数据
1 | |
下面给一个小小的代码审计
代码来源于sql-labs的less-42/43/44/45
1 | |
username被限制了,但是password可以操作,于是就可以堆叠注入了
1 | |
当然这里可以使用联合注入和报错注入等其他快速获得密码的方法
less-43闭合方式不一样,剩下的差不多了
可以看到这一小节的名字是常规的堆叠注入,所有下面会介绍协一些不常规的堆叠注入
handler查询法
这里有一个sql执行语句
1 | |
解释一下这个执行语句
首先是使用security这个数据库,然后是使用handler打开users表别名设置为p
在这里我们先解释一下什么是handler
1 | |
看一下ai的对比

了解完handler之后咱们继续
然后handler p打开已经操作的句柄p,read first是直接简单粗暴的把底层的存储引擎返回出来
在自己本地进行实验的时候执行完handler之后最好是使用handler p close
1 | |
但是这样只能读取一个数据有没有什么方法是可以读取很多数据呢
有!
1 | |
然后就这样一直next下去即可
现在自己写代码搭建一个漏洞网站试一试(为了详细的看看输出的效果于是我修改了一开始给出来的代码,加了一个循环输出功能)
1 | |
于是我们使用刚刚学习到的注入语句
1 | |
预编译绕过
这个是不是听起来很高大上呢
预编译其实就是提前写好
我们先打打基础
1 | |
执行结果如下
1 | |
对于这个可以知道在mysql里面可以通过使用@设置变量
在这里扩展一个mysql的变量知识
1 | |
了解完这个之后咱们继续讲解预编译基础知识
1 | |
句柄名是我们自己取的名字(记得与后面的语句一样就好了)
字符串表达式是上面我们上面自己定义的用户变量
所以你应该可以看到下面这个语句了
1 | |
接下来就是执行了
1 | |
所以总的执行结果是
1 | |
最后就是完整的注入流程了
1 | |
使用concat可以绕过很多限制
修改原查询法
这个属于堆叠注入的另外一种方法了
这样的操作就有很多了,因为可以修改表的名字和列的名字,思路一下子就打开了
现在就开始讲解修改原查询法吧
先看到基础的语法
1 | |
在这里考考大家,下面的sql语句干啥了
1 | |
答案如下
修改users的表名为test
然后修改emails表名为users
然后修改users表的email_id列的名字为username
肯定不止这些操作
添加新列
1 | |
读取系统文件
读取系统文件是需要修改一些配置的
mysql的环境配置就是在mysql.ini文件里面写入
1 | |
下面是sql堆叠注入的一般逻辑
1 | |
但是这个有一个限制就是,服务端的代码里面有select才有回显才行
简单讲就是需要循环处理mysqli_multi_query($con1, $sql)的查询结果
合在一起的注入语句如下
1 | |
为了有一个好的效果于是我们自己可以写一写代码
1 | |
对于这个自己写的代码注入语句如下
1 | |

但是如果不能回显第二次sql查询的结果的话,我们就得使用dnslog外带了
但是很遗憾的是dnslog外带是只能在win系统下面才能使用的
因为其核心是利用 LOAD_FILE() 访问 UNC 路径(\xxx\yyy),这是 Windows 独有的文件共享路径格式,linux是不支持这种格式的
于是我们还是使用上面那个服务端的代码
于是我们可以这样注入
1 | |
效果如下

对于这个是不是也可以出一个题目呢(但是对于这个题目的制作难点是需要win环境)
锁表拒绝服务攻击
堆叠注入的锁表攻击,就是利用分号追加 LOCK TABLES 命令,阻塞所有其他数据库连接对核心表的访问,从而快速瘫痪整个网站
payload如下
1 | |
至此堆叠注入就告一段落了,接下来是最后一个常见的注入方式排序注入
排序注入
首先先看看后端的执行逻辑
1 | |
可以看到这里使用的sql语句是SELECT * FROM users ORDER BY $id
排序注入是只能使用除了联合注入之外的其他注入方式
报错,时间,布尔,堆叠
为什么不能使用联合注入
ORDER BY 后面只能跟列名、列位置、表达式,不能跟 UNION SELECT
注入点测试
测试的sql语句如下
1 | |
注意一下最后的延时测试,测试结果是如果这个表有13个数据则延时的时间是13秒
因为
ORDER BY 需要知道每一行的排序依据值,所以对每一行都要计算 (sleep(1)) 的值,每一行都等 1 秒
1 | |
报错注入
因为报错注入手法有很多于是在这里只讲解几个常见的
extractvalue版本
1 | |
updatexml版本
1 | |
exp版本
1 | |
布尔注入
1 | |
解释这个payload
这payload使用了rand随机函数
使用的种子是ascii(substr(database(),1,1))>xxx
通过上面的实验可以知道如果返回的是正确的话则使用的是rand(1)进行排序
如果返回的是错误的话则使用的是rand(0)排序
这两个排序的结果是不一样的所以可以进行布尔盲注
来一个小李子
1 | |
这个就可以知道第一个字符的ascii码是101
时间盲注
在前面我们讲注入点测试的时候有一个问题就是会有多少行数据就执行多少个sleep这样是很影响效率的
下面看我的注入语句
1 | |
思路就是使用一个select 1作为一个派生表
可以看到这个if语句是放在select里面了,然后有一个临时表,这个表的内容只有一个内容
这里是不是感觉limit 1是没有必要的,其实很有必要因为
1 | |
再来一个小例子
1 | |
也是可以执行的
一句话木马注入
对目标网站写入一句话需要一下几个条件
root权限以及网站的绝对路径,mysql配置了可以写入文件的配置(有可能不能在任意位置写入文件,只能在特定的文件里面写入文件)
mysql配置文件my.ini里面写入secure-file-priv=””即可,在引号里面写入目录路径就可以控制可入文件的路径限制在哪里
联合查询一句话注入
1 | |
OUTFILE = 带格式的文本导出,多行友好,但会加“多余字符”。
DUMPFILE = 纯净二进制/文本导出,只限一行,完全原样。
所以是最好是使用dumpfile
看看效果
1 | |
可以成功执行
非联合一句话注入
语句如下
1 | |
FIELDS TERMINATED BY 是 SELECT … INTO OUTFILE 语句中的一个格式化选项,用来指定列与列之间的分隔符
有一个值得注意的是这个里面的outfile是不能换为dumpfile的,因为fields terminated by是outflie的专属语法
突破secure-file-priv=””限制一句话写入
但是上面的情况是mysql.ini文件里面是没有secure-file-priv=””限制的
1 | |
在mysql 5.6.34版本以后 secure_file_priv 的值默认为NULL。并且无法用SQL语句对其进行修改。
这种情况是在没有权限写入文件的且有root权限的时候操作的
方法1 日志写入shell
这个主要的原理就是通过修改日志的默认路径,然后通过查询语句,在查询语句里面写入shell,让日志记录shell
主要的操作如下
1 | |
于是我们开始一个实验
下面见具体的操作
1 | |
其实很简单的
当然想执行这个不是随随便便执行的,是有一些环境要求的
查看SUPER 或 SYSTEM_VARIABLES_ADMIN 权限
1 | |
查看FILE权限
1 | |
可以看到root用户是所有权限都有的
于是新建一个用户看看效果
1 | |
执行上面的命令,可以发现没有任何权限
1 | |
然后我们给test_user加上特权权限
1 | |
这个时候可以看到test_user有了这些权限
方法2 慢查询写入shell
为什么要用慢查询写呢?上边说过开启日志监测后文件会很大,网站访问量大的话我们写的shell会出错
完整的操作如下
1 | |
输入这个命令可以知道数据库的查询到多长时间会把这个查询语句写入慢查询日志中,默认是10秒
1 | |
通常情况下执行sql语句时的执行时间一般不会超过10s,所以说这个日志文件应该是比较小的,而且默认也是禁用状态,不会引起管理员的察觉
拿到shell后上传一个新的shell,删掉原来shell,新shell做隐藏,这样shell可能还能活的时间长些
下面我们开始做一个实验
1 | |
执行set global slow_query_log=1;之后slow_query_log慢查询打开了,然后我们使用超过十秒的sql查询,就可以写入文件了
看看shell.php文件里面的内容
1 | |
成功
现在开始恢复
1 | |
慢查询写入所需要的权限跟日志写入shell所需要的权限是一样的,在这里就不讲了
一句话注入就这样结束了
绕过
前面已经把sql注入的所有知识讲完了,但是对于做题来说还是有一些艰难的,还有最后一个重要的内容绕过部分没有讲
sql注入里面的绕过还是非常复杂的,各种手法千奇百怪
开始吧
字符串聚合函数
这种类型的函数有三种
concat和concat_ws和group_concat和json_arrayagg
json_arrayagg这个只适合MySQL 5.7.22版本
如果其中一个或者两个被过滤了可以使用别的姿势
前三个的使用方法如下
1 | |
0x3a是:冒号
还有一个是比较逼格的输出方式,但是这个方法只适合使用在group_concat函数
1 | |

通过这个图片我们可以知道,separator是group_concat()函数的一个参数
然后0x3c62723e表示的意思是html的标签
所以使用这个的输出效果是

可以说是非常的优雅
1 | |
现在开始着重的讲解json_arrayagg的使用
这个函数需要配合别的函数使用才有完整的效果
1 | |
这两个是差不多的,一个是使用json_array一个是使用json_object
一个是数组一个是对象而已,没有什么区别
尝试一下完整的语句
1 | |
另外一个json_object一样的,但是但是有一个问题json_object需要的参数是偶数不能是基数,因为object是需要的是键值对
1 | |
有没有解决方法?肯定是有的
如下
1 | |
这些就是字符串聚合函数的使用,主要是看实际的情况而使用的
where绕过
对于这个绕过肯定是有方法的(不然我也不会在这里写了)
有两个
heving
limit配合offset
下面开始仔细讲
HAVING 是 SQL 中用于对分组后的结果进行过滤的子句
HAVING 的设计初衷是和 GROUP BY 搭配,过滤聚合函数的结果
类似于这种效果
1 | |
1 | |
limit配合offset
1 | |
LIMIT 1 最多返回 1 行
OFFSET 1 从第 0 行之后开始取
实验
1 | |
这样就可以替换where了
计算长度函数绕过
length和octet_length()和char_length()和character_length()和BIT_LENGTH()
解释一下这些函数
1 | |
看看这些函数的使用方法
1 | |
ascii码计算
在时间盲注以及布尔盲注的时候我们经常使用ascii码计算字符
对于这个还是有很多的方法的
ascii()
ord()
hex()
unhex()
conv()
来讲解一下
1 | |
接下来我们试验一下
1 | |
字符切割函数
这类的函数也有很多
substr()
substring()
mid()
left(),right(),insert(),reverse()
substring_index()
下面开始着重讲这些函数
substr() substring() mid()
这三个就不多讲了
1 | |
所以在这里就不提供盲注脚本了
接下来是left(),right(),insert(),reverse()
看看基本操作
left() right() insert() reverse()这四者的配合可以打出奇效
left()与right()的配合(只展示爆出数据库名的代码,后面的是一个道理)
1 | |
服务端的执行语句类似为
1 | |
然后再看到right()与insert()函数的配合
1 | |
服务端执行的sql语句
1 | |
只是这个有一个注意的就是循环的边界问题
1 | |
很简单的,逻辑是一样的
现在看另外一个配合right()与left()与reverse()
其实REVERSE配合起来像是脱裤子放屁
1 | |
and和or限制
话不多说直接说
先讲and
&&
这个与and是一模一样的
1 | |
xor
这个是一个运算的符号,相同为假,不同为真,为了相比较左右的值,就必须得计算右边的值
1 | |
下面这个就不能直接说是可以替换and,只能适合在特殊的环境下面
case和if语句
1 | |
原理是啥
原理是因为使用我们将前面置空,导致前面始终是false,之后后面的or 的真假与否决定权在我们的判断与句话里面
所以这个特殊的环境是,需要我们能控制前面的内容是假
然后就是绕过or限制
or——>||
额好像就这个手法
睡觉函数
在时间盲注的时候经常会使用sleep,可以说sleep函数是重中之重,如果sleep不能使用怎么办
如下
benchmarck()
GET_LOCK()
RLIKE REGEXP
笛卡尔积
先来到benchmarck()函数
1 | |
重要的事说三遍!!!
这个方法跟机器的性能有很大的关系,需要根据实际情况调整数字
这个方法跟机器的性能有很大的关系,需要根据实际情况调整数字
这个方法跟机器的性能有很大的关系,需要根据实际情况调整数字
接下来是GET_LOCK()函数
这个函数的使用是比较的鸡肋的,因为需要两个会话
实验一下,首先我们在第一个会话里面输入
1 | |
这样做的目的是为了先拿到一个名字是a的锁,这个时候a在会话1里面
然后在第二个会话里面输入
1 | |
为什么会等十秒,是因为,a在会话1里面会话2是拿不到的,所以这里我们可以设置一个类似于超时时间的东西,会话2在拿的期间等10秒就不等了
接下来是RLIKE REGEXP正则匹配
直接看看用法
1 | |
RPAD(str, len, padstr) 是一个字符串填充函数,意思是:如果字符串 str 不够 len 长,就在它的右边不断用 padstr 填充,直到长度达到 len,所以RPAD(‘a’, 1500000, ‘a’) 生成了一个由 150万个字母 ‘a’ 组成的、极长的字符串
REPEAT(‘(a.)+’, 30):将 (a.)+ 这个模式重复 30 次,… ‘b’:最后在末尾加上一个字母 b,最终生成的正则表达式(简化为重复 2 次的版本,你这里是 30 次):(a.)+(a.)+b
核心
1 | |
对于这个可以多讲一点,这里有一个漏洞就是使用dos攻击
1 | |
但是没有复现成功,mysql有一个复杂度保护机制
接下来就是笛卡尔积了
先看看效果
1 | |
笛卡尔积在这里本质上就是“统计字符(行数)”
information_schema.columns 这张系统表记录了整个MySQL实例中所有数据库、所有表的每一个字段信息。
FROM A, B, C 这种不加任何连接条件的多表查询,会生成 N × N × N 行的中间结果集。
这个过程就是纯粹的“行数乘法”,MySQL 需要遍历这个巨大的临时结果集来统计 COUNT(*)
这个也是跟机器的性能有关
1 | |
判断语句绕过
判断语句就是if那些东西,是一个十分重要的东西,可以替换的语句有下面这些
if(xxx,1,0)
case when xxx then 1 else 0 end
nullif(0,xxx)
elf(xxx,1,0)
下面一个一个介绍使用的方法
if
1 | |
case
1 | |
nullif
1 | |
nullif的意思是如果参数一的值和参数二的值是一样的话,则返回null
对于这个执行逻辑可能会有一些绕
SELECT 1 AND NULLIF(0, LENGTH(DATABASE()) != 8) AND SLEEP(3);
我们的database()长度就是8,LENGTH(DATABASE()) != 8为0,于是0=0,NULLIF(0, 0)等于null,其中 NULL 不是 FALSE,还有“翻身”的可能,所以执行sleep
1 | |
这里其实涉及mysql 的三值逻辑和短路由
接下来是ELT
ELT(index, str1, str2, …)第一个参数是索引,如果是1则返回str1,2则返回str2
语句如下
1 | |
判断语句就到这里了
注释过滤
服务端的代码类似于
1 | |
方法有如下
and ‘1’=’1
‘
or ‘1’=’1
空白符与制表符
空字节
后面这两个只有在理论上存在
开始详细的讲解
and ‘1’=’1
直接在后面加上and ‘1’=’1
这种方法在联合注入的时候会出现一个问题
我们在探测输出的列数的时候使用order by函数会遇到一个问题,如下
1 | |
于是可以这样构造代码
1 | |
之后就是正常注入了
1 | |
‘
直接在后面加
1 | |
一个小点有这么大的威力
or ‘1’=’1
and 可以or当然也是不影响的
1 | |
空白符与制表符
如果后端语言只考虑过滤– 而不是过滤-的话可以使用空表符进行绕过
空表符
1 | |
%0A– (换行符后跟注释):…%0A– …
空字节
1 | |
这个空字节使用到了;所以需要满足堆叠注入的条件
空格过滤
服务端的代码可能是如下
1 | |
这些方法都是可以绕过的
%09 TAB键(水平)
%0a 新建一行
%0c 新的一页
%0d 回车功能
%0b TAB键(垂直)
%a0 空格
()绕过空格
/**/内联注释符号
反引号``
浮点数
开始一个一个实验
%0b
1 | |
%09
1 | |
%0a
1 | |
%0c
1 | |
%0d
1 | |
%0a
1 | |
这些实测都是可以的,跟环境有关
()括号使用
报错注入如下
1 | |
想想能不能使用()的联合注入
这里我只注意绕过空格
1 | |
但是但是眼睛尖的同学可以知道其中的问题,– -也有空格
于是只能这样使用了
1 | |
为什么不使用or(‘1’)=’1
因为这得看服务端的代码,后面有limit是不能使用or去闭合单引号的
内联注释符号
原理解释
mysql有两种特殊注释方法
一个是普通内联注释/**/
一个是条件编译注释/*!版本号 内容*/
如果当前MySQL版本 大于等于 指定的版本号,注释里的内容就会被执行。
如果版本低于指定值,就当作普通注释忽略。
/*!000001*/,几乎所有MySQL版本都大于0.0.1,所以里面的代码必定执行。文中用它替换空格
/*!50100*/ 表示只在MySQL 5.01.00及以上版本才执行里面的内容
差不多
1 | |
实验
1 | |
在SQL中,反引号(``)用于标识数据库、表或列的名称。在反引号的两端可以没有多余的空格:
1 | |
使用浮点数:
1 | |
引号绕过
使用十六进制编码绕过:
1 | |
等号绕过
like
not
strcmp
REGEXP
in
1 | |
1 | |
1 | |
1 | |
1 | |
逗号绕过
情景在我们使用时间或者布尔盲注的时候会使用切割字符创的函数
但是这个函数会使用逗号,类似于
1 | |
方法
from结合for和
join连接和
模糊查询和
offset
from结合for
1 | |
join
使用join连接操作绕过逗号:
1 | |
模糊查询
盲注时逐个判断查到字符ascii码时,可以直接使用模糊查询来绕过字符串截取函数的逗号:
1 | |
offset
对于limit可以使用offset来绕过逗号:
1 | |
数字绕过
使用个浮点数即可
1 | |
比较符号><绕过
greatest
strcmp
least
in
BETWEEN AND
like
regexp
GREATEST(…, 64) 取两个数中最大值
实验
1 | |
过程
如果 ASCII 码 > 64 → GREATEST 返回 ASCII 码 → ≠ 64 → 条件为假
如果 ASCII 码 ≤ 64 → GREATEST 返回 64 → = 64 → 条件为真
greateste去最大值,然后使用等于号比较
strcmp函数
这个函数需要着重讲一下
怎么做到绕过比较符号的呢
strcmp(n,m)
如果n大于m则返回1,如果n小于m则返回-1
1 | |
o_o原来如此,那么是不是可以这样了呢
select strcmp(length(database()),1000);
1 | |
什么!返回的是居然是1,怎么会不是-1
因为strcmp比较的是字符不是数字
mysql的字符比较是比较第一个字符的
所以对于select strcmp(length(database()),1000);比较的是8跟1比较所以8>1返回1
咦?按照这个思路是不是!!!! qwq
思路就这么来了,这个函数完全可以用来盲注
1 | |
果然跟我想的是一样的,如果是一样的返回0,如果是不一样的返回1,盲注即可
least
这个函数与greateste相反
取两者的最小值
1 | |
in
判断该字符里面有没有该字符。。。。额好绕没事看例子
1 | |
高级点的玩法就是使用多字符查询
1 | |
between and
在什么什么之间
先看一个简单的
1 | |
下面直接上实验
1 | |
like
这个可以替换等于号
LIKE 用于模糊匹配,但如果不使用通配符 % 和 _,它的行为就完全等价于等号
1 | |
regexp
REGEXP 使用正则表达式匹配,功能更强大,可以一次性判断字符范围
| 原写法 | 替换写法 | 说明 |
|---|---|---|
= 'a' |
REGEXP '^a$' |
精确匹配字符’a’ |
= 'admin' |
REGEXP '^admin$' |
精确匹配字符串’admin’ |
常用正则符号:
| 正则 | 含义 | 示例 |
|---|---|---|
^ |
字符串开头 | '^a' 以a开头 |
$ |
字符串结尾 | 'a$' 以a结尾 |
[abc] |
字符集合 | '^[abc]' 以a、b或c开头 |
[a-z] |
字符范围 | '^[a-z]' 以小写字母开头 |
. |
任意单个字符 | '^a.' a后面跟任意字符 |
1 | |
字符替换
字符替换
1 | |
str:原始字符串
from_str:要被替换的部分
to_str:替换后的新内容
反斜杠绕过
%
遇到要使用\的时候
将\变为URL编码%54 代替 \x54 一样能够达到效果
为了能够正常传递百分号 %,一般需要进行双重 URL 编码,即将 %54 再次编码为 %25%35%34。
information_schema的替代
mysql版本5.7.26-0ubuntu0.18.04.1-log
直接获取所有库名与表名
1 | |
获取库名、表名及列名
1 | |
获取所有库名 (仅库名)
1 | |
获取部分列名
1 | |
读取历史查询记录 (窃取其他查询的敏感数据)
1 | |
获取库中表的数量
1 | |
其他特殊表
1 | |
综合绕过思路
大小写
1 | |
str_replace这个函数是直接限制的所以我们直接大小写绕过即可
双写
1 | |
str_ireplace这个函数是不看大小写的,但是这个方法只是使用了单词过滤,而且只是替换为空而已
所以我们使用双写绕过即可
ascii码替换
1 | |
使用ascll码绕过
例如:SELEC\x54 来替代 SELECT
T的十六进制是\x54有一些语言在处理这个的时候会把16进制的东西自动替换为实际的值
base64绕过
版本限制5.7.x以上
TO_BASE64
FROM_BASE64
基础
1 | |
高级一点
1 | |
这个是输入,输出呢
1 | |
这个很秀
预处理拼接
需要时堆叠注入
PREPARE FROM :将查询语句 query 取名为 name;
EXECUTE [ USING ]:执行名为 name 的语句,可以带参数(不过用处不大)
1 | |
完全过滤
一个是尝试注入
一个是尝试小语种
1 | |
可以通过传入数组绕过。也就是构造 URL 为:
1 | |
preg_match 只能对字符串进行正则匹配。如果传入的是数组,它会直接返回 false(即“未匹配到”),且不会报错。 这就绕过了原本的过滤逻辑
小语种
在 MySQL 里,如果设置了 utf8_general_ci 或者 utf8_unicode_ci,德语变音字母 Ä、Ö、Ü、ẞ(注意这不是希腊字母 β,是德语字母 ss)等价于普通字母 A、O、U 和 S(对于 unicode_ci,等价于双写字母 SS)。
所以可以尝试用 UNIÖN 代替 UNION,这样可以绕开 PHP 的检测逻辑,同时也能在 MySQL 上正常运行
1 | |
字符拼接
1 | |
特殊注释
原理解释
mysql有两种特殊注释方法
一个是普通内联注释/**/
一个是条件编译注释/*!版本号 内容*/
如果当前MySQL版本 大于等于 指定的版本号,注释里的内容就会被执行。
如果版本低于指定值,就当作普通注释忽略。
/*!000001*/,几乎所有MySQL版本都大于0.0.1,所以里面的代码必定执行。文中用它替换空格
/*!50100*/ 表示只在MySQL 5.01.00及以上版本才执行里面的内容
有了这个思路是不是就可以绕过一下对于函数的限制了呢
当然
1 | |

配合换行符号
还有一种的话就是内联注释的利用方法就是中间加注释符再加换行,也就是/*!%23%0a*/这种形式
1 | |

将%23换成–+再构造试一试
这样也是可以的
1 | |
MySQL从/*开始,会进入注释解析模式,它寻找的是能闭合自己的第一个 */
所以我们可以在这个注释符里面随便混淆

后面的payload
1 | |
md5构造
如果后端的处理方式是
1 | |
我们就可以利用起来
MD5 函数的两种输出
平时用 md5(字符串) 得到的是像 e10adc3949ba59abbe56e057f20f883e 这样的十六进制字符串,都是字母和数字,很 “规矩”。
但如果用 md5(字符串, true),得到的是 “原始字节流”—— 这东西可能包含各种奇怪字符,比如单引号、双引号,甚至 OR 这样的数据库关键字。
当后端于洋在处理我们输入的md5这个函数的时候,这个时候就可以构造一些不可思议的危险函数
我要使用的就是利用自带的true构造一些关键的字符
上面的操作是在php层的注入的,有没有可以直接在mysql层的注入呢? 有!
如下
1 | |
看这样是不是就可以获得了特殊字符了呢?这样是不是就可以拓宽视野了呢?感觉可以出一个题目
本人的研究如下(实力有有限,只能到这了)
见下:
但是好像只能是堆叠注入
1 | |
我们只需要替换单引号里面的值就好了
大概框架就是
1 | |
为了方便,于是下面这个脚本诞生了
1 | |
运行的效果如下
1 | |
可以看到使用这个方法需要堆叠注入环境,这个方法只使用了
1 | |
这些关键字
扩展一手
同理sha1是不是也是可以做到的呢
当然
1 | |
感觉此方法肯定是没有经常使用的,多一种思路^_^
最后一个环节,fuzz字典
1 | |
mysql注入手法总结就结束啦,在这个快节奏的社会里面哪有怀念的时间呢,拜^_^
持续学习
2026.5.25判断注入类型
今天又扩展了一个视野,判断注入类型
在平时测试漏洞的时候我们只会测试单引号或者是双引号,今天发现了一个新的思路
注入的时候还可以测试括号或者是加减符号
这里看到开发者可能会开发的代码
1 | |
我们可以这样测试
使用5-1和5来测试
结果一样 → 可能没问题
结果不同 → 数值型 SQL 注入
还有还有测试引号的时候不只只是测试单引号还可以测试
1 | |
这里介绍后面两个
1 | |
[SUCTF 2019]EasySQL
对于这到题目我真的没招了

前面获得的已知的信息是:
过滤了from、union、extractvalue、PREPARE等许多关键字,回显提示:NONONO!
过滤的效果非常严格
推理
对于这个题目最重要的就是要搞清楚后端代码的逻辑是什么
先慢慢来
1 | |
发现页面回显的结果是不一样的


所以说放一百个心肯定是有sql注入的,而且是数字型注入
发现不能使用盲注,因为sleep和from和information_schema等关键字被严格过滤了
使用堆叠注入易得database(),table_name
使用
1 | |

后续经过堆叠注入发现Flag这个表名也被过滤了,这该如何是好
首先肯定猜测的后端处理逻辑类似于
1 | |
猜测查询正确则回显1错误回显0
但是实际测试下来发现并不是这么一回事
如果使用的是上面这个后端处理逻辑,输入0没有回显,输入1-1就有回显Array ( [0] => 0 )
于是到这里就发现了一个问题,同样是0(0=1-1),但是回显的东西不一样
猜测可能是后端的处理方法在作祟,尝试输入0-0之后发现回显是Array ( [0] => 0 ),所以这里就可以猜测这个只是后端处理时候的问题而已,不在这里做过多的停留,继续寻找线索
题目的提示信息是
1 | |
有理由猜测跟布尔有关,这里现猜测后端处理的逻辑是查询成功回显1查询失败回显0,或者是查询有数据回显1没数据回显0
还是使用上面这个where的后端处理逻辑来进行注入,就又发现了一个问题了,输入1和输入2或者3,4,5….的回显是一样的都是Array ( [0] => 1 ),按照之前的猜测
后端处理的逻辑是查询成功回显1查询失败回显0,或者是查询有数据回显1没数据回显0
很明显就不对了
如果后端的处理方式是查询成功回显1查询失败回显0
那么我输入1-1查询回显的是0说明查询失败了,我输入1回显1说明查询成功了
那么我再输入-1回显的居然是1说明查询成功了

好家伙,难道是-1有数据,0没有数据吗?
很明显不对,所以到了这里我们就可以推翻前面猜测的后端处理逻辑了,后端的处理逻辑不是select * from Flag where id=$_POST[‘id’];而是一个跟布尔相关的逻辑(这个结论是根据提示来了,后面会有证实)
当我输入一个巨大无比的数字的时候回显的还是Array ( [0] => 1 )

说明什么,说明就两种回显一个是0一个是1
绝对是布尔相关的注入了,而且布尔的逻辑是后端写死的
那么怎么获得的Flag呢,后端又是布尔相关的?
现在就是直接猜测后端的sql语句,猜到了就是胜利
首先后端肯定要有from Flag 不然没有办法输出Flag
其次后端肯定要有or之类的
所以后端处理逻辑类似于
1 | |
我们输入的内容是经过布尔表达式计算之后的
回想到之前我们输入1-1回显0,难道说后端处理过后,跟布尔一样只要是0就回显0,不是0全部回显1
测试输入(本来是使用or测试的,发现or被过滤了)
1 | |
这个时候就可以大胆猜测一下后端的处理逻辑是
1 | |
方式一
遇到这样的
1 | |
怎么注入?
肯定是要利用一下逗号隔开||对我们输入的payload的影响,然后有要输出flag表里面的所有内容
所以就是
1 | |
解释:
查询两列一列是* 一列是1|| 列名
所以这样操作就不会影响到我们的select * from flag
在本地测试一样
1 | |

方式二
咱们都能堆叠注入了,肯定有其他的方式
如下
1 | |
set sql_mode=pipes_as_concat
这是核心。 把当前会话的sql_mode改成 PIPES_AS_CONCAT,在这个模式下,|| 的含义从逻辑或变成了字符串拼接
select 1 || flag from Flag就变成了select concat(1, flag) from Flag;
1 | |

搭建本地环境
初始化sql
1 | |
php代码
1 | |
结束