常规分析

这块很多步骤参考原作者文章,地址如下:

https://mp.weixin.qq.com/s/loXsmR8uhrtFA_3EuBQB8w

目标地址

aHR0cHM6Ly9tdXNpYy4xNjMuY29tLyMvc29uZz9pZD0yMDYxOTc4OTYx

分析数据包

1、获取评论接口为https://music.163.com/weapi/comment/resource/comments/get?csrf_token=

image.png

2、POST请求,里面有两个参数 “params“和”encSecKey

image.png

尝试多次请求,发现这两个参数值是一直变化的。

image.png

分析参数

按照以往的抓取经验来说,它们大概率是在一起的,因此可以通过全局搜索encSecKey这个词汇。

image.png

var bKL9C = window.asrsea(JSON.stringify(i6c), bvl1x(["流泪", "强"]), bvl1x(Rj5o.md), bvl1x(["爱心", "女孩", "惊恐", "大笑"]));
e6c.data = j6d.cq6k({
params: bKL9C.encText,
encSecKey: bKL9C.encSecKey
})

打断点调试一下:

fae1bc4fedfcd635849bfcf0d8c47326.png

d296bd02b2adf2c40ac2011ddc78fd5a.png

成功定位到参数的具体位置。。。

寻找加密函数

var bKL9C = window.asrsea(JSON.stringify(i6c), bvl1x(["流泪", "强"]), bvl1x(Rj5o.md), bvl1x(["爱心", "女孩", "惊恐", "大笑"]));
e6c.data = j6d.cq6k({
params: bKL9C.encText,
encSecKey: bKL9C.encSecKey
})

加密函数应该就是asrsea,在里面一共要传递四个参数。我们可以看看这四个参数分别是什么?

点击翻页,然后断点下来后查看参数值:

img

由上图可以发现,这四个参数中,分别找了两个数据包来判断,它们会不会有什么变化,变化的值是pageNo。

当然了,这里除了pageNo之外,还有cursor也是变化的,它代表的是时间戳。

意味着即使是相同的页面paramsencSecKey的值也是时时刻刻都在发生变化的。

进入加密函数

img

进来之后,函数套函数,开始补全js代码

function d(d, e, f, g) {
var h = {}
, i = a(16);
return h.encText = b(d, g),
h.encText = b(h.encText, i),
h.encSecKey = c(i, e, f),
h
}

image.png

里面包含了

  • 函数a
  • 函数b
  • 函数c

函数a

function a(a) {
var d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; a > d; d += 1)
e = Math.random() * b.length,
e = Math.floor(e),
c += b.charAt(e);
return c
}

不难看出,它的主要作用是生成16位随机字符

简单做个修改

function a(length){
let d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; length > d; d += 1){
e = Math.random() * b.length;
e = Math.floor(e);
c += b.charAt(e);
}
return c
}

let result = a(16);

函数b

function b(a, b) {
var c = CryptoJS.enc.Utf8.parse(b)
, d = CryptoJS.enc.Utf8.parse("0102030405060708")
, e = CryptoJS.enc.Utf8.parse(a)
, f = CryptoJS.AES.encrypt(e, c, {
iv: d,
mode: CryptoJS.mode.CBC
});
return f.toString()
}

接下来,就来解释一下,这段代码的意思。

a代表的是你要加密的字符串,b代表密钥。它的基本作用是利用AES加密算法配合CBC模式的加密过程,最后以base64的形式输出

简单修改一下,上述代码

function generateRandomString(length){
let d, e, b = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", c = "";
for (d = 0; length > d; d += 1){
e = Math.random() * b.length;
e = Math.floor(e);
c += b.charAt(e);
}
return c
}

// 获取AES加密数据
const CryptoJS = require("crypto-js")
function encryptMessage(message, key) {
let keyUtf8 = CryptoJS.enc.Utf8.parse(key);
let iv = CryptoJS.enc.Utf8.parse("0102030405060708");
let messageUtf8 = CryptoJS.enc.Utf8.parse(message);
let encrypted = CryptoJS.AES.encrypt(messageUtf8, keyUtf8, {
iv: iv,
mode: CryptoJS.mode.CBC});
return encrypted.toString();
}

let message = {"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"3","pageSize":"20","cursor":"1708238561030","offset":"0","orderType":"1","csrf_token":""};
let key = generateRandomString(16);
let encrypyResult = encryptMessage(message, "0CoJUm6Qyw8W8jud")
// console.log(encrypyResult)
let encText = encryptMessage(encrypyResult, key)
console.log(encText)

函数c

function c(a, b, c) {
var d, e;
return setMaxDigits(131),
d = new RSAKeyPair(b,"",c),
e = encryptedString(d, a)
}

这段代码里面分别有三个函数,得把这三个函数给补全了

首先来补全setMaxDigits函数

function setMaxDigits(a) {
maxDigits = a,
ZERO_ARRAY = new Array(maxDigits);
for (var b = 0; b < ZERO_ARRAY.length; b++)
ZERO_ARRAY[b] = 0;
bigZero = new BigInt,
bigOne = new BigInt,
bigOne.digits[0] = 1
}

这里有一点需要注意一下,BigInt并不是JavaScript内置的类型,而是自定义的,因此我们需要找到它。

function BigInt(a) {
this.digits = "boolean" == typeof a && 1 == a ? null : ZERO_ARRAY.slice(0),
this.isNeg = !1
}

上述两段代码的作用应该是用于做一些运算相关的动作。具体的事情我们也不必深究,接下来要做的事情就是将两个函数合并起来。修改后如下:

function BigInt(value) {
let ZERO_ARRAY = new Array(maxDigits);
this.digits = "boolean" == typeof a && 1 == value ? null : ZERO_ARRAY.slice(0),
this.isNeg = !1
}
function setMaxDigits(value) {
let maxDigits = value;
let ZERO_ARRAY = new Array(maxDigits);
for (let i = 0; i < ZERO_ARRAY.length; i++) {
ZERO_ARRAY[i] = 0;
}
let bigZero = new BigInt(131); // 传递false代表我们希望初始化为0的BigInt对象
let bigOne = new BigInt(131);
bigOne.digits[0] = 1;
}

紧接着分析RSAKeyPair函数

function RSAKeyPair(a, b, c) {
this.e = biFromHex(a),
this.d = biFromHex(b),
this.m = biFromHex(c),
this.chunkSize = 2 * biHighIndex(this.m),
this.radix = 16,
this.barrett = new BarrettMu(this.m)
}

剩下的就不细说了,继续往前补全就行。。。。

最终代码

function get_post_data(i3x) {
e = '010001'
f = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
g = '0CoJUm6Qyw8W8jud'
return bVg1x = asrsea(JSON.stringify(i3x), e, f, g);
}

let i3x = '{"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"1","pageSize":"20","cursor":"-1","offset":"0","orderType":"1","csrf_token":""}';
let result1 = get_post_data(i3x)
console.log(result1)

python

import requests
import execjs
import time
import pprint



headers = {
"authority": "music.xxx.com",
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://music.xxx.com",
"pragma": "no-cache",
"referer": "https://music.xxx.com/outchain/player?type=2&id=2051548110&auto=1&height=66&bg=e8e8e8",
"sec-ch-ua": "\"Google Chrome\";v=\"119\", \"Chromium\";v=\"119\", \"Not?A_Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
}

params = {
'csrf_token': '',
}

with open('xzwy.js', 'r', encoding='utf8') as f:
js = f.read()
cursor = int(time.time() * 1000)
i3x = {"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"1","pageSize":"20","cursor":f"{cursor}","offset":"0","orderType":"1","csrf_token":""}
# print(i3x)
result = execjs.compile(js).call('get_post_data', i3x)
encText = result.get('encText')
encSecKey = result.get('encSecKey')
print(encText)
print(len(encSecKey))

data = {
'params': encText,
'encSecKey': encSecKey,
}

response = requests.post(
url='xxx',
params=params,
headers=headers,
data=data
)
print(response.status_code)
pprint.pprint(response.json())

img

JsRpc调用

可以看到,在后续定位到函数后,各种扣取代码,函数嵌套函数,为整个分析的过程带来了很大的麻烦。

那可不可以不扣取代码就实现获取评论的功能呢?

JSRPC刚好就可以实现该功能。

加密函数参数分析

window.asrsea(JSON.stringify(i6c), bvl1x(["流泪", "强"]), bvl1x(Rj5o.md), bvl1x(["爱心", "女孩", "惊恐", "大笑"]));

有4个参数:

JSON.stringify(i6c) 
bvl1x(["流泪", "强"])
bvl1x(Rj5o.md)
bvl1x(["爱心", "女孩", "惊恐", "大笑"])

上面4个参数,对应某次的请求的值,分别如下

'{"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"4","pageSize":"20","cursor":"1713198658380","offset":"0","orderType":"1","csrf_token":""}'
'010001'
'00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7'
'0CoJUm6Qyw8W8jud'

可以看到后三个参数是先调用了bvl1x方法的,所以在hook加密函数之前,要先对他进行hook。

(貌似这后三个值不会变化,可以写死,但这块为了验证RPC,还是借用他来实现)

bvl1x方法

1、定义全局导出函数

window.p = bvl1x

image.png

2、配置RPC环境

(1)注入JSenv环境

img

(2)运行exe,浏览器(客户端)和 本地(服务端)建立连接

image.png

// 注入环境后连接通信
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz&name=hlg");

image.png

3、在前端注入JS代码

function hlg1(data1){
return p(data1);
}

demo.regAction("hacker1", function (resolve,param){
res=hlg1(param["data1"]);
resolve(res);
})

image.png

4、python脚本如下:

import json
import requests
import jsonpath

jscode1 = """
(function(){
s = p(["流泪", ""]);
return s
})()
"""

jscode2 = """
(function(){
var Rj5o_md = ["", "流感", "这边", "", "嘴唇", "", "开心", "呲牙", "憨笑", "", "皱眉", "幽灵", "蛋糕", "发怒", "大哭", "兔子", "星星", "钟情", "牵手", "公鸡", "爱意", "禁止", "", "亲亲", "", "礼物", "", "", "生病", "钻石", "", "", "示爱", "", "小鸡", "痛苦", "撇嘴", "惶恐", "口罩", "吐舌", "心碎", "生气", "可爱", "鬼脸", "跳舞", "男孩", "奸笑", "", "", "便便", "外星", "圣诞"]
s = p(Rj5o_md);
return s
})()
"""

jscode3 = """
(function(){
s = p(["爱心", "女孩", "惊恐", "大笑"]);
return s
})()
"""


url = "http://localhost:12080/execjs"


def des_enc():
data1 = {
"group": "zzz",
"name": "hlg",
"action": "hacker1",
"jscode": jscode1
}
res1 = requests.post(url, data=data1)
print(res1.text)

data2 = {
"group": "zzz",
"name": "hlg",
"action": "hacker1",
"jscode": jscode2
}
res2 = requests.post(url, data=data2)
print(res2.text)

data3 = {
"group": "zzz",
"name": "hlg",
"action": "hacker1",
"jscode": jscode3
}
res3 = requests.post(url, data=data3)
print(res3.text)


des_enc()

运行后,获得三个参数的值。img

加密函数调用

1、前端注入JS代码

function hlg2(data2){
var param2 = "010001";
var param3 = "00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7"
var param4 = "0CoJUm6Qyw8W8jud"
console.log(asrsea(data2,param2,param3,param4))
obj = asrsea(data2,param2,param3,param4)
return JSON.stringify(obj);
}

demo.regAction("hacker2", function (resolve,param){
res=hlg2(param["data2"]);
resolve(res);
})

img

2、python脚本如下

param1 = '{"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"4","pageSize":"20","cursor":"1713198658380","offset":"0","orderType":"1","csrf_token":""}'

url = "http://localhost:12080/go"


def enc():
data = {
"group": "zzz",
"name": "hlg",
"action": "hacker2",
"param": json.dumps({"data2": param1})
}
res = requests.post(url, data=data)
return json.loads(res.text)


print(enc())

image.png

3、但上面代码是在前端中将参数写死的,这块可以动态调用Python脚本中计算的值。

function hlg2(data2,param2,param3,param4){
console.log(asrsea(data2,param2,param3,param4))
obj = asrsea(data2,param2,param3,param4)
return JSON.stringify(obj);
}

demo.regAction("hacker2", function (resolve,param){
res=hlg2(param["data2"],param["param2"],param["param3"],param["param4"]);
resolve(res);
})
def enc():
data = {
"group": "zzz",
"name": "hlg",
"action": "hacker2",
"param": json.dumps({"data2": param1, "param2": param2, "param3": param3, "param4": param4})
}
res = requests.post(url, data=data)
return json.loads(res.text)


print(enc())

img

完整代码:

import json
import requests

jscode1 = """
(function(){
s = p(["流泪", ""]);
return s
})()
"""

jscode2 = """
(function(){
var Rj5o_md = ["", "流感", "这边", "", "嘴唇", "", "开心", "呲牙", "憨笑", "", "皱眉", "幽灵", "蛋糕", "发怒", "大哭", "兔子", "星星", "钟情", "牵手", "公鸡", "爱意", "禁止", "", "亲亲", "", "礼物", "", "", "生病", "钻石", "", "", "示爱", "", "小鸡", "痛苦", "撇嘴", "惶恐", "口罩", "吐舌", "心碎", "生气", "可爱", "鬼脸", "跳舞", "男孩", "奸笑", "", "", "便便", "外星", "圣诞"]
s = p(Rj5o_md);
return s
})()
"""

jscode3 = """
(function(){
s = p(["爱心", "女孩", "惊恐", "大笑"]);
return s
})()
"""

url = "http://localhost:12080/execjs"


def des_enc():
data1 = {
"group": "zzz",
"name": "hlg",
"action": "hacker1",
"jscode": jscode1
}
res1 = requests.post(url, data=data1)

data2 = {
"group": "zzz",
"name": "hlg",
"action": "hacker1",
"jscode": jscode2
}
res2 = requests.post(url, data=data2)

data3 = {
"group": "zzz",
"name": "hlg",
"action": "hacker1",
"jscode": jscode3
}
res3 = requests.post(url, data=data3)

return res1, res2, res3


res1, res2, res3 = des_enc()
param2 = json.loads(res1.text)['data']
param3 = json.loads(res2.text)['data']
param4 = json.loads(res3.text)['data']

param1 = '{"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"4","pageSize":"20","cursor":"1713198658380","offset":"0","orderType":"1","csrf_token":""}'

url = "http://localhost:12080/go"


def enc():
data = {
"group": "zzz",
"name": "hlg",
"action": "hacker2",
"param": json.dumps({"data2": param1, "param2": param2, "param3": param3, "param4": param4})
}
res = requests.post(url, data=data)
return json.loads(res.text)


print(enc())

autodecoder联动

加密函数:
var bKL3x = window.asrsea(JSON.stringify(i1x), bvl3x(["流泪", "强"]), bvl3x(Rj6d.md), bvl3x(["爱心", "女孩", "惊恐", "大笑"]));

1、前端注入的JS进行参数调用,可以修改为:

window.bvl3x = bvl3x
window.Rj6d_md = ["色", "流感", "这边", "弱", "嘴唇", "亲", "开心", "呲牙", "憨笑", "猫", "皱眉", "幽灵", "蛋糕", "发怒", "大哭", "兔子", "星星", "钟情", "牵手", "公鸡", "爱意", "禁止", "狗", "亲亲", "叉", "礼物", "晕", "呆", "生病", "钻石", "拜", "怒", "示爱", "汗", "小鸡", "痛苦", "撇嘴", "惶恐", "口罩", "吐舌", "心碎", "生气", "可爱", "鬼脸", "跳舞", "男孩", "奸笑", "猪", "圈", "便便", "外星", "圣诞"]
demo.regAction("enc", function (resolve,param){
res=asrsea(param,bvl3x(["流泪", "强"]), bvl3x(Rj6d_md), bvl3x(["爱心", "女孩", "惊恐", "大笑"]));
resolve("params="+encodeURIComponent(res.encText)+"&encSecKey="+res.encSecKey);
})

2、Python代码也可以修改如下:

import json
import requests


param = '{"rid":"R_SO_4_2061978961","threadId":"R_SO_4_2061978961","pageNo":"4","pageSize":"20","cursor":"1713198658380","offset":"0","orderType":"1","csrf_token":""}'

url = "http://localhost:12080/go"


def enc():
data = {
"group": "zzz",
"name": "hlg",
"action": "enc",
"param": json.dumps(param)
}
res = requests.post(url, data=data)
return json.loads(res.text)["data"]


print(enc())

image.png

3、autodecoder代码

import json
import requests
from flask import Flask, request

app = Flask(__name__)
url = "http://localhost:12080/go"


@app.route('/decode', methods=["POST"])
def decrypt():
param = request.form.get('dataBody') # 获取 post 参数
data = {
"group": "zzz",
"name": "hlg",
"action": "enc",
"param": json.dumps(param)
}
res = requests.post(url, data=data) # 这里换get也是可以的

encry_param = json.loads(res.text)['data']
print("param:", json.dumps(param))
print("="*200)
print("encry_param:",encry_param)
return encry_param


@app.route('/encode', methods=["POST"])
def encrypt():
param = request.form.get('dataBody') # 获取 post 参数

return param


if __name__ == '__main__':
app.debug = True # 设置调试模式,生产模式的时候要关掉debug
app.run(host="0.0.0.0", port="8888")

img

img