reverse
js 逆向
某东web逆向

本文将会逆向京东的 JS,分析京东请求内的 h5st 加密参数生成规则及部分 Cookie 生成

1 h5st

1.1 加密定位

进入网址:手机 - 商品搜索 - 京东 (jd.com) (opens in a new tab) 后打开浏览器 F12 进行抓包

img.png

可以看到请求都有一个 h5st 的加密参数,通过全文搜索,关键字可以是 .h5st 可以查看调用的地方,定位到加密的地方

img_1.png

1.2 算法分析

打上断点开始逐步分析发现 window.PSign.sign(e) 是一个 Promise ,也就是一个异步的处理

img_2.png

ebody 进行加密后的结果,加密方式为 sha256

img_3.png

加密原文是之前的 body

img_4.png

所以可以还原 genBody 函数

def gen_body(functionid, ts, body):
    """
 
 
    :param body: 请求参数内的 body
    :param functionid: 函数 id
    :param ts: 时间戳, 秒级
    :return:
    """
    return {
        "appid": "search-pc-java",
        "functionId": functionid,
        "client": "pc",
        "clientVersion": "1.0.0",
        "t": ts,
        "body": hashlib.sha256(body.encode()).hexdigest()
    }

根据断点步进查看 window.PSign.sign 函数

img_5.png

查看 window.PSign 这个对象,因为加密都是调用的这个对象的方法,看看有没有可以的方法!

img_6.png

点开 Prototype 查看发现里面有几个疑似函数

img_7.png

分析一下 window.PSign.__genSign

img_8.png

发现 t 是一串加密的字符串,r 是上一步加密好的 body

img_9.png

让我们来看一下代码是啥

img_10.png

代码很简单,可以看出来:t 就是一个加密的 key, 而 u 是之前的 body 进行的格式化,加密方式为 hamc

img_11.png

这边等于清楚了第一步,那么接下来要分析 t 是怎么来的,我们需要在堆栈这里找到上一步,然后打断点继续分析

img_12.png

断点下来我们可以发现是在这里调用的 __genSign 方法,主要分析 C 是怎么来的, 断点调试发现:

this[u(0, Qe, 0, $e)] ? C = this[u(0, to, 0, ro)](m, w, s, A, eI).toString() || "" : b ? C = this[a(0, 0, -no, -eo) + a(0, 0, 30, oo)](b, w, s, A) : (this._defaultToken = kf[u(0, io, 0, ao)](II, this[u(0, co, 0, uo) + "nt"]),
                        C = this["__genDefau" + u(0, fo, 0, so)](this[u(0, ee, 0, ho) + a(0, 0, -lo, -vo)], w, s, A));
 

浏览器自助解一下混淆,然后看解完混淆之后的代码:

this['_isNormal'] ? C = this['__genKey'](m, w, s, A, eI).toString() || "" : b ? C = this['__genDefaultKey'](b, w, s, A) : (this._defaultToken = II(this['_fingerprint']), C = this['__genDefaultKey'](this['_defaultToken'], w, s, A));
                

因为 this['_isNormal']true,所以 C = this['__genKey'](m, w, s, A, eI).toString() || ""

this['__genKey'] 这个函数又是来源于:

img_13.png

因此接下来要分析 m, w, s, A, eI 都是哪里来的

  • mthis._token
  • wthis._fingerprint
  • ss = XC(h, 'yyyyMMddhhmmssSSS'),跟时间相关的,后面的是他的格式
  • Athis['_appId']
  • eI:是一个加密工具,分析 this['__genKey'] 的代码发现并没有使用

所以可以还原出 genKey 的加密算法了:

def gen_key(ts):
    """
    生成hmac256 盐值
    """
 
    rd = '6vRABalFEPcI'
 
    str_ = f'{T["env"]["tk"]}{T["env"]["fingerprint"]}{ts}{appid}{rd}'
    return hashlib.md5(str_.encode()).hexdigest()

接着还原 genSign 的代码:

def gen_sign(data: dict, key):
    """
    生成加密参数
 
    :param data: hash 之后的body
    :param key: 加密的 key
    :return:
    """
    message = ""
    for k in sorted(data.keys()):
        message += f'{k}:{data[k]}'
    return hmac.new(key.encode(), message.encode('utf-8'), digestmod=sha256).hexdigest()
 

这个 data 是需要加密的 body,而那个 key 是调用 genKey 生成的 key

最后分析完了这三个关键函数,我们就要分析 h5st 了,继续分析刚刚的代码:

img_14.png

img_15.png

发现又调用了 __genSignParams 方法

img_16.png

这不就是我们需要的 h5st 吗?那我们追进去看看

  • l:是上面 gen_key 生成的结果
  • h:是一个时间戳
  • s:是时间戳的字符串形式
  • r:是函数传进来的参数,先不着急分析

先分析一下 __genSignParams 方法,发现就是进行拼接

img_17.png

解一下混淆并去掉没用的代码:

[n, this['_fingerprint'], this['_appId'], this['_token'], t, this['_version'], r, e]['join'](";")

python 还原一下

def gen_h5st(sign, ts, tstr):
    return ";".join([tstr, T["env"]["fingerprint"], appid, T["env"]["tk"], sign, version, ts, T["env"]["e"]])

通过上述分析,我们还有几个参数不知道怎么来的:

  • this['_fingerprint']
  • this['_token']
  • e

接下来我们来分析这个 e 是怎么来的,首先在刚刚那个加密的地方打上断点,然后刷新页面

img_18.png

可以看到 r 就是我们需要分析的参数

img_19.png

点到上一层堆栈查看可以知道 r 来源与这个 q, 而 q = t[i(m, 0, 718)]

img_20.png

解开混淆发现:q = t["sent"],所以我们需要去查看这个的来源,那么这个 t 又是怎么来的呢?网上查看代码发现 t 也是传入进来的,所以我们要分析一下,传入来的 t 是不是就包含了这个 sent,还是说一开始没有这个,后面走了一段代码后就有了呢?所以我们可以使用 Chrome 自带的 js 文件替换进行插桩输出看看!

img_21.png

插桩之后查看日志可以知道,有时候有,有时候没有

img_22.png

我们继续在上层堆栈中插桩,输出所有的 n

img_23.png

刷新页面后查看日志

img_24.png

发现这个加密参数出现的时候有一个这样的日志

img_25.png

也就是当 sent{sua: 'Macintosh; Intel Mac OS X 10_15_7', pp: {…}, random: 'GFfcGJW9IV', referer: 'https://www.jd.com/', v: 'v3.2.1', …} 的时候,他的 rval 就是我们需要的值,其实熟知 JavaScript 的应该知道,这是 js 中常见的异步手段,通过 promise 实现的,sent 就是参数,rval 就是返回值!所以这个参数肯定和这条日志是有关系的!

我们接着插桩,当 sent{sua: 'Macintosh; Intel Mac OS X 10_15_7', pp: {…}, random: 'GFfcGJW9IV', referer: 'https://www.jd.com/', v: 'v3.2.1', …} 的时候,我们就断下来,然后逐步分析

img_26.png

断下来了,这一步还没有生成我们需要的加密参数,所以我们先在下一行打一个断点,放过这个断点后,再分析下一步的执行流程

img_27.png

此时放过去之后即将调用 t 函数,那么这个 t 函数是哪个呢?我们可以在 console 上输出一下

img_28.png

发现他调用的是这个函数,所以我们可以进入到这个函数里面,分析一下,进去发现是一段分支语句

for (; ; ){
switch (j.prev = j[N(0, t, 0, r)]) {
    case 0:
        return j[N(0, 460, 0, 444)] = 2,
        I[N(0, 662, 0, n)](KI, 1);
    case 2:
        return (M = j.sent).fp = this[N(0, e, 0, o) + "nt"],
        O = JSON.stringify(M, null, 2),
        this[N(0, 478, 0, i)](I.aDGtR[N(0, a, 0, c)](O)),
        L = eI[P(-19, 0, 0, -u)].encrypt(O, eI.enc[P(f, 0, 0, s)][P(h, 0, 0, 184)](["wm", I[N(0, l, 0, 326)], "w_", I[N(0, v, 0, p)], I[P(d, 0, 0, 46)], "o("][P(-y, 0, 0, -d)]("")), {
            iv: eI[N(0, 379, 0, g)][P(f, 0, 0, -x)][N(0, 526, 0, _)](["01", "02", "03", "04", "05", "06", "07", "08"].join("")),
            mode: eI[P(-m, 0, 0, b)][N(0, w, 0, A)],
            padding: eI[N(0, C, 0, S)][P(-10, 0, 0, -B)]
        }),
        j[N(0, 339, 0, z)](I.KWfsh, L.ciphertext[P(D, 0, 0, k)]());
    case 8:
    case I[N(0, E, 0, 425)]:
        return j[P(-99, 0, 0, -T)]()
    }
}
                           

我们先解一下混淆

for (; ; ){
switch (j.prev = j['next']) {
    case 0:
        return j['next'] = 2,
        I['fMHMe'](KI, 1);
    case 2:
        return (M = j.sent).fp = this['_fingerprint'],
        O = JSON.stringify(M, null, 2),
        this['_log']('__collect envCollect='['concat'](O)),
        L = eI['AES'].encrypt(O, eI.enc['Utf8']['parse']('wm0!@w_s#ll1flo('), {
            iv: eI['enc']['Utf8']['parse']('0102030405060708'),
            mode: eI['mode']['CBC'],
            padding: eI['pad']['Pkcs7']
        }),
        j['abrupt'](I.KWfsh, L.ciphertext['toString']());
    case 8:
    case 'end':
        return j['stop']()
    }
}
                           

就可以很清晰的看到一个 AES 加密了,我们可以手动再 console 上调用一下,看看是不是我们需要的结果,发现断点不好打,就直接插桩输出看看吧,插桩代码如下:

(M = j.sent).fp = this[N(0, e, 0, o) + "nt"];
O = JSON.stringify(M, null, 2)
L = eI[P(-19, 0, 0, -u)].encrypt(O, eI.enc[P(f, 0, 0, s)][P(h, 0, 0, 184)](["wm", I[N(0, l, 0, 326)], "w_", I[N(0, v, 0, p)], I[P(d, 0, 0, 46)], "o("][P(-y, 0, 0, -d)]("")), {
    iv: eI[N(0, 379, 0, g)][P(f, 0, 0, -x)][N(0, 526, 0, _)](["01", "02", "03", "04", "05", "06", "07", "08"].join("")),
    mode: eI[P(-m, 0, 0, b)][N(0, w, 0, A)],
    padding: eI[N(0, C, 0, S)][P(-10, 0, 0, -B)]
})
 
console.log("key: ", ["wm", I[N(0, l, 0, 326)], "w_", I[N(0, v, 0, p)], I[P(d, 0, 0, 46)], "o("][P(-y, 0, 0, -d)](""))
console.log("iv: ", ["01", "02", "03", "04", "05", "06", "07", "08"].join(""))
console.log("mode: ", eI[P(-m, 0, 0, b)][N(0, w, 0, A)])
console.log("padding: ", eI[N(0, C, 0, S)][P(-10, 0, 0, -B)])
console.log("msg: ", O)
console.log("result: ", L.ciphertext[P(D, 0, 0, k)]())
 
 
return j[N(0, 339, 0, z)](I.KWfsh, L.ciphertext[P(D, 0, 0, k)]())

img_29.png

因此就可以得到我们需要的参数 e 了,从上述分析步骤我们可以大概看出来加密方式是 aes CBC Pkcs7 ,我们使用现成的工具验证一下

img_30.png

没有问题,所以这个加密参数已经还原了!接下来还有两个参数:_fingerprinttoken,这两个参数我们通过分析请求可以看到其实是来源于请求里的:

img_31.png

但是这个请求却又有一段加密参数

img_32.png

因此我们分析完 expandParams 之后基本所有的参数就全部分析完毕了,我们直接全局搜索 expandParams,找到了这么一个地方

img_33.png

所以继续查看代码分析一下这个参数的来源,发现其来源于:$ = t[K(0, 970, 0, B)],打上断点继续分析

img_34.png

解混淆发现其来自于 t['env'],那么这个 t 又是怎么来的呢 ?根据堆栈往上层找

img_35.png t 就是 this['_onRequestTokenRemotely'],而 t['env'] 来源于 hh 又来源于 s.ciphertext.toString()

img_36.pngs 来源于下面这一行代码

s = eI[e(M, 366)][Xt(484, 370)](f, eI[Xt(594, O)].Utf8[Xt(L, R)](["wm", n[Xt(P, N)], "w-", n[Xt(504, W)], n[e(H, q)], "o("].join("")), {
                                    iv: eI[Xt(F, U)][e(G, K)][e(V, Y)](["01", "02", "03", "04", "05", "06", "07", "08"][e(A, J)]("")),
                                    mode: eI[Xt(X, Z)][e(567, $)],
                                    padding: eI[e(285, tt)][Xt(rt, nt)]
                                })

解一下混淆

s = eI['AES']['encrypt'](f, eI['enc'].Utf8['parse']('wm0!@w-s#ll1flo('), {
                                    iv: eI['enc']['Utf8']['parse']('0102030405060708'),
                                    mode: eI['mode']['CBC'],
                                    padding: eI['pad']['Pkcs7']
})

很简单就可以看出来又是一个 AES 加密,我们可以看看加密的 f 是什么

img_37.png 发现是一些浏览器的信息!使用通用的工具验证一下,发现也没有问题

img_38.png 到目前为止,我们需要的所有参数全部分析完毕,可以愉快的生成 h5st 了,读者可以总结归纳一下加密流程,这边就不做太多的介绍了,附上完整的 Python 还原代码

# -*- coding: utf-8 -*-
# @Time    : 2023-05-31 10:38
# @Author  : Kem
# @Desc    : 生成加密参数 h5st
# -*- encoding: utf-8 -*-
import datetime
import hashlib
import hmac
import json
import time
from hashlib import sha256
 
import requests
from lego.state import T
from lego.utils import fake
from lego.utils.crypto import AES
from loguru import logger
 
appid = 'f06cc'
version = '3.1'
key1 = "wm0!@w-s#ll1flo("
key2 = "wm0!@w_s#ll1flo("
iv = "0102030405060708"
 
 
def gen_key(ts):
    """
    生成hmac256 盐值
    """
 
    rd = '6vRABalFEPcI'
 
    str_ = f'{T["env"]["tk"]}{T["env"]["fingerprint"]}{ts}{appid}{rd}'
    return hashlib.md5(str_.encode()).hexdigest()
 
 
def gen_sign(data: dict, key):
    """
    生成加密参数
 
    :param data: hash 之后的body
    :param key: 加密的 key
    :return:
    """
    message = ""
    for k in sorted(data.keys()):
        message += f'{k}:{data[k]}'
    return hmac.new(key.encode(), message.encode('utf-8'), digestmod=sha256).hexdigest()
 
 
def gen_body(functionid, ts, body):
    """
 
 
    :param body: 请求参数内的 body
    :param functionid: 函数 id
    :param ts: 时间戳, 秒级
    :return:
    """
    return {
        "appid": "search-pc-java",
        "functionId": functionid,
        "client": "pc",
        "clientVersion": "1.0.0",
        "t": ts,
        "body": hashlib.sha256(body.encode()).hexdigest()
    }
 
 
def gen_h5st(sign, ts, tstr):
    return ";".join([tstr, T["env"]["fingerprint"], appid, T["env"]["tk"], sign, version, ts, T["env"]["e"]])
 
 
def init_env():
    while True:
        # 0102030405060708
        fake_fp = fake.gen_random_str(16, "0123456789")
        local_env = {
            "wc": 0,
            "wd": 0,
            "l": "zh-CN",
            "ls": "zh-CN",
            "ml": 2,
            "pl": 5,
            "av": "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35",
            "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35",
            "sua": "Macintosh; Intel Mac OS X 10_15_7",
            "pp": {},
            "pp1": "jsavif=0; jsavif=0; __jda=122270672.1685523235636810059383.1685523236.1685523236.1685523236.1; __jdc=122270672; __jdv=122270672|direct|-|none|-|1685523235637; shshshfpa=1fba7094-cf40-400d-972a-11138fdc6ca9-1685523235; shshshfpx=1fba7094-cf40-400d-972a-11138fdc6ca9-1685523235; shshshfpb=pAiDkfHR84jPVp-gmv-FY8g; areaId=18; ipLoc-djd=18-1482-0-0; rkv=1.0; qrsc=3; __jdb=122270672.11.1685523235636810059383|1.1685523236",
            "pm": {
                "ps": "prompt",
                "np": "default"
            },
            "w": 1920,
            "h": 1080,
            "ow": 1920,
            "oh": 1055,
            "url": "https://search.jd.com/Search?keyword=%E6%89%8B%E6%9C%BA&wq=%E6%89%8B%E6%9C%BA&pvid=8858151673f941e9b1a4d2c7214b2b52&page=5&s=116&click=0",
            "og": "https://search.jd.com",
            "pr": 1,
            "re": "",
            "random": "bf35MB70a6",
            "referer": "",
            "v": "v3.2.1",
            "ai": appid,
            "fp": fake_fp
        }
        expand_params = AES.CBC.encrypt(
            msg=json.dumps(
                local_env,
                indent=2,
                ensure_ascii=False
            ),
            key=key1,
            iv=iv,
            _output="hex"
        )
        url = "https://cactus.jd.com/request_algo?g_ty=ajax"
        payload = json.dumps({
            "version": "3.1",
            "fp": fake_fp,
            "appId": appid,
            "timestamp": int(time.time() * 1000),
            "platform": "web",
            "expandParams": expand_params.ciphertext,
            "fv": "v3.2.1"
        })
        headers = {
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'DNT': '1',
            'Origin': 'https://search.jd.com',
            'Pragma': 'no-cache',
            'Referer': 'https://search.jd.com/',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-site',
            'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35',
            'accept': 'application/json',
            'content-type': 'application/json',
            'sec-ch-ua': '"Microsoft Edge";v="113", "Chromium";v="113", "Not-A.Brand";v="24"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"macOS"',
            'Cookie': 'ipLoc-djd=18-1482-48937-0'
        }
 
        response = requests.request("POST", url, headers=headers, data=payload)
        data = response.json()
 
        if data.get('status') == 200:
            logger.info(f"初始化环境成功, 新的 fingerprint 为: {data['data']['result']['fp']}")
            env = {
                "sua": "Macintosh; Intel Mac OS X 10_15_7",
                "pp": {},
                "random": fake.gen_random_str(10),
                "referer": "https://www.jd.com/",
                "v": "v3.2.1",
                "fp": data['data']['result']['fp']
            }
            e = AES.CBC.encrypt(
                msg=json.dumps(
                    local_env,
                    indent=2,
                    ensure_ascii=False
                ),
                key=key2,
                iv=iv,
                _output="hex"
            )
            # e = crypto.call("AES.encrypt", json.dumps(env, ensure_ascii=False, indent=2), key, iv)
            T["env"] = {
                "fingerprint": data['data']["result"]['fp'],
                "tk": data['data']["result"]['tk'],
                "e": e.ciphertext,
            }
            return
 
        else:
            logger.warning(f"初始化环境失败, 等待重试")
            time.sleep(60)
 
 
def encrypt(functionid, body, ts=None):
    """
    获取 h5st
 
    :param functionid:
    :param body:
    :param ts:
    :return:
    """
    if not T["env"]: init_env()
 
    ts = ts or time.time()
    if len(str(ts)) == 13: ts = int(ts) / 1000
    # ts = 1685500202335 / 1000
    tstr = datetime.datetime.fromtimestamp(ts).strftime('%Y%m%d%H%M%S%f')[:-3]
    # tstr = "20230531092616337"
    body = gen_body(functionid, int(ts * 1000), body)
    key = gen_key(tstr)
    sign = gen_sign(body, key)
 
    return {
        "h5st": gen_h5st(sign, str(int(ts * 1000)), tstr),
        "ts": str(int(ts * 1000))
    }

2 Cookie

分析完 h5st 的算法,下面我们继续分析一下京东的 Cookie 生成规则,因为京东接口请求成功率还是和 Cookie 有一点点关系的,分析的 Cookie 都选取的算法生成的,至于请求生成的,那就自己抓包分析吧,本文分析的 Cookie 如下:

  • __jda
  • __jdb
  • __jdc
  • __jdv
  • shshshfpx
  • shshshfpa
  • mba_muid
  • mba_sid

分析 Cookie 生成规则可以借助一个油猴脚本,可以很快速的定位位置,地址如下:JS Cookie Monitor/Debugger Hook (greasyfork.org) (opens in a new tab)

装完这个脚本之后,你可以明确的在控制台看到所有 Cookie 操作了,真的很 nice

img_39.png 下面我们拿 ___jda 举例,看看是如何分析的, 首先我们先把 Cookie 全部清空,然后刷新浏览器,就可以看到新增了的(绿色的就是新增的)

img_40.png 这边还能指向是在哪里执行的,我们点进去后打上断点,清除 Cookie 了重来一次

img_41.png 找前面的堆栈

img_42.png 看到了设置的地方,可以看到 __jda 来源于下面这一行代码

[(e = ce).get(f), e.get(h) || "-", e.get(A) || "-", e.get(L) || "-", e.get(D) || "-", e.get(W) || 1].join(".")

img_43.png 好像是从一个对象里面取的值,我们看看是哪个对象

img_44.png 发现其实就是 e 这个对象,因此我们需要知道 e 是怎么设置的值

[(e = ce).get('2'), e.get('1') || "-", e.get('22') || "-", e.get('23') || "-", e.get('24') || "-", e.get('25') || 1].join(".")

我们可以在他的 set 方法插桩,看一下设置流程

img_45.png 清除 Cookie, 刷新页面,查看日志

img_46.png 运行打上断点,开始查看

  • 0this.set(g, "UA-J2011-1");, 所以 0 一直都是 "UA-J2011-1"
  • 3this.set(p, t)t 来源于 window.document.domain.indexOf("360buy") >= 0 ? "360buy.com" : "jd.com";
  • 2this.set(f, Q()),来源与 Q()
re = function(e) {
        var t, r = 1, n = 0;
        if (!te(e))
            for (r = 0,
            t = e[a] - 1; t >= 0; t--)
                n = e.charCodeAt(t),
                r = (r << 6 & 268435455) + n + (n << 14),
                n = 266338304 & r,
                r = 0 != n ? r ^ n >> 21 : r;
        return r
    }
 
function() {
        return re(r.domain)
    }
  • 4this.set(m, Math.round((new Date).getTime() / 1e3)), 时间戳
  • 5this.set(v, 15552e6), 常量,15552000000
  • 6this.set(w, 1296e6),常量,1296000000
  • 7this.set(_, 18e5),常量,1800000
  • 13this.set(C, ee());,代码一堆,但是实际就是返回 '-'
  • 14this.set(x, r.name),浏览器名称
  • 15this.set(N, r.version),浏览器版本
  • 16this.set(O, ie());,系统名称
  • 17this.set(R, n.D) ,分辨率
  • 18this.set(M, n.C),位数
  • 19this.set(T, n.language), 语言
  • 20this.set(I, n.javaEnabled), 是否运行 JavaScript
  • 21his.set(J, n.characterSet)characterSet
  • 27this.set(H, X)X是浏览器中定义的一个数组
  • 32this.set(B, (new Date).getTime()), 时间戳
  • 35this.set(Y, i[a] ? i[0] : "-"), 其实就是 -
  • 36this.set(F, s || "-"),其实就是 -
  • 8e.set(b, r.location.hostname),当前网站的 hostname
  • 9e.set(j, r.title.replace(/\$/g, "")),当前网址标题
  • 10e.set(y, r.location.pathname),网址 path
  • 11e.set(k, r.referrer.replace(/\$/g, ""))referrer 地址
  • 12e.set(S, r.location.href),当前网址
  • 1K(),时间戳 + 随机数
  • 22:取之前的 4
  • 23:取之前的 4
  • 24:取之前的 4
  • 25:恒为 1
  • 28direct
  • 29-
  • 30none
  • 31-
  • 260
  • 33-
  • 340

分析完这个对象怎么生成的就很容易分析出我们需要的 Cookie 是怎么生成的了!这边不分析了,附上 Python 版本算法

# -*- coding: utf-8 -*-
# @Time    : 2023-06-05 17:54
# @Author  : Kem
# @Desc    : 生成京东一些根据 js 生成的 Cookie
import json
import random
import time
from urllib.parse import urlparse
 
import requests
from lego.utils import fake, crypto
 
 
def ct(t):
    return t is None or t == "-" or t == ""
 
 
def ut(t):
    n = 1
    if not ct(t):
        for e in range(len(t) - 1, -1, -1):
            char_code = ord(t[e])
            n = (n << 6 & 268435455) + char_code + (char_code << 14)
            r = 266338304 & n
            n = n ^ r >> 21 if r != 0 else n
    return n
 
 
def parse_domain(url: str):
    # 解析URL,获取域名和子域名
    parsed_url = urlparse(url)
    domain_parts = parsed_url.hostname.split('.')
    if len(domain_parts) >= 2:
        return '.'.join(domain_parts[-2:])
    else:
        return parsed_url.hostname
 
 
def gen_cookie(url: str):
    domain = parse_domain(url)
    timestamp = int(time.time() * 1000)
    t1, t2, t3 = ut(domain), f'{timestamp}{fake.gen_random_str(10, base_str="0123456789")}', str(timestamp // 1000)
    jda = ".".join([str(t1), t2, t3, t3, t3, "1"])
    jdb = ".".join([str(t1), "1", f'{t2}|1', t3])
    jdc = ".".join([str(t1)])
    jdv = "|".join([str(t1), "direct", "-", "none", "-", str(timestamp)])
    shshshfpx = generate_uuid()
    shshshfpa = generate_uuid()
    mba_muid = f'{timestamp}{fake.gen_random_str(10, base_str="0123456789")}'
    mba_sid = f'{timestamp}{fake.gen_random_str(16, base_str="0123456789")}.0'
    # ; shshshfpb={get_shshshfpb()}
    return "; ".join([
        f'__jda={jda}',
        f'__jdb={jdb}',
        f'__jdc={jdc}',
        f'__jdv={jdv}',
        f'shshshfpx={shshshfpx}',
        f'shshshfpa={shshshfpa}',
        f'mba_muid={mba_muid}',
        f'mba_sid={mba_sid}',
    ])
 
 
def generate_uuid():
    uuid = ''
    for i in range(1, 33):
        random_num = random.randint(0, 15)
        uuid += hex(random_num)[2:]
        if i in [8, 12, 16, 20]:
            uuid += '-'
    now = time.time()
    uuid += '-' + str(int(now * 1000 / 1000))
    return uuid
 
 
def get_shshshfpb(shshshfpa=None):
    """
    这个获取 Cookie 的暂时用不上
 
 
    :param shshshfpa:
    :return:
    """
    shshshfpa = shshshfpa or generate_uuid()
    body = {
        "appname": "jdwebm_hf",
        "jdkey": "",
        "isJdApp": False,
        "jmafinger": "",
        "whwswswws": "",
        "businness": "jshopx",
        "body": {
            "browser_info": crypto.Hash.md5(shshshfpa),
            "client_time": int(time.time() * 1000),
            "period": 24,
            "shshshfpa": shshshfpa,
            "ecflag": "n",
            "whwswswws": "",
            "jdkey": "",
            "isJdApp": False,
            "jmafinger": "",
            "cookie_pin": "",
            "jdu": "",
            "mba_muid": "",
            "visitkey": "",
            "msdk_version": "2.5.0",
            "wid": "",
            "lan": "zh-CN",
            "scrh": 1080,
            "scrah": 1055,
            "scrw": 1920,
            "scaw": 1920,
            "oscpu": "",
            "platf": "MacIntel",
            "pros": "20030107",
            "temp": 33,
            "hll": False,
            "hlr": False,
            "hlo": False,
            "hlb": False,
            "color_depth": 24,
            "pixel_ratio": 1,
            "resolution": "1920;1080",
            "available_resolution": "1920;1055",
            "session_storage": 1,
            "local_storage": 1,
            "indexed_db": 1,
            "open_database": 1,
            "cpu_class": "unknown",
            "navigator_platform": "MacIntel",
            "regular_plugins": "PDF Viewer::Portable Document Format::application/pdf~pdf,text/pdf~pdf;Chrome PDF Viewer::Portable Document Format::application/pdf~pdf,text/pdf~pdf;Chromium PDF Viewer::Portable Document Format::application/pdf~pdf,text/pdf~pdf;Microsoft Edge PDF Viewer::Portable Document Format::application/pdf~pdf,text/pdf~pdf;WebKit built-in PDF::Portable Document Format::application/pdf~pdf,text/pdf~pdf",
            "adblock": False,
            "touch_support": 0,
            "app_code_name": "Mozilla",
            "app_name": "Netscape",
            "app_version": "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.35",
            "cookie_enabled": True,
            "regular_mimetypes": "::Portable Document Format::;::Portable Document Format::",
            "online": "unknown",
            "hardwareConcurrency": 8,
            "product": "Gecko",
            "vendor": "Google Inc.",
            "vendorSub": "unknown",
            "devicePixelRatio": 1,
            "updateInterval": "unknown",
            "orientationType": "landscape-primary",
            "doNotTrack": "1",
            "canvas": "canvas winding:yes~canvas fp:518a43b5a0f974add394ce72abb7b18b",
            "webgl": "fp:02ffb1efaaefd7e35acf063246d0e907~extensions:ANGLE_instanced_arrays;EXT_blend_minmax;EXT_color_buffer_half_float;EXT_disjoint_timer_query;EXT_float_blend;EXT_frag_depth;EXT_shader_texture_lod;EXT_texture_compression_rgtc;EXT_texture_filter_anisotropic;EXT_sRGB;KHR_parallel_shader_compile;OES_element_index_uint;OES_fbo_render_mipmap;OES_standard_derivatives;OES_texture_float;OES_texture_float_linear;OES_texture_half_float;OES_texture_half_float_linear;OES_vertex_array_object;WEBGL_color_buffer_float;WEBGL_compressed_texture_s3tc;WEBGL_compressed_texture_s3tc_srgb;WEBGL_debug_renderer_info;WEBGL_debug_shaders;WEBGL_depth_texture;WEBGL_draw_buffers;WEBGL_lose_context;WEBGL_multi_draw~aliased line width range:[1, 1]~aliased point size range:[1, 64]~alpha bits:8~antialiasing:yes~blue bits:8~depth bits:24~green bits:8~max anisotropy:16~max combined texture image units:32~max cube map texture size:16384~max fragment uniform vectors:1024~max render buffer size:16384~max texture image units:16~max texture size:16384~max varying vectors:31~max vertex attribs:16~max vertex texture image units:16~max vertex uniform vectors:1024~max viewport dims:[16384, 16384]~red bits:8~renderer:WebKit WebGL~shading language version:WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)~stencil bits:0~vendor:WebKit~version:WebGL 1.0 (OpenGL ES 2.0 Chromium)~unmasked vendor:Google Inc. (Apple)~unmasked renderer:ANGLE (Apple, Apple M1, OpenGL 4.1)~vertex high float:23(127,127)~vertex medium float:23(127,127)~vertex low float:23(127,127)~fragment high float:23(127,127)~fragment medium float:23(127,127)~fragment low float:23(127,127)~vertex high int:0(31,30)~vertex medium int:0(31,30)~vertex low int:0(31,30)~fragment high int:0(31,30)~fragment medium int:0(31,30)~fragment low int:0(31,30)",
            "device_memory": 8,
            "is_headless_browser": 0
        }
    }
 
    url = "https://blackhole-m.m.jd.com/getinfo"
    with requests.post(url=url, data={"body": json.dumps(body, ensure_ascii=False)}) as res:
        return "; ".join([f'{k}={v}' for k, v in res.cookies.items()])
 
 
def get_shshshs_id(shshshfpa):
    return f'{crypto.Hash.md5(shshshfpa)}_1_{int(time.time() * 1000)}'
 
 
if __name__ == '__main__':
    print(gen_cookie("https://mall.jd.com/view_search-472889-0-0-1-24-3.html"))
    # print(get_shshshs_id(generate_uuid()))