Chybeta

BackdoorCTF 2017-Extends Me-writeup

哈希长度扩展攻击 以及一种奇葩解法

已将该题收录至Code-Audit-Challenges-python:1

Task

1
2
Extends Me
https://extend-me-please.herokuapp.com/login

页面打开如下:

Solution

题目提供了源码,如下:
server.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
from flask import *
from hash import SLHA1
app = Flask(__name__)
key = file('SECRET').read().strip()
@app.route('/')
def root():
return redirect(url_for('login'))
@app.route('/login',methods = ['GET', 'POST'])
def login():
if request.method == 'POST':
if not request.form.get('username'):
return render_template('login.html')
else:
username = str(request.form.get('username'))
if request.cookies.get('data') and request.cookies.get('user'):
data = str(request.cookies.get('data')).decode('base64').strip()
user = str(request.cookies.get('user')).decode('base64').strip()
temp = '|'.join([key,username,user])
if data != SLHA1(temp).digest():
temp = SLHA1(temp).digest().encode('base64').strip().replace('\n','')
resp = make_response(render_template('welcome_new.html',name = username))
resp.set_cookie('user','user'.encode('base64').strip())
resp.set_cookie('data',temp)
return resp
else:
if 'admin' in user: # too lazy to check properly :p
return "Here you go : CTF{XXXXXXXXXXXXXXXXXXXXXXXXX}"
else:
return render_template('welcome_back.html',name = username)
else:
resp = make_response(render_template('welcome_new.html',name = username))
temp = '|'.join([key,username,'user'])
resp.set_cookie('data',SLHA1(temp).digest().encode('base64').strip().replace('\n',''))
resp.set_cookie('user','user'.encode('base64').strip())
return resp
else:
return render_template('login.html')
@app.route('/logout')
def logout():
resp = make_response(render_template('login.html'))
resp.set_cookie('data','',expires=0)
resp.set_cookie('user','',expires=0)
return (resp)
if __name__=="__main__":
app.run()

还有hash.py,鉴于太长,这里就不贴出来了,可以见这:Code-Audit-Challenges-python:1

看一下server.py的流程,分为三个路由:

  1. 根路径,会重定向到login
  2. login页面,登陆后能拿到flag。
  3. logout页面

流程分析

接下来细看login(),这里会啰嗦点尽量把整个流程解释清楚(其实都是废话)。

当我们在表单中输入用户名并提交后,server.py通过str(request.form.get('username'))获取用户名并保存到变量username中。

接下来,如果在cookie中能获取到data和user字段,则接收并进行base64解码后去掉两边的空格之后存放到对应的变量data和user中:

1
2
data = str(request.cookies.get('data')).decode('base64').strip()
user = str(request.cookies.get('user')).decode('base64').strip()

其中strip()的作用是去掉字符串头尾指定的字符,默认为空格。

接下来通过join操作,得到一个变量temp,其组成为key|username|user,其中username和user即前面提到的。而这个key是通过下面的语句定义的:

1
key = file('SECRET').read().strip()

也就是说,变量key是未知的。

继续,截取server.py的代码如下:

1
2
3
4
5
6
7
8
9
10
11
if data != SLHA1(temp).digest():
temp = SLHA1(temp).digest().encode('base64').strip().replace('\n','')
resp = make_response(render_template('welcome_new.html',name = username))
resp.set_cookie('user','user'.encode('base64').strip())
resp.set_cookie('data',temp)
return resp
else:
if 'admin' in user: # too lazy to check properly :p
return "Here you go : CTF{XXXXXXXXXXXXXXXXXXXXXXXXX}"
else:
return render_template('welcome_back.html',name = username)

会进行一个data与SLHA1(temp)的比较,其中SLHA1(temp)的具体实现在hash.py)中。如果比较相等且字符串admin在变量user中,则得到flag。注意这里是用in操作符,所以如下的情况是真的:

1
2
>>> 'admin' in 'userxxxxxxxxxadmin'
True

如果比较不等,则会进行一个set_cookie操作,即我们会接受到以下的cookie:

1
2
user = base64encode(user)
data = temp 即 data = SLHA1(key|username|user).digest().encode('base64').strip().replace('\n','')

如果在cookie中不能能获取到data和user字段,它会执行下面的代码:

1
2
3
4
5
resp = make_response(render_template('welcome_new.html',name = username))
temp = '|'.join([key,username,'user'])
resp.set_cookie('data',SLHA1(temp).digest().encode('base64').strip().replace('\n',''))
resp.set_cookie('user','user'.encode('base64').strip())
return resp

也是有一个set_cookie操作,我们会得到以下的cookie:

1
2
data = SLHA1(key|username|'user').digest().encode('base64').strip().replace('\n','')
user = base64encode('user')

这里括号中的'user'是指定的字符串,跟前面我们通过cookie传入得到的user变量是不一样的。

接下来看看SLHA1函数,这个是模仿了SHA1加密,对一些参数等做了修改,但本质上是基于Merkle–Damgård construction。所以我可以尝试一下哈希长度扩展攻击。

法一:哈希长度扩展攻击

基本的思路如下:

  1. 先获取到cookie,其中data=SLHA1(key|xxx),user=base64encode(‘user’)
  2. 基于第一步,通过哈希长度扩展攻击,得到SLHA1(key|xxx。。。admin)。
  3. 构造data,user字段,发送cookie,使之满足data == SLHA1(temp).digest()

第一步获取cookie:

几个已知的参数如下:

1
2
3
4
5
username = "chybeta"
data="GwgWlwVYqelmztYx1n//EfyTIU6cH8ab"
user="dXNlcg=="
base64decode("dXNlcg==") = "user"

data由服务器端经过SLHA1("xxxx|chybeta|user")加密得到,括号里的都是字符串不是变量,xxxx表示key。

第二步,进行哈希长度扩展攻击。先看看目标,注意以下代码:

1
2
3
4
5
if request.cookies.get('data') and request.cookies.get('user'):
data = str(request.cookies.get('data')).decode('base64').strip()
user = str(request.cookies.get('user')).decode('base64').strip()
temp = '|'.join([key,username,user])
if data != SLHA1(temp).digest():

要构造出SLHA1("xxxx|chybeta|user"+padding+"admin"),并将其设置为data。同时设置cookie中的user为base64encode(“user”+padding+”admin”)。即:

1
2
data = SLHA1("xxxx|chybeta|user"+padding+"admin")
user = base64encode("user"+padding+"admin")

这样服务器端的流程约莫如下:

  1. 保持post传入的username仍为”chybeta”
  2. data = str(request.cookies.get(‘data’)).decode(‘base64’).strip() 得到 data = SLHA1(“xxxx|chybeta|user”+padding+”admin”)
  3. user = str(request.cookies.get(‘user’)).decode(‘base64’).strip() 得到 user = “user”+padding+”admin”
  4. temp = ‘|’.join([key,username,user]) 得到 temp = “xxxx|chybeta|user”+padding+”admin”
  5. 判断 SLHA1(“xxxx|chybeta|user”+padding+”admin”) == SLHA1(“xxxx|chybeta|user”+padding+”admin”)

因为出题者自己写了SLHA1,所以现成的工具是不行的。对照SHA1加密算法,我们写一个对SLHA1的长度扩展攻击算法。

上面这图是SHA1加密算法的流程:首先有原始register值,然后将hash的字符串分组等初始化操作后进行复杂的数学运算,同时会生成新的register的值,供下一个chunk进行加密使用。

在hash.py中,SLHA1算法的原始register值有6个:a,b,c,d,e,f。它们在每个chunk加密后会被更新,以参与下一个chunk的加密。可以通过self._h进行赋值来直接指定SLHA1算法的register值。

对一个字符串进行SLHA1算法加密,可以通过调用update()来进行.
对加密字符串的初始化操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def _produce_digest(self):
message = self._unprocessed
message_byte_length = self._message_byte_length + len(message)
message += b'\xfd'
message += b'\xab' * ((56 - (message_byte_length + 1) % 64) % 64)
message_bit_length = message_byte_length * 8
message += struct.pack(b'>Q', message_bit_length)
h = _process_chunk(message[:64], *self._h)
if len(message) == 64:
return h
return _process_chunk(message[64:], *h)

message是要进行加密的字符串。它会先加上一个字节\xfd,之后再加上一堆的\xab,使得chunk的长度能满足整除64后余数为56。之后添上8个字节的长度描述符。接下去从h = _process_chunk...开始是对最后一个chunk的加密处理。

由于要进行padding,我们需要知道原本xxxx|chybeta的长度,但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
from hash import SLHA1
import struct
import requests
def extend(digest, length, ext):
pad = '\xfd'
pad += '\xab' * ((56 - (length + 1) % 64) % 64)
pad += struct.pack('>Q', length * 8)
slha = SLHA1()
slha._h = [struct.unpack('>I', digest[i*4:i*4+4])[0] for i in range(6)]
slha._message_byte_length = length + len(pad)
slha.update(ext)
return (pad + ext, slha.digest())
post = {
'username': 'chybeta'
}
cookies = {
'data': 'GwgWlwVYqelmztYx1n//EfyTIU6cH8ab',
'user': 'dXNlcg=='
}
orig_digest = cookies['data'].decode('base64')
orig_user = cookies['user'].decode('base64')
min_len = len('|'.join(['?', post['username'], orig_user]))
for length in range(min_len, min_len+64):
print('[+] Trying length: {}'.format(length))
ext, new_digest = extend(orig_digest, length, 'admin')
cookies['data'] = new_digest.encode('base64').strip().replace('\n', '')
cookies['user'] = (orig_user + ext).encode('base64').strip().replace('\n', '')
r = requests.post('https://extend-me-please.herokuapp.com/login', data=post, cookies=cookies)
if 'CTF{' in r.text:
print(r.text)
break

PS: 其实今天早上(17/9/29)自己也写了一个脚本,但有些问题,准备过会再调试一下。然后下课后,TT && GG:

网站居然下线了woc。。。。。

法二:剑走偏锋:)

这个算是非预期解。毕竟这题名字叫Extend me,明显就是考哈希长度扩展攻击。不过这个非预期解法也蛮好玩的。

先通过post参数,设置username为 “chybeta|admin”。接着服务器进行加密 SLHA1(“xxxx|chybeta|admin|user”),这里的xxxx是指key,后面的user是服务器端默认的,中间的“chybeta|admin”即为变量username,这个加密过程对应:

1
2
3
4
5
6
7
8
9
10
11
12
username = str(request.form.get('username')) # username = "chybeta|admin"
if request.cookies.get('data') and request.cookies.get('user'):
。。。
else:
。。。
# 假设key的值为 xxxx
temp = '|'.join([key,username,'user']) # temp = xxxx|chybeta|admin|user
resp.set_cookie('data',SLHA1(temp).digest().encode('base64').strip().replace('\n',''))
# data = SLHA1("xxxx|chybeta|admin|user").encode('base64')...
resp.set_cookie('user','user'.encode('base64').strip())
# user = 'user'.encode('base64')
return resp

第二步,更改cookie中的user字段:

1
2
原本:user = "dXNlcg==" # base64decode("dXNlcg==")="user"。
现在:user = "YWRtaW58dXNlcg==" # base64decode("YWRtaW58dXNlcg==") = "admin|user"。

Cookie的data字段保持不变。post进的username改为”chybeta”
接下来仔细看一下关键验证代码:

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
username = str(request.form.get('username')) # username = "chybeta"
if request.cookies.get('data') and request.cookies.get('user'):
data = str(request.cookies.get('data')).decode('base64').strip()
# data = SLHA1("xxxx|chybeta|admin|user")
user = str(request.cookies.get('user')).decode('base64').strip()
# user = "admin|user"
# 假设 key的值为xxxx
temp = '|'.join([key,username,user])
# temp = key + "|" + username + "|" + user
# temp = "xxxx" + "|" + "chybeta" + "|" + "admin|user" = "xxxx|chybeta|admin|user"
# data = SLHA1("xxxx|chybeta|admin|user")
# SLHA1(temp) = SLHA1("xxxx|chybeta|admin|user")
if data != SLHA1(temp).digest():
temp = SLHA1(temp).digest().encode('base64').strip().replace('\n','')
resp = make_response(render_template('welcome_new.html',name = username))
resp.set_cookie('user','user'.encode('base64').strip())
resp.set_cookie('data',temp)
return resp
else:
# user = "admin|user"
if 'admin' in user: # too lazy to check properly :p
# 'admin' in "admin|user" OK!!!!
return "Here you go : CTF{XXXXXXXXXXXXXXXXXXXXXXXXX}"
else:
return render_template('welcome_back.html',name = username)

微信扫码加入知识星球【漏洞百出】
chybeta WeChat Pay

点击图片放大,扫码知识星球【漏洞百出】

本文标题:BackdoorCTF 2017-Extends Me-writeup

文章作者:chybeta

发布时间:2017年09月28日 - 12:09

最后更新:2017年09月29日 - 15:09

原始链接:http://chybeta.github.io/2017/09/28/BackdoorCTF-2017-Extends-Me-writeup/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。