当前位置: 首页 > 洞见&观察 > 软件开发

一文掌握前端加密和js逆向

作者:虾咪 | 发布时间:2025-12-25阅读数:
看点:学习的必要性在如今科技飞速发展的时代,传统的明文传输已经慢慢退出历史的舞台。更多的是对用户传入的参数进行加密。甚至有些时候,对用户的响应内容也是加密的。那么,当

学习的必要性

在如今科技飞速发展的时代,传统的明文传输已经慢慢退出历史的舞台。更多的是对用户传入的参数进行加密。甚至有些时候,对用户的响应内容也是加密的。那么,当我们测试越权、弱口令和 sql 注入的时候,就必须要对传入的参数进行加密。否则大多时都是无功而返。同时针对于前端加密还有一个天然的优势。由于内容都是经过加密的。 自然不会被 WAF 所拦截。

常见的 JS 加密

对称加密比如 AES、DES 使用的加密和解密的密钥都是同一个。所以叫做对称加密。非对称加密比如 RSA 和 ECBSA 使用非对称加密,也就是说,加密和解密使用的是两个密钥,公钥一般在前端,私钥放在后端用于解密。

JS 逆向

快速定位到加密处代码 通过字段关键字 虽然这里传入的值是经过加密处理的,但是它的 key 是明文的。因此我们可以直接进行搜索data:或data=以及data =。进行定位。当然这里我更推荐在 network 中使用 Ctrl + F 键进行搜索: 定位到相关位置只会可以发现这里使用的是 DES 加密。

  • key 为 12345678。
  • 偏移量 IV 为 12345678。
  • 模式为 CBC
  • 填充为 Pkcs7

对加密的内容进行解密:通过 JS 关键字可以全局搜索一些关键字如CryptoJS、encrypt、decrypt、key、iv等。通过路由点击查询或登录的时候,我们来到 network 查看请求,可以发现请求的接口为getUserInfo看其启动器即可定位到请求时执行的方法。定位到相应方法只会,查看对接口传入的参数。本案例传入的是{data : encrypted}接着往上追踪即可。通过事件监听这里的加密字段是通过点击 query 按钮的时候进行加密的,因此我们可以查看该元素的监听事件。重点关注一些 click 事件和 submit 事件。最后点击定位即可。

配合断点调试

实际业务系统中,可能会添加一些混淆、或代码量比较大,方法比较多。那么此时我们就需要配合断点调试。通过以上几种方法定位到大概位置,然后多下几个断点进行调试: 一步一步查看断点,此时我们的 username 还为明文。继续跟进发现这里对我们传入的用户名作为对象中的值,其属性名称为 username 值为我们传入的内容,即 user1,最后对该对象进行 DES 加密之后,作为getUserInfo接口的 body 部分中的{data : "此处为加密内容"}内容发送给后端。因此前端实际发送的内容为:

这里需要注意的是,断点不要下在函数定义上。

靶场实践

AES 固定 KEY发送给后端的内容: 由于这里传入的参数名为encryptedData因此我们在前端进行搜索: 可以发现该接口发送的内容跟我们抓包获取的发送内容一致。都是以encryptedData=开头。并且该 JS 代码是做了混淆处理的。接着我们向上追踪。定位对加密字段的处理片段,可以发现是_0x1375d7这个变量。在这里发现 iv 的变量为_0x2d9cd5,那么上一个则是 AES 的 KEY 值。最后发现sendDataAes()函数的形参为 api 的 URL 地址。上述之后我们发现:

  • 加密方式:AES
  • 模式:CBC
  • 填充:未知
  • IV:1234567890123456
  • KEY:1234567890123456

这个填充字段由于混淆比较严重,于是我们这里尝试断点调试。接着在 console 控制台中输出 padding 属性对应的值获取方法,但是这里为 undefined。但是这个偏移量填充字段随便输入也能解密。最后想知道哪里调用了sendDataAes()方法搜索sendDataAes(即可。最终发现是我们点击登录的按钮绑定了该方法。传入的 api 地址为encrypt/aes.php至此已经分析完成。

AES 服务端获取 KEY

输入用户名和密码点击登录进行抓包,发现这里先请求了server_generate_key.php获取 KYE 和 IV。因此现在已经拿到了 KEY 和 IV。但是直接解密是无法解密的。猜测可能进行了其他处理。接着在前端进行定位。这里我们采用查看登录按钮的事件进行定位。定位到fetchAndSendDataAes()函数的定义,搜索fetchAndSendDataAes(。发现这里先请求了server_generate_key.php文件。接着 debug 发现这里经过一系列的处理,将 key 和 iv 赋值给这两个变量。当 debug 经过这里之后,我们在控制台进行打印。实际上最终的 KEY 和 IV 是十六进制的。后面的代码就是获取用户名和密码。我将其转为 JSON 格式:例如:

{  
"username":"admin",  
"password":"admin"  
}

接着调用加密方法进行加密。从上图中我们可以知道填充为:Pkcs7,但是加密方式以及模式还不知道。这里我们就需要 debug 到以上代码的下方。然后在 console 控制台打印。最后得到:

  • 加密方式:AES
  • 密钥:87eeb363f722d4730ffdce736f8f19ff
  • 偏移量:aa29401e5892b0820cefa2f31bb4e141
  • 模式:CBC
  • 填充:Pkcs7

这里使用 autoDecoder 插件对其进行爆破,首先配置: 我们再来看请求,这里就会对内容进行解密。接着我们需要使用 Intruder 模块对此处进行爆破(直接在上图中右键发送即可)虽然这里看到的传输内容是明文,但是实际上是加密只会发送到服务端的。这是 autoDecoder 自动会对请求体进行加密。

RSA 加密

抓取登录的数据包: 这里我使用的还是通过按钮的事件定位到具体的方法。进入到这里只会,我们在这里打断点。这里很明显是一个公钥。当将公钥赋值给变量执行之后,我们在 console 控制台打印这个公钥。由于存在\n换行故而这里通过console.log()进行打印。会解析换行符号。这里有个坑点就是需要 URL 编码一下。注意红框部分不要填,不然会解密失败。对密码进行爆破,autodecoder 插件会自动对 body 进行加密。

DES 规律 KEY

定位到相关函数之后,我们这里直接找到 KEY 和 IV 所在的位置。

  • 红色箭头指向的为 IV
  • 绿色箭头指向的伪 KEY
  • 蓝色箭头指向的为 Padding

这里我们 debug 到以上代码的下方,然后在控制台打印。可以得到信息如下:

  • KEY:61646d696e363636
  • IV:3939393961646d69
  • Padding:Pkcs7

其中 KEY 和 IV 是经过 HEX 编码之后的。解码如下: 这里我们也可以在控制台将他们进行打印。这里蓝色箭头所指向的是用户名,红色箭头所指向的是对密码进行 DES 加密之后传给后端的。因此用户名为明文,密码为密文传输。代码中很明显将 666 和 9999 写死了,而动态的是用户名。如果传入的用户名为 test,则 KEY 就为 test666,IV 为 9999test。所以 KEY 和 IV 的生成规则为:

  • KEY:用户名+666
  • IV:9999+用户名前四位

但是经过实际测试,如果用户名 8 位,则 KEY 的后面不填充 6,反则将空余的位置填充 6。IV 则是在前面填充 9999 后面取用户名的前四位。在上方我们就发现,这里将 KEY 和 IV 采用了十六进制编码。如果我们想要爆破用户名 admin,则 KEY 为 admin666 的十六进制。IV 为 9999admi。将其转为十六进制即可。接着代码中将密码直接获取并进行加密。因此密文就是密码的直接加密形式。在 autoDecoder 插件中配置 Burp 解密效果如下,将其发送到爆破模块。核对一下地址,有时候会自动转为 https 并且添加枚举处。简单添加一下字典。 明文加签 通过事件监听定位到该按钮的点击事件。

  • 红色框标注的分别获取了用户名和密码的值。
  • 绿色框标注了通过生成随机数字将其转为 32 进制变成 0-9 a-z 范围的数据。
  • 蓝色框标注的为当前时间戳(JS 获取的有毫秒时间戳,最后除了 1000 变为秒级的)。
  • 黄色框标注的为固定 KEY。
  • 紫色框标注的是将用户名+密码+绿色框随机字符+蓝色框随机字符串拼接形成新的字符串。
  • 黑色框标注的是通过 HmacSHA256 算法将紫色框的内容使用黄色框的 KEY 进行加密。

nonce 为上图绿色框标注的内容,timestamp 为蓝色框标注的内容,signature 为紫色框标注的内容(黑色框进行加密的,最后返回十六进制)。由于 autoDecoder 没有这个算法,故而需要我们编写加密接口的脚本:

import json  
from flask import Flask, request, jsonify  
import random  
import time  
from Crypto.Hash import HMAC, SHA256  
  
app = Flask(__name__)  
  
@app.route("/encode", methods=["POST"])  
defencrypt():  
# 获取dataBody的参数值  
    request_data = request.form.get('dataBody')  
  
# 解析为字典  
try:  
        request_data = json.loads(request_data)  
except json.JSONDecodeError:  
return jsonify({"error": "Invalid JSON"}), 400  
  
# 生成新值  
    new_nonce = generate_0x3db627()  
    new_timestamp = generate_0x1a525d()  
    _0xaf577e = generate_0xaf577e(request_data["username"], request_data["password"], new_nonce, new_timestamp)  
    _0x2e9aaf = generate_0x2e9aaf()  
    new_signature = generate_0x2ab511(_0xaf577e, _0x2e9aaf)  
  
# 替换字段  
    request_data["nonce"] = new_nonce  
    request_data["timestamp"] = new_timestamp  
    request_data["signature"] = new_signature  
  
# 返回修改后的数据(或继续处理)  
return jsonify(request_data)  
  
# 获取随机字符  
defgenerate_0x3db627():  
# 1. 生成随机数并转换为 36 进制字符串(模拟 JS 的 toString(36))  
    random_num = random.random()  
# 2. 手动实现 36 进制转换(0-9 + a-z)  
    chars = "0123456789abcdefghijklmnopqrstuvwxyz"  
# 3. 取小数点后的部分(类似 JS 的 substring(2))  
    base36 = ""  
for _ inrange(10):  # 限制长度,避免过长  
        random_num *= 36  
        digit = int(random_num)  
        base36 += chars[digit]  
        random_num -= digit  
return base36  
  
# 获取当前时间  
defgenerate_0x1a525d():  
    timestamp_seconds = int(time.time())  
returnstr(timestamp_seconds)  
  
defgenerate_0x2e9aaf():  
return"be56e057f20f883e"  
  
defgenerate_0xaf577e(username, password, _0x3db627, _0x1a525d):  
return username + password + _0x3db627 + _0x1a525d;  
  
defgenerate_0x2ab511(_0xaf577e, _0x2e9aaf):  
# 1. 将字符串转换为字节(UTF-8 编码)  
    key_bytes = _0x2e9aaf.encode('utf-8')  # 密钥  
    msg_bytes = _0xaf577e.encode('utf-8')  # 消息  
  
# 2. 计算 HMAC-SHA256  
    hmac_obj = HMAC.new(key_bytes, msg=msg_bytes, digestmod=SHA256)  
  
# 3. 返回十六进制  
return hmac_obj.hexdigest()  
  
if __name__ == '__main__':  
    app.run(debug=True)

在 autoDecoder 插件中配置:选择接口加解密配置加解密的接口接着 burpsuite 枚举密码字段即可:虽然这里的 nonce 、timestamp、signature 没有改变。但是实际上发给服务器的实际通过加密接口进行替换了,这里可以看下 wireshark 抓的数据包:先请求加密接口替换数据获取加密的数据:然后向接口发送校验请求:可以看到相关的字段已经被加密了。

禁止重放

当我们抓取登录的数据包时,第一次发送正常:第二次发送就提示错误:通过事件监听定位到相应的代码并进行断点调试,发现这里调用了generateRequestData()方法获取了响应体。接着直接发送了。跟到创建请求体的方法:

  • 第一个绿色框标注的为获取用户名和密码的值
  • 第二个绿色框标注的为获取当前时间戳(毫秒级)
  • 红色框标注的为获取公钥

继续跟进分析:这里将毫秒级的时间戳和公钥带入到了 _0x5b0e97() 函数,该函数返回的值,就是最终 random 的值。跟进到该方法分析,就是将当前的毫秒级时间戳进行 RSA 加密只会,作为 random 的值来防止重放。编写脚本:

import json  
from flask import Flask, request, jsonify  
import time  
from Crypto.PublicKey import RSA  
from Crypto.Cipher import PKCS1_v1_5  
import base64  
app = Flask(__name__)  
  
@app.route("/encode", methods=["POST"])  
defencrypt():  
# 获取dataBody的参数值  
    request_data = request.form.get('dataBody')  
# 解析为字典  
try:  
        request_data = json.loads(request_data)  
except json.JSONDecodeError:  
return jsonify({"error": "Invalid JSON"}), 400  
    timestamp = str(int(time.time() * 1000))  
    request_data["random"] = encrypt_rsa(timestamp)  
# 返回修改后的数据(或继续处理)  
return jsonify(request_data)  
  
defencrypt_rsa(timestamp_str):  
    public_key_pem = """  
-----BEGIN PUBLIC KEY-----  
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDRvA7giwinEkaTYllDYCkzujvi  
NH+up0XAKXQot8RixKGpB7nr8AdidEvuo+wVCxZwDK3hlcRGrrqt0Gxqwc11btlM  
DSj92Mr3xSaJcshZU8kfj325L8DRh9jpruphHBfh955ihvbednGAvOHOrz3Qy3Cb  
ocDbsNeCwNpRxwjIdQIDAQAB  
-----END PUBLIC KEY-----  
    """  
    public_key = RSA.import_key(public_key_pem.strip())  
    cipher = PKCS1_v1_5.new(public_key)  # 使用 PKCS#1[](javascript:;) v1.5  
    ciphertext = cipher.encrypt(timestamp_str.encode("utf-8"))  
return base64.b64encode(ciphertext).decode('utf-8')  
  
if __name__ == '__main__':  
    app.run(debug=True)

在 autoDecoder 中配置使用即可,具体参考上一关《明文加签》

加签 KEY 在服务端

先看 burpsuite 抓的登录请求:首先会访问后端接口将用户名和密码发送给加签的接口,获取一个签名。接着将签名发送给校验用户名和密码的接口进行校验。如果修改了用户名或密码字段则需要重新获取签名。前端代码跟我们在 burpsuite 看到的效果是一样的,先从服务端获取签名,再去校验。虽说签名加密和解密都在后端,但是我们可以编写脚本进行密码枚举。

import json  
from flask import Flask, request, jsonify  
import requests  
import time  
  
app = Flask(__name__)  
  
@app.route("/encode", methods=["POST"])  
defencrypt():  
# 获取原始请求体  
    request_data = request.form.get('dataBody')  
# 解析为字典  
try:  
        request_data = json.loads(request_data)  
except json.JSONDecodeError:  
return jsonify({"error": "Invalid JSON"}), 400  
  
# 用户名、密码、时间戳必须和获取签名时的保持一致  
    request_data["signature"],request_data["timestamp"],  = get_signature(request_data["username"], request_data["password"])  
# 返回修改后的数据(或继续处理)  
return jsonify(request_data)  
  
defget_signature(username, password):  
    headers = {  
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.199 Safari/537.36"  
    }  
  
    timestamp = str(int(time.time()))  
  
    data = {  
"username": username,  
"password": password,  
"timestamp": timestamp,  
    }  
  
# 发送 POST 请求(注意:json 参数会自动序列化,无需手动 json.dumps)  
    resp = requests.post(  
"http://127.0.0.1:9090/encrypt/get-signature.php",  
        headers=headers,  
        json=data  # 自动处理 JSON 序列化  
    )  
# 解析响应(
上一篇:解锁企业数字化转型的无限潜能
下一篇:没有了
热门服务和内容
推荐文章
在线客服咨询