简介

蝉MM上商品库数据返回后是加密的需要处理才能正常阅读。经过分析为AES加密ECB模式,Pkcs7填充,解密后经过一个u函数处理再gzip解压。
目标页面:aHR0cHM6Ly93d3cuY2hhbm1hbWEuY29tL3Byb21vdGlvblJhbmsvP2tleXdvcmQ9Jmhhc19qeF9jb21taXNzaW9uPTA=

分析

  1. 浏览器F12打开开发者工具,切换到“网络”工具可以看到返回数据类似下面这样
    1
    2
    3
    4
    5
    6
    7
    8
    {
    "data": {
    "is_encrypt": true,
    "data": "YS2uKFD807w8u6sRgZ20YYglwYz2JvhIymEnYMQNKcudNDDLLP8+HhcOeb6zTv6btyIPjU7DlznAJts6v7LZmtVY040w80qVCQdlT0VHMDHrdLvBPJiyOsNsn2FNoC5S6E8ka8U5YM1iwuKIe6I+kGG5HMY1reCV0kyoIucnud8="
    },
    "errCode": 0,
    "rid": "3d9c327a08d38387cecf87966e74c266"
    }
  2. data字段看起来像是base64编码的,但是经过base64解码后发现是乱码,可能是加密后的数据,所以需要解密才能看到明文。直接搜索“is_encrypt”可以在js内🫡两个地方,在两个地方直接下断点,刷新页面略微看了下断下来的数据发现ajax返回的数据走其中一个断点参数为e。代码片段如下:
    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
    function $() {
    return $ = (0,
    r.Z)((0,
    a.Z)().mark((function t(e, n) {
    var i, o, r, s, c, l, u;
    return (0,
    a.Z)().wrap((function(t) {
    while (1)
    switch (t.prev = t.next) {
    case 0:
    if (c = null === e || void 0 === e ? void 0 : e.data,
    !(!0 === (null === c || void 0 === c || null === (i = c.data) || void 0 === i ? void 0 : i.is_encrypt) && "string" === typeof (null === c || void 0 === c || null === (o = c.data) || void 0 === o ? void 0 : o.data) && (null === c || void 0 === c || null === (r = c.data) || void 0 === r || null === (s = r.data) || void 0 === s ? void 0 : s.length) > 0)) {
    t.next = 6;
    break
    }
    return t.next = 4,
    (0,
    Z.ZP)(null === c || void 0 === c || null === (l = c.data) || void 0 === l ? void 0 : l.data);
    case 4:
    u = t.sent,
    c.data = u;
    case 6:
    n(e);
    case 7:
    case "end":
    return t.stop()
    }
    }
    ), t)
    }
    ))),
    $.apply(this, arguments)
    }
    所有数据都走这里看起来解密的时候也在这里根据条件走某个分支,直接在i.is_encrypt行下条件断点e.data.data.is_encrypt == true,刷新页面等断下来单步步入。十多步之后发现了如下加密代码:
    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
    function m() {
    return m = (0,
    o.Z)((0,
    i.Z)().mark((function t(e) {
    var n, a, o, r;
    return (0,
    i.Z)().wrap((function(t) {
    while (1)
    switch (t.prev = t.next) {
    case 0:
    return t.prev = 0,
    n = s.enc.Utf8.parse(27..toString(36).toLowerCase().split("").map((function(t) {
    return String.fromCharCode(t.charCodeAt() + -39)
    }
    )).join("") + 24901..toString(36).toLowerCase() + 33..toString(36).toLowerCase().split("").map((function(t) {
    return String.fromCharCode(t.charCodeAt() + -39)
    }
    )).join("") + 976..toString(36).toLowerCase() + 20..toString(36).toLowerCase().split("").map((function(t) {
    return String.fromCharCode(t.charCodeAt() + -39)
    }
    )).join("") + function() {
    var t = Array.prototype.slice.call(arguments)
    , e = t.shift();
    return t.reverse().map((function(t, n) {
    return String.fromCharCode(t - e - 24 - n)
    }
    )).join("")
    }(10, 127, 154, 91, 151, 91, 136) + 11..toString(36).toLowerCase() + 13..toString(36).toLowerCase().split("").map((function(t) {
    return String.fromCharCode(t.charCodeAt() + -13)
    }
    )).join("")),
    a = s.AES.decrypt(e, n, {
    mode: s.mode.ECB,
    padding: s.pad.Pkcs7
    }),
    o = u(a),
    r = c.ungzip(o, {
    to: "string"
    }),
    t.abrupt("return", JSON.parse(r));
    case 8:
    throw t.prev = 8,
    t.t0 = t["catch"](0),
    console.log(t.t0),
    new Error("date decrypt error");
    case 12:
    case "end":
    return t.stop()
    }
    }
    ), t, null, [[0, 8]])
    }
    ))),
    m.apply(this, arguments)
    }
    主要特征为:
    1
    2
    3
    4
    5
    6
    7
    8
    a = s.AES.decrypt(e, n, {
    mode: s.mode.ECB,
    padding: s.pad.Pkcs7
    }),
    o = u(a),
    r = c.ungzip(o, {
    to: "string"
    })
    据此判断为AES解密,n为key,测试用n不会变。解密之后经过u函数再ungzip解压。可以跟进s.AES.decrypt函数查看上下文,发现是调用了CryptoJS的aes解密函数。

搞定

那么剩下就是搞个CryptoJS再搞个ungzip解压。我直接实用了CryptoJS库,ungzip实用了pako库。u函数在这段代码上面直接扣。

1
2
3
4
5
6
7
8
9
10
function u(t) {
var e, n, i = t.words.length, a = new Uint8Array(t.sigBytes), o = 0;
for (n = 0; n < i; n++)
e = t.words[n],
a[o++] = e >> 24,
a[o++] = e >> 16 & 255,
a[o++] = e >> 8 & 255,
a[o++] = 255 & e;
return a
}

然后组合js到一起,由于主程序实用python,所以直接execjs模块跑起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 此处解决 execjs 报下面错误的问题 一定写在import execjs前
# AttributeError: 'NoneType' object has no attribute 'replace'
import subprocess
from functools import partial
subprocess.Popen = partial(subprocess.Popen, encoding='utf-8')
import execjs

se = "YS2uKFD807w8u6sRgZ20YYglwYz2JvhIymEnYMQNKcudNDDLLP8+HhcOeb6zTv6btyIPjU7DlznAJts6v7LZmtVY040w80qVCQdlT0VHMDHrdLvBPJiyOsNsn2FNoC5S6E8ka8U5YM1iwuKIe6I+kGG5HMY1reCV0kyoIucnud8="
decrypt_js_file = 'd:\cmm_decrypt_aes.js'
ctx = execjs.compile(open(decrypt_js_file).read())
with open(file,'r',encoding='utf-8') as f:
js_text = f.read()
compil = execjs.compile(js_text)
cmm_decrypt = compil.call('cmm_decrypt',se)

print(cmm_decrypt)

输出为:

1
2
3
4
5
6
7
8
{
"init_cate": true,
"cate_info": { "cate_ids": "", "create_at": 0, "id": 0, "user_id": 0 },
"first_init": true,
"init_module": true,
"board_id": 0,
"module_list": []
}

经过对比和网站上的一致。

附件

  1. cmm_decrypt_aes.js