资讯详情

某东h5st逆向实战:webpack签名参数定位与Python复现

📅 2026/10/11 13:54:59 | 华诺云谱 👁 阅读
某东h5st逆向实战:webpack签名参数定位与Python复现
简介这份资源面向具备一定前端基础、希望深入理解移动端加密参数生成机制的爬虫学习者与安全测试人员围绕某东平台webpack打包方式下的h5st逆向分析提供一套可运行的完整代码示例。压缩包共2个文件包含1个Python脚本与1个JavaScript文件分别承担请求逻辑与前端加密逻辑的还原整体约159KB体积轻量便于快速阅读与调试。目前已有855人学习下载说明该方向在爬虫与逆向圈内关注度较高。读者可从中获取webpack模块加载与打包结构的分析思路、h5st相关参数的定位与还原方法以及Python侧对接加密逻辑的调用示例适合作为理解前端混淆与接口参数构造的实战参考。需要提醒的是相关技术应仅用于合法合规的学习与研究尊重平台规则与版权边界。1. 某东 h5st 逆向webpack 打包的签名参数到底难在哪打开某东 H5 页面抓包会看到一个叫h5st的参数它随请求一起提交缺了它或者算错了接口直接返回风控错误。很多人第一次接触会以为这是个简单的 MD5 或时间戳拼接结果把 JS 翻了一遍才发现整个签名逻辑被 webpack 打包进了压缩混淆的 chunk 里函数名全是单字母调用链藏在模块加载器内部。这就是 h5st 逆向的核心难点不是算法本身多复杂而是你要先学会在 webpack 的模块体系里找到那个真正干活的函数。这篇内容面向的是有基本 JS 逆向经验、能看懂浏览器调试器、想把这套签名逻辑用代码复现出来的工程师。我会从 webpack 打包结构讲起一步步定位 h5st 的生成入口把关键参数拆出来最后给出一份可运行的 Python 复现代码。整个过程不依赖任何现成的第三方破解库所有逻辑都从源码里抠出来。需要提前说明的是这类签名算法会随版本更新而变化今天能跑通的代码过几个月可能需要重新对参数所以重点在于掌握定位和还原的方法而不是死记某一份代码。2. webpack 模块体系与 h5st 入口定位2.1 webpack 打包后的代码长什么样webpack 打包出来的 JS 文件最外层通常是一个立即执行函数接收一个模块数组或模块对象作为参数。每个模块被包在一个函数里通过__webpack_require__来加载。压缩之后模块 ID 可能变成数字导出变量名变成n、r、t这种单字母。你直接在源码里搜h5st大概率搜不到因为字符串可能被拆开拼接或者经过十六进制转义。常见做法是先在浏览器里抓一个完整的请求把h5st的值复制下来然后在 Sources 面板里全局搜索这个值的一部分。如果搜不到就搜h5st这个 key 名通常能找到赋值的地方。找到赋值点之后往上追调用栈看这个值是从哪个函数返回的。这一步是整个逆向的起点也是最耗时间的环节。我一般会先把目标 JS 文件下载到本地用js-beautify格式化一遍再用编辑器打开。格式化之后模块结构会清晰很多虽然变量名还是乱的但至少能看清函数边界。下面这条命令是常用的格式化方式# 安装 js-beautify npm install -g js-beautify # 格式化目标文件输出到新文件 js-beautify target.js -o target.beauty.js # 如果文件很大可以只格式化前 5000 行先看结构 head -n 5000 target.js | js-beautify - part.beauty.js格式化的作用是把压缩成一行的代码展开让每个语句独立成行。参数-o指定输出文件不指定则打印到终端。对于几 MB 的文件建议先截取一部分分析避免编辑器卡死。格式化之后搜索h5st关键字如果还是没有就搜sign、token、fp这类常见签名相关词。2.2 在调用栈里锁定签名函数假设你已经在某个请求的发起处打上了断点或者用 XHR 断点拦截到了请求。在 Call Stack 里从下往上翻找到第一个看起来像在拼字符串或者做加密的函数。通常这个函数会接收几个参数比如请求的 URL、body、时间戳然后返回一个长字符串。这个返回值就是 h5st。定位到函数之后不要急着把整个函数抄下来。先看它内部调用了哪些其他函数把调用关系画出来。webpack 模块之间通过__webpack_require__引用你可以在控制台里手动调用这些模块来验证。比如在断点处控制台输入__webpack_require__(模块ID)看返回什么。模块 ID 可以从函数定义处往上找通常形如__webpack_require__(12345)。下面是一个典型的 webpack 模块引用结构格式化后大概长这样// 模块定义 (function(module, exports, __webpack_require__) { var _utils __webpack_require__(10086); var _crypto __webpack_require__(10087); function generateSign(params) { var timestamp Date.now(); var raw _utils.concatParams(params) timestamp; return _crypto.sha256(raw); } module.exports { generateSign: generateSign }; })这段代码里__webpack_require__(10086)和__webpack_require__(10087)就是依赖的其他模块。你要做的是顺着这些依赖往下挖直到找到最底层的加密实现。如果加密用的是标准库比如 CryptoJS那复现起来就简单如果是自定义的混淆算法就需要逐行翻译。提示在浏览器控制台里__webpack_require__通常挂在全局对象上但有些打包配置会把它隐藏起来。如果直接访问不到可以在断点处的作用域链里找或者手动在源码里搜索__webpack_require__的定义。2.3 把关键参数从闭包里捞出来h5st 的生成通常依赖几个动态参数时间戳、随机数、请求体的哈希、以及一个固定的密钥。这些参数可能分散在不同的闭包里你需要把它们一个个揪出来。时间戳一般来自Date.now()或new Date().getTime()随机数可能是Math.random()或者更复杂的种子生成器。请求体的哈希通常是对排序后的参数做 MD5 或 SHA256。固定密钥是最麻烦的它可能硬编码在某个模块里也可能通过接口动态下发。如果是硬编码格式化后搜一长串十六进制字符串或者 Base64 字符串往往能找到。如果是动态下发就需要先请求某个配置接口把密钥拿到再算签名。这一步没有捷径只能靠耐心和调试器。我习惯在断点处把关键变量都打印出来对比几次请求的差异。比如同一个接口连续请求两次看哪些参数变了哪些没变。变的一般是时间戳和随机数不变的就是固定密钥或者盐值。把不变的参数记下来变的参数找到生成逻辑整个签名算法就还原得差不多了。3. 用 Python 复现 h5st 签名从抠代码到跑通请求3.1 把 JS 加密逻辑翻译成 Python假设你已经定位到了核心函数它大概做了这么几件事收集参数、按 key 排序、拼接成字符串、加上时间戳和随机数、做一次 MD5、再做一次自定义的位运算、最后 Base64 编码。翻译成 Python 的时候要注意 JS 和 Python 在字符串编码、数字精度、位运算上的差异。下面是一段典型的 JS 签名逻辑我把它简化了一下function sign(params) { var keys Object.keys(params).sort(); var str ; for (var i 0; i keys.length; i) { str keys[i] params[keys[i]] ; } str str.slice(0, -1); var ts Date.now().toString(); var rand Math.random().toString(36).slice(2, 10); var raw str ts ts rand rand key固定密钥; return md5(raw) ts rand; }对应的 Python 实现import hashlib import time import random import string def generate_sign(params): # 按 key 排序并拼接 sorted_keys sorted(params.keys()) parts [] for k in sorted_keys: parts.append(f{k}{params[k]}) str_to_sign .join(parts) # 时间戳JS 的 Date.now() 是毫秒 ts str(int(time.time() * 1000)) # 随机数模拟 JS 的 Math.random().toString(36) rand .join(random.choices(string.ascii_lowercase string.digits, k8)) # 拼接固定密钥 raw f{str_to_sign}ts{ts}rand{rand}key固定密钥 # MD5 哈希 md5_hash hashlib.md5(raw.encode(utf-8)).hexdigest() return f{md5_hash}{ts}{rand} # 测试 params {skuId: 100012043978, num: 1} print(generate_sign(params))这段代码的关键点在于time.time() * 1000取毫秒时间戳和 JS 的Date.now()对齐random.choices生成随机字符串虽然和 JS 的Math.random().toString(36)不完全等价但大多数场景下服务端只校验格式和哈希不校验随机数的具体生成方式。如果服务端校验严格就需要用execjs直接调用 JS 代码而不是纯 Python 翻译。参数说明params是请求的业务参数ts是毫秒时间戳rand是 8 位随机字符串key是硬编码的固定密钥。MD5 之后拼接时间戳和随机数是为了让每次签名都不同防止重放。3.2 用 execjs 直接跑 JS 代码如果签名逻辑太复杂纯 Python 翻译容易出错更稳妥的做法是把抠出来的 JS 代码保存成文件用execjs调用。这样能保证加密结果和浏览器完全一致。import execjs # 把抠出来的 JS 代码保存为 sign.js with open(sign.js, r, encodingutf-8) as f: js_code f.read() # 创建执行环境 ctx execjs.compile(js_code) # 调用 JS 函数 params {skuId: 100012043978, num: 1} result ctx.call(generateSign, params) print(result)execjs需要一个 JS 运行时常见的是 Node.js。安装方式pip install PyExecJS然后确保系统里有node命令。如果 JS 代码里用到了浏览器特有的 API比如window、document、navigator需要在 JS 文件开头做兼容处理比如var window global;或者手动补上缺失的对象。注意execjs调用 JS 的性能比纯 Python 低如果签名频率很高建议还是翻译成 Python。另外JS 代码里的console.log在execjs里不会输出调试时可以改成return或者写入文件。3.3 把签名塞进请求里跑通一次调用拿到签名之后下一步是构造完整的请求。某东 H5 的接口通常需要带上h5st、Cookie、User-Agent等头部。用requests库发请求的示例import requests import json def build_h5st(params): # 这里调用前面实现的签名函数 return generate_sign(params) url https://api.m.jd.com/client.action # 业务参数 body { functionId: skuDetail, body: json.dumps({skuId: 100012043978}), appid: jd-cphdeveloper-m } # 生成 h5st h5st build_h5st(body) body[h5st] h5st headers { User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15, Referer: https://item.m.jd.com/, Content-Type: application/x-www-form-urlencoded } resp requests.post(url, databody, headersheaders) print(resp.status_code) print(resp.text[:500])这段代码里functionId是接口名body是业务参数序列化后的字符串appid是固定值。h5st的计算需要包含这些参数具体哪些参数参与签名取决于你逆向出来的逻辑。请求头里的User-Agent和Referer也很关键缺失或者不对会被风控拦截。跑通一次之后把返回结果和浏览器里的对比确认数据一致。如果返回风控错误检查三个地方签名参数是否完整、时间戳是否在有效期内、请求头是否和浏览器一致。常见问题是时间戳偏差太大服务端允许的误差通常是几分钟本地时间不准的话需要校准。4. h5st 逆向避坑那些让我熬夜的翻车现场4.1 搜不到 h5st 字符串全局搜索也没结果现象在 Sources 面板里搜h5st搜不到任何赋值语句搜签名值的一部分也搜不到。原因字符串被拆分了比如h5 st或者被转成了 Unicode 转义\u0068\u0035\u0073\u0074。还有一种可能是签名逻辑在 Web Worker 里不在主线程的 JS 文件里。解决搜单个字符h5或者st范围太大就加上引号搜h5。如果是 Unicode 转义搜\u0068。Web Worker 的话在 Sources 面板的 Threads 里切换过去。我遇到过一次签名逻辑在一个单独的 worker.js 里主线程只负责发消息找了半天才反应过来。4.2 本地算出来的签名和浏览器不一致现象同样的参数Python 算出来的 h5st 和浏览器里抓到的值不一样接口返回签名错误。原因最常见的是时间戳精度问题JS 的Date.now()是毫秒Python 的time.time()是秒忘了乘 1000。其次是字符串编码JS 默认 UTF-8Python 如果用了错误的编码也会导致 MD5 不同。还有一种情况是参数排序规则不一致JS 的sort()默认按 Unicode 码点排序Python 的sorted()默认也是但如果 key 里有数字和字母混合排序结果可能不同。解决把两边的中间结果都打印出来逐段对比。先对比拼接后的原始字符串再对比 MD5 之前的值。如果原始字符串一致但 MD5 不同检查编码。如果原始字符串不同检查排序和拼接格式。我一般会在 JS 里加console.log(raw)在 Python 里加print(raw)两边一对比问题立刻暴露。4.3 execjs 调用报错提示 window is not defined现象用execjs跑抠出来的 JS 代码报ReferenceError: window is not defined。原因JS 代码里用了浏览器环境特有的全局对象Node.js 里没有。解决在 JS 文件开头补上缺失的对象。常见的补丁// 补浏览器环境 var window global; var document { cookie: , createElement: function() { return {}; }, getElementsByTagName: function() { return []; } }; var navigator { userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 }; var location { href: https://item.m.jd.com/, protocol: https: };补完之后如果还报错看具体缺什么缺什么补什么。有些代码会检测window.top、window.self直接赋值为window本身即可。这个办法虽然土但能解决大部分环境缺失问题。4.4 签名有效期太短请求还没发出去就过期了现象本地生成签名后隔了几秒再发请求接口返回签名过期。原因h5st 里包含时间戳服务端会校验时间戳和当前时间的差值超过阈值就拒绝。阈值通常是几分钟但有些接口可能更短。解决生成签名后立即发请求不要缓存签名。如果业务需要批量请求每个请求单独生成签名。另外检查本地系统时间是否准确和 NTP 服务器同步一下。我遇到过本地时间慢了 3 分钟导致所有签名都过期排查了半天才发现是系统时间的问题。4.5 换了接口 h5st 算法就变了现象同一个页面不同接口的 h5st 生成方式不一样A 接口能跑通B 接口就报错。原因不同接口可能用了不同的签名版本或者参与签名的参数不同。某东的 h5st 有多个版本v1、v2、v3 的算法有差异具体用哪个版本取决于接口和页面。解决每个接口单独抓包分析不要假设一套代码通吃。在请求参数里找h5st的版本标识比如h5stVersion或者version。如果没有显式标识就对比不同接口的签名逻辑看差异在哪里。常见差异是参与签名的字段不同或者加密轮数不同。5. 让 h5st 复现更稳的几个进阶技巧5.1 用代理拦截 自动替换验证签名逻辑与其每次手动抓包对比不如在本地起一个代理拦截请求后自动替换 h5st然后观察接口是否返回正常。这样可以快速验证你的签名算法是否正确而不需要反复在浏览器和 Python 之间切换。用mitmproxy写一个简单的脚本from mitmproxy import http import json def request(flow: http.HTTPFlow): if api.m.jd.com in flow.request.pretty_url: # 解析请求体 body flow.request.get_text() params dict(item.split() for item in body.split()) # 重新生成 h5st new_h5st generate_sign(params) params[h5st] new_h5st # 重新编码请求体 new_body .join(f{k}{v} for k, v in params.items()) flow.request.set_text(new_body) print(f替换 h5st: {new_h5st})这个脚本会在请求经过代理时自动把 h5st 替换成你本地算出来的值。如果接口返回正常说明你的算法是对的如果返回错误说明算法还有问题。mitmproxy的安装和使用这里不展开核心思路是利用代理做中间人把验证环节自动化。5.2 把签名逻辑封装成独立服务如果你需要频繁调用某东的接口每次都在 Python 里跑一遍签名逻辑效率不高。更好的做法是把签名逻辑封装成一个本地 HTTP 服务用 Node.js 跑原始的 JS 代码Python 通过 HTTP 调用。这样既能保证签名和浏览器一致又能避免execjs的性能问题。用express起一个简单的服务const express require(express); const app express(); app.use(express.json()); // 加载抠出来的签名模块 const signModule require(./sign.js); app.post(/sign, (req, res) { const params req.body; const h5st signModule.generateSign(params); res.json({ h5st: h5st }); }); app.listen(3000, () { console.log(签名服务已启动端口 3000); });Python 端调用import requests def get_h5st(params): resp requests.post(http://127.0.0.1:3000/sign, jsonparams) return resp.json()[h5st]这种架构的好处是职责分离Node.js 负责签名Python 负责业务逻辑。签名逻辑更新时只需要替换 Node.js 侧的 JS 文件Python 代码不用动。缺点是多了一个服务进程部署时需要考虑进程管理和端口占用。5.3 监控签名失效率及时发现算法更新某东的签名算法不是一成不变的可能某天就更新了。如果你在生产环境里用这套逻辑需要加一个监控统计签名请求的成功率如果成功率突然下降大概率是算法变了。简单的做法是在每次请求后检查返回码如果连续多次返回签名错误就触发告警。告警方式可以是邮件、钉钉机器人或者简单的日志标记。我一般会在代码里加一个计数器失败次数超过阈值就打印一条醒目的日志方便快速定位。import logging sign_fail_count 0 def request_with_sign(url, params): global sign_fail_count h5st generate_sign(params) params[h5st] h5st resp requests.post(url, dataparams) if 签名错误 in resp.text or sign error in resp.text.lower(): sign_fail_count 1 logging.warning(f签名失败累计失败次数: {sign_fail_count}) if sign_fail_count 5: logging.error(签名失效率过高请检查算法是否更新) else: sign_fail_count 0 return resp这段代码的逻辑很简单每次签名失败就累加计数成功则清零。连续失败超过 5 次就输出错误日志。阈值可以根据实际请求量调整请求量大就调高请求量小就调低。关键是要有一个反馈机制而不是等业务方找上门才发现签名失效了。5.4 用浏览器自动化做兜底方案如果算法更新太频繁逆向成本太高可以考虑用浏览器自动化做兜底。用 Playwright 或 Puppeteer 打开页面让浏览器自己生成 h5st然后从网络请求里截取。这种方案性能差但胜在稳定只要页面能正常访问签名就是对的。from playwright.sync_api import sync_playwright def get_h5st_from_browser(url): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() # 拦截请求提取 h5st h5st_value None def handle_request(request): nonlocal h5st_value if h5st in request.url or h5st in str(request.post_data): h5st_value request.post_data print(f捕获到 h5st: {h5st_value}) page.on(request, handle_request) page.goto(url) page.wait_for_timeout(5000) browser.close() return h5st_value这个方案的思路是不自己算签名而是让浏览器算你只负责把结果拿出来。适合签名算法极其复杂、更新频繁、且对性能要求不高的场景。缺点是资源消耗大每个请求都要起一个浏览器实例并发能力有限。5.5 我踩过的最大一个坑忽略了 Cookie 里的某个字段最后说一个血泪教训。有一次我签名算法完全正确但接口一直返回风控错误。排查了一整天最后发现是 Cookie 里少了一个叫__jdv的字段。这个字段是浏览器首次访问时种下的后续请求必须带上否则服务端会认为请求来源异常。解决方式很简单在浏览器里把完整的 Cookie 复制出来放到请求头里。但问题是这个 Cookie 有有效期过期了需要重新获取。更稳妥的做法是用浏览器自动化先访问一次首页把 Cookie 拿到再用于后续请求。这个坑让我明白签名只是风控的一环请求的完整性同样重要。头部、Cookie、Referer、User-Agent任何一个缺失或不对都可能导致请求失败。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑