学习的必要性

在如今科技飞速发展的时代,传统的明文传输已经慢慢退出历史的舞台。更多的是对用户传入的参数进行加密。

甚至有些时候,对用户的响应内容也是加密的。

那么,当我们测试越权、弱口令和 sql 注入的时候,就必须要对传入的参数进行加密。否则大多时都是无功而返。同时针对于前端加密还有一个天然的优势。由于内容都是经过加密的。 自然不会被 WAF 所拦截

常见的 JS 加密

在线解密工具:https://www.ssleye.com/ssltool/

对称加密

比如 AES、DES 使用的加密和解密的密钥都是同一个。所以叫做对称加密。

非对称加密

比如 RSA 和 ECBSA 使用非对称加密,也就是说,加密和解密使用的是两个密钥,公钥一般在前端,私钥放在后端用于解密。

JS 逆向

快速定位到加密处代码

示例靶场地址:https://github.com/outlaws-bai/GalaxyDemo

通过字段关键字

虽然这里传入的值是经过加密处理的,但是它的 key 是明文的。

因此我们可以直接进行搜索 data:data=以及 data =。进行定位。

当然这里我更推荐在 network 中使用 Ctrl + F 键进行搜索:

定位到相关位置只会可以发现这里使用的是 DES 加密。

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

对加密的内容进行解密:

通过 JS 关键字

可以全局搜索一些关键字如 CryptoJSencryptdecryptkeyiv等。

通过路由

点击查询或登录的时候,我们来到 network 查看请求,可以发现请求的接口为 getUserInfo看其启动器即可定位到请求时执行的方法。

定位到相应方法只会,查看对接口传入的参数。本案例传入的是 {data : encrypted}接着往上追踪即可。

通过事件监听

这里的加密字段是通过点击 query 按钮的时候进行加密的,因此我们可以查看该元素的监听事件。重点关注一些 click 事件和 submit 事件。最后点击定位即可。

配合断点调试

实际业务系统中,可能会添加一些混淆、或代码量比较大,方法比较多。那么此时我们就需要配合断点调试。

通过以上几种方法定位到大概位置,然后多下几个断点进行调试:

一步一步查看断点,此时我们的 username 还为明文。

继续跟进发现这里对我们传入的用户名作为对象中的值,其属性名称为 username 值为我们传入的内容,即 user1,最后对该对象进行 DES 加密之后,作为 getUserInfo接口的 body 部分中的 {data : "此处为加密内容"}内容发送给后端。

因此前端实际发送的内容为:

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

靶场实践

这里使用的靶场为:https://github.com/SwagXz/encrypt-labs

AES 固定 KEY

发送给后端的内容:

由于这里传入的参数名为 encryptedData因此我们在前端进行搜索:

可以发现该接口发送的内容跟我们抓包获取的发送内容一致。都是以 encryptedData=开头。并且该 JS 代码是做了混淆处理的。

接着我们向上追踪。定位对加密字段的处理片段,可以发现是 _0x1375d7这个变量。在这里发现 iv 的变量为 _0x2d9cd5,那么上一个则是 AES 的 KEY 值。最后发现 sendDataAes() 函数的形参为 api 的 URL 地址。

上述之后我们发现:

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

这个填充字段由于混淆比较严重,于是我们这里尝试断点调试。

接着在 console 控制台中输出 padding 属性对应的值获取方法,但是这里为 undefined。

但是这个偏移量填充字段随便输入也能解密。https://www.toolhelper.cn/SymmetricEncryption/AES

最后想知道哪里调用了 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 格式:例如:

1
2
3
4
{
"username": "admin",
"password": "admin"
}

接着调用加密方法进行加密。

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

最后得到:

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

这里使用 autoDecoder 插件对其进行爆破,首先配置:

我们再来看请求,这里就会对内容进行解密。

接着我们需要使用 Intruder 模块对此处进行爆破(直接在上图中右键发送即可)

虽然这里看到的传输内容是明文,但是实际上是加密只会发送到服务端的。这是 autoDecoder 自动会对请求体进行加密。

RSA 加密

抓取登录的数据包:

这里我使用的还是通过按钮的事件定位到具体的方法。

进入到这里只会,我们在这里打断点。这里很明显是一个公钥。

当将公钥赋值给变量执行之后,我们在 console 控制台打印这个公钥。由于存在 \n换行故而这里通过 console.log()进行打印。会解析换行符号。

但是由于该环境 RSA 只有公钥泄漏没有私钥,因此只能用于加密,无法对内容进行解密。因此进行密码的爆破还是可以的。

这里有个坑点就是需要 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 没有这个算法,故而需要我们编写加密接口的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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"])
def encrypt():
# 获取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)

# 获取随机字符
def generate_0x3db627():
# 1. 生成随机数并转换为 36 进制字符串(模拟 JS 的 toString(36))
random_num = random.random()
# 2. 手动实现 36 进制转换(0-9 + a-z)
chars = "0123456789abcdefghijklmnopqrstuvwxyz"
# 3. 取小数点后的部分(类似 JS 的 substring(2))
base36 = ""
for _ in range(10): # 限制长度,避免过长
random_num *= 36
digit = int(random_num)
base36 += chars[digit]
random_num -= digit
return base36

# 获取当前时间
def generate_0x1a525d():
timestamp_seconds = int(time.time())
return str(timestamp_seconds)

def generate_0x2e9aaf():
return "be56e057f20f883e"

def generate_0xaf577e(username, password, _0x3db627, _0x1a525d):
return username + password + _0x3db627 + _0x1a525d;

def generate_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 的值来防止重放。

编写脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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"])
def encrypt():
# 获取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)

def encrypt_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 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 看到的效果是一样的,先从服务端获取签名,再去校验。

虽说签名加密和解密都在后端,但是我们可以编写脚本进行密码枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import json
from flask import Flask, request, jsonify
import requests
import time

app = Flask(__name__)

@app.route("/encode", methods=["POST"])
def encrypt():
# 获取原始请求体
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)

def get_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 序列化
)
# 解析响应(确保响应是 JSON 格式)
content = resp.json()
return content["signature"], timestamp
if __name__ == '__main__':
app.run(debug=True)

运行脚本,配置 autoDecoder 爆破即可:

经过大量的枚举测试,把线程调低一点(便于有足够的时间去获取签名),还是可行的。

在实战场景下,如果有验证码的存在,可能会稍微复杂一些。

JS 逆向的高级应用

虽说这个系统本是一个靶场,不过这个靶场比较贴合实际场景,因此将其记录。

靶场地址:https://github.com/0ctDay/encrypt-decrypt-vuls

尝试登录系统发现传入的用户名和密码都进行了加密(属于不显示参数的那种),而响应包也是加密了的。

同时在请求头中也发现了随机的 requestId 和 sign 值来防止重放:

作者文章中提到“很多新手会认为一定得找到加解密函数。其实不然, 老手都会告诉你无论是APP还是JS中可以先找到明文点”。那么什么是明文点呢?

在前端进行复杂的请求操作时,肯定会经过一系列从A函数–>B函数–>C函数–>D函数–>E函数之类的流程, 那么在这个流程中,假设D函数是加密函数,那么ABC函数中原始请求参数均是明文的,这就是明文点,找到明文点后再一步步调试, 其实就能顺腾摸瓜找到加密函数了。

寻找明文点

方法一: v_jstools

作者项目地址:https://github.com/cilame/v_jstools

新版可能有点问题,不行的话可以去链接: https://pan.baidu.com/s/14ZDJ4roEfa1Q1k2hbzdy3w 提取码: 15vc 进行如下配置:

打开如下按钮

接着控制台会出现如下 inject start!表示开启成功。

由于控制台有一些干扰信息清空之后,进行登录:

这里就发现了明文的位置,我们点击打印的 URL 地址跳转到相应为止。

在此处进行断掉调试,此时的 t.data还是明文。接着经过函数 v 处理之后依旧是明文,因此加密点不在这里。

看后续代码我们可以发现:

  • 请求头中的 timestamp 为变量 r 的值,即当前时间戳(毫秒级,转为秒级需除 1000)
  • 请求头中的 requestId 为变量 i 的值,i 的值由 p 函数生成并返回。
  • 请求头中的 sign 值为 n+i+r 的 MD5 值。n 为用户名密码验证码的 JSON。

最后将 n 带入到了函数 l 进行了处理。我们执行到该处。可以发现经过函数 l 处理之后就变成了加密内容。

因此我们在函数 l(n)处下一个断点,进入到该函数:

按照 AES 的加密方法使用,可得出:

  • t 为要加密的数据
  • f 为加密所使用的 KEY
  • h 为加密所使用的偏移量
  • 加密模式为 CBC
  • 填充为 Pkcs7

因此使用在线网站解密:

解密请求包:

解密响应包:

方法二:JS Hook

修改当前数据包

断点调试修改

这种方式不推荐,比较麻烦。

JS-forward

该系统存在 requestId,timestamp,和 sign 因此可以借助 JS-Forward 进行发送(即使知道了 KEY 和 IV, 也不能在 Burpsuite 中进行重发),后续可能通过 JSRPC 远程调用或 Python 编写加密脚本解决这一问题。

首先我们了解一下 JS-forward 运行原理,简单来说就是在明文点处插入一段 JS 代码,这段代码先通过 AJAX 请求将现有的请求内容发送给 burpsuite,burpsuite 拦截并修改内容后,返回到原始变量中,优点是操作比较统一,如果明文点正确,后续所有的改包操作都可以在 burpsuite 中进行

第一步确认明文点:

刚刚通过 v_jstools 工具已经确认了明文点,也就是 t.data

JS-Forward 项目地址:https://github.com/G-Security-Team/JS-Forward

启动 JS-Forward:

第二步插入 JS 代码:

JS-Forward 的使用尽量在明文点函数的第一行插入 JS-Forward 生成的代码,因为不知道后续代码做了什么操作。

这里以 Chrome 浏览器插入 JS 为例:

b1:找到F12–源代码–替换(覆盖)–点击选择文件夹–选择我们硬盘中一个空文件夹

如果有提问,点击编辑文件

b2: 在 网页–明文点JS文件处–右键–替换内容

在函数的第一行插入 JS-Forward 生成的 JS 代码,然后 CTRL + S 保存。

关闭调试功能或关闭 F12,打开 burpsuite 进行拦截(浏览器无需设置代理到 burpsuite):

该方式是不需要浏览器设置代理为 Burpsuite 的。

工具的原理如下:在明文代码处插入 JS 代码,JS-Forward 会将明文数据的变量值发给 Burpsuite(通过代理的模式),将 Burpsuite 的返回的 JSON 字段,赋值给 t.data

主动发包的加密和解密

以上《修改当前数据包》只适合浏览器进行提交操作后的数据修改,而实际场景下,可能有一些自动请求的接口也加密了。

  • 优点:是简单易上手,就算是复杂的加密环境,只要找到明文点,后续工作不太复杂。
  • 缺点:是无法应对主动发包的情况,比如要使用被动扫描工具,暴力破解,重放测试等需求的时候,无法自动化完成。

JsRpc 远程 JS 调用

项目地址:https://github.com/jxhczhl/JsRpc

启动 JsRpc 并注入代码

运行程序:

接着复制 JsRpc 项目目录下的 resource 目录的 JsEnv_Dev.js 文件内容到 console 控制台:

接着输入如下代码连接客户端:

1
2
var example = new Hlclient("ws://127.0.0.1:12080/ws?group=example"); 
// example 变量名称可以为其他,但不要使用容易与系统源码重复的;group也是。

此时的服务端状态:

记录加密函数

根据上诉对系统的分析,我们可以得知,加密函数为 l。因此我们需要记录加密函数到 window。但是由于函数 l可能是局部的函数,因此我们需要断点到其使用处再进行记录。

1
2
window.enc = l
enc("admin")

向 JsRpc 注册加密函数

控制台执行:

1
2
3
4
5
// 这个example要和客户端连接变量名一致
example.regAction("enc", function (resolve, param) {
var res = enc(String(param));
resolve(res);
})

此时就可以把断点执行掉了(不然会提示超时)。接着访问:

1
http://127.0.0.1:12080/go?group=example&action=enc&param=admin123

此时该接口就调用了 JS 中的加密函数返回加密之后的值。但是此时还不足以进行爆破。因为不只是对请求体的内容进行加密,而请求头部信息中还有 requestId、sign、timestamp 等随机值。

JSRPC-配合 AutoDecoder

方案介绍和请求分析

目前比较流行的一个解决方案, 通过 mitm 将原始请求发送到 JS-RPC 中进行加密后修改原始数据包内容, 再进行发包 。

这里关于网站的加密分析在本文的“寻找明文点->方法一:v-jstools”已经分析了。下面直接引用:

请求头中的随机字段:

  • timestamp
  • requestId
  • sign

请求体的加密字段:

  • 用户名、密码、验证码的 JSON 数据

后续代码调试我们可以发现:

  • 请求头中的 timestamp 为变量 r 的值,即当前时间戳(毫秒级,转为秒级需除 1000)
  • 请求头中的 requestId 为变量 i 的值,i 的值由 p 函数生成并返回。
  • 请求头中的 sign 值为 n+i+r 的 MD5 值。n 为用户名密码验证码的 JSON。

最后将 n 带入到了函数 l进行了处理,函数 l就是请求体的加密函数。

针对数据包的修改我们需要在请求头中添加

  • timestamp
  • requestId
  • sign

然后对请求体的内容调用页面的函数 l进行加密。

启动 JsRpc 并注入代码

复制 resouces/JsEnv_Dev.js 的内容注入到网页并进行连接:

1
var example = new Hlclient("ws://127.0.0.1:12080/ws?group=example");

断点调试记录函数并注册

断点到执行加密函数的函数内:

记录函数:

1
2
3
4
5
6
7
8
9
10
//时间戳
window.time = Date.parse
//requestId
window.id = p
//v函数
window.v1 = v
//签名
window.m = a.a.MD5
//加密
window.enc = l

注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//md5函数
example.regAction("req", function (resolve,param) {
//请求头
let timestamp = time(new Date());
let requestid = id();
let v_data = JSON.stringify(v1(param));
let sign = m(v_data + requestid + timestamp).toString();
//加密请求体
let encstr = enc(v_data);
let res = {
"timestamp":timestamp,
"requestId":requestid,
"encstr":encstr,
"sign":sign
};
resolve(res);
})

测试 JSRPC

由于我们注册的时候使用的是 req,因此这里的 action 为 req。

这样我们就可以一次性获取所有请求的需求了。

密码破解

这里需要说明的是,该靶场的验证码在前端生成,也就是说验证码在后端是没有校验的,因此随便填即可。

编写脚本 用于调用 JSRPC 将请求体进行加密,并返回 timestamp,requestId,sign 以及 body 加密的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import json
from flask import Flask, request, jsonify
import requests
import time

app = Flask(__name__)


@app.route("/encode", methods=["POST"])
def encrypt():
# 获取原始请求体
request_data = request.form.get('dataBody')
# 获取请求头
headers = request.form.get('dataHeaders')

data_dict, enc_str = get_encrypt_body(request_data)
if headers != None: # 开启了响应头加密
headers = headers + "timestamp: {}\r\n".format(data_dict['timestamp'])
headers = headers + "requestId: {}\r\n".format(data_dict['requestId'])
headers = headers + "sign: {}".format(data_dict['sign'])
return headers.strip() + "\r\n\r\n\r\n\r\n" + enc_str # 返回值为固定格式,不可更改
return enc_str

def get_encrypt_body(request_body):
url = "http://127.0.0.1:12080/go?group=example&action=req&param=" + request_body
resp = requests.get(url)
# 1. 获取 data 字段(JSON 字符串)
data_json_str = resp.json().get('data')
# 2. 解析 data 字段为字典
data_dict = json.loads(data_json_str)
# 3. 提取 encstr
enc_str = data_dict['encstr']
return data_dict, enc_str
if __name__ == '__main__':
app.run()

接着 autoDecoder 配置:

添加 payload 之后进行枚举(这里建议不要使用多线程,不限请求加密接口可能不及时,导致一些漏处理):

失败的数据包:

Yakit 热加载

实战项目

JS 源码地图

对某系统进行渗透测试,发现在获取一些当前用户信息、用户列表等接口的时候,这里传入了 token。

尝试对其进行 base64 解码,发现也是乱码,那么可能就没有那么简单。

于是在进行翻阅 JS 文件的时候,意外发现该网站是通过 webpack 进行打包的,存在 .js.map文件直接可以还原混淆的打包之前的 vue 源码。这里直接查看在工具类中发现 crypto.js 文件,文件中发现了 KEY 以及模式。

通过上诉信息,解密我们传入的 Token,发现 id 为 23。

尝试将 1 进行加密之后查看返回信息:

返回了其他信息,证明此处是存在越权的。

对影响内容按照上面的方法进行解密,成功越权。

参考