JS逆向实战:从宿务航空机票搜索到参数签名算法解析
1. 项目概述从一张机票到一段JS逆向旅程最近在分析一些在线服务的数据交互逻辑时遇到了一个挺有意思的目标——宿务太平洋航空cebupacificair的网站。这不算是一个高难度的挑战但对于想入门JS逆向或者想找一个贴近实际、流程完整的练手项目来说它简直是个“教科书式”的案例。整个分析过程不涉及复杂的混淆和反调试但完整涵盖了从网页请求观察到参数逆向、再到本地复现的核心链路。说白了这就是一个典型的“看清网站如何与服务器对话并学会模仿它说话”的过程。对于前端开发或者对网络爬虫感兴趣的朋友这类分析能让你深刻理解一个现代Web应用是如何工作的它的数据从哪里来又经过了怎样的处理才发送出去。而对于安全研究或测试人员这也是理解接口安全性的基础。本次分析的目标很单纯弄清楚在搜索航班时网站向后台发送了哪些关键参数特别是那些看似随机或加密的参数是如何生成的。我们会使用最基础的开发者工具一步步拆解最终用几行JavaScript代码模拟出这个请求。你会发现很多看似神秘的“加密参数”其背后的逻辑可能比你想象的要简单。2. 环境准备与初步侦查工欲善其事必先利其器。进行JS逆向分析你不需要什么高端武器浏览器自带的开发者工具DevTools就是你的瑞士军刀。我主要使用Chrome或Edge浏览器它们的工具链基本一致。2.1 核心工具配置首先打开宿务太平洋航空的官网。在开始搜索前我们需要对开发者工具进行一些关键设置以便更好地捕捉和分析网络请求。打开开发者工具F12切换到Network网络面板。这里有几个关键过滤器需要勾选XHR/Fetch这能过滤出绝大多数由JavaScript发起的、用于获取数据的Ajax请求是我们关注的重点。同时确保Preserve log保留日志是勾选状态。否则当你提交搜索表单、页面跳转或刷新时之前的请求记录会被清空你就抓不到关键的初始化请求了。另一个至关重要的面板是Sources源代码或调试器。在这里我们可以给JavaScript代码设置断点。当程序执行到断点处时会暂停允许我们查看当时所有变量的值、调用栈以及单步执行代码。这是逆向动态生成参数的最核心手段。为了能顺利调试我们通常需要禁用任何可能干扰的浏览器扩展并确保网站没有启用强力的反调试策略幸运的是这个目标网站没有。注意在开始操作前建议先打开一个无痕窗口进行测试。无痕模式会禁用大部分扩展提供一个更干净的分析环境避免缓存或插件干扰请求的捕获。2.2 关键请求定位与初筛设置好工具后在网站首页填写航班搜索信息选择单程/往返、出发城市、到达城市、日期、乘客数量然后点击搜索。点击搜索后你的眼睛要紧紧盯住Network面板。一瞬间会冒出很多请求包括图片、CSS、字体、以及多个脚本和XHR请求。我们的目标是找到那个真正携带了你的搜索条件、向服务器查询航班列表的请求。如何快速定位它有几个技巧看请求类型重点关注Fetch或XHR类型的请求。看请求URLURL中很可能包含search、flight、availability、api等关键词。对于这个网站我观察到的一个关键请求是向https://beta.cebupacificair.com/api/v1/...这样的域名路径发起的。看请求负载Payload点击疑似请求查看Headers选项卡下的Request Payload或Form Data以及Preview选项卡看返回的数据结构。真正的搜索请求其Payload必然包含你输入的出发地、目的地、日期等信息而返回的Preview应该是结构化的航班数据如航班号、时间、价格等。通过筛选我找到了一个名为availability的POST请求。它的请求负载是一个JSON对象里面包含了origin、destination、departureDate等明文信息但同时也包含了一些看起来是哈希或令牌的字段比如signature、token或requestId。这些就是我们需要逆向的目标——网站用它们来防止简单的脚本直接调用接口。3. 核心参数逆向分析找到关键请求后逆向工作就正式开始了。我们的目标是找到像signature这类参数的计算方法。3.1 逆向入口从请求发起处打断点我们知道了目标请求的终点现在要找到它的起点——究竟是哪一段JavaScript代码发起了这个请求。在Network面板中找到那个关键的availability请求右键点击它选择“Copy” - “Copy as cURL”或“Copy as fetch”。不过这里我们更关心调用栈。在请求的Headers选项卡最下方有一个“Initiator”列。它显示了是哪个脚本文件、哪一行代码发起了这个请求。点击那个文件名链接它会直接跳转到Sources面板对应的代码行。这就是我们的第一个突破口。但是生产环境的代码通常是被压缩minify过的变量名都是a, b, c单行代码极长可读性为零。别慌Chrome提供了“Pretty print”功能那个{}图标点击它可以将代码格式化恢复一定的结构虽然变量名无法恢复但至少有了换行和缩进。在发起请求的代码行通常是fetch或axios.post语句左侧的行号处点击设置一个断点。然后回到网页再次点击搜索按钮。此时代码执行会在断点处暂停。3.2 关键参数生成逻辑追踪当代码在断点处暂停时我们的“侦探”工作就进入了微观层面。在右侧的Scope或调试器面板中你可以看到当前作用域内所有变量的值。重点查看即将被用作请求参数的那个对象。以signature为例你需要向上追溯它的值是怎么来的。在格式化后的代码中搜索signature:这个赋值语句。找到后观察它的值是一个变量如e还是一个函数调用的结果如o.getSignature()。如果是变量你需要查看这个变量在何处被赋值。可以在这个变量被赋值的地方再打一个断点然后刷新页面或重新触发搜索追踪其来源。如果是函数调用这更常见。将鼠标悬停在函数名上或者在该函数调用处打上断点然后单步步入F11这个函数内部。在宿务太平洋航空的这个案例中经过追踪我发现signature参数并非由复杂的加密算法生成。它更像是一个请求校验令牌其生成逻辑可能如下在页面加载初期网站可能从一个初始化接口获取了一个初始的token或seed。在发起搜索请求时会将这个token与你输入的搜索条件如航线、日期按特定顺序拼接成一个字符串。对这个拼接后的字符串执行一个简单的哈希运算比如 MD5 或 SHA1但在前端更常见的是对字符串进行某种自定义的变换。最终得到的哈希值或变换后的字符串就作为signature随请求发出。为了验证我需要在代码中寻找类似concat、字符串拼接、md5、CryptoJS、createHash等关键词。在Sources面板按CtrlShiftF进行全局搜索是快速定位相关函数的好方法。实操心得不要一上来就试图理解全部代码。逆向就像解谜抓住一条主线比如signature深挖下去。利用好调试器的“单步执行”、“步入”、“步出”功能并时刻关注“调用堆栈Call Stack”它能告诉你当前执行的函数是如何被一层层调用的帮助你理解代码的执行脉络。3.3 算法还原与本地模拟经过一步步调试和观察变量值的变化我逐渐摸清了signature的生成规律。它可能类似于signature md5( token ‘|’ origin ‘|’ destination ‘|’ departureDate )的某种变体。为了在本地复现我需要做两件事提取关键函数在开发者工具的Console面板中你可以直接访问当前页面上下文中的任何全局函数或对象。尝试输入你怀疑的函数名比如window.getSignature看看它是否存在。如果存在你可以尝试调用它传入一些参数看输出是否与网络请求中的一致。如果函数是某个模块内部的你可能需要找到该模块的引用路径。重写算法如果函数逻辑清晰且不依赖太多内部状态我更喜欢用纯净的JavaScript重新实现它。这样代码更干净不依赖原网页环境。例如如果发现它只是用了CryptoJS.MD5那么我可以在自己的Node.js脚本中引入crypto-js库来实现。在这个具体案例中我通过调试发现signature的生成依赖了一个在页面加载时从服务器获取的动态值我们暂称它为sessionKey。这意味着完全本地模拟需要两步第一步模拟一个初始化请求获取sessionKey。第二步用sessionKey和搜索参数按照观察到的规则生成signature。4. 完整请求模拟与代码实现分析清楚逻辑后就可以动手编写代码来模拟整个搜索请求了。我选择使用 Node.js 环境因为它能方便地处理HTTP请求和加密库。4.1 依赖安装与项目结构首先初始化一个项目并安装必要的包npm init -y npm install axios crypto-jsaxios一个优秀的HTTP客户端用于发送请求比原生的fetch或http模块更易用。crypto-js一个JavaScript加密算法库如果逆向发现使用了MD5、SHA256等用它来实现非常方便。创建一个名为cebu_search.js的文件作为我们的主脚本。4.2 分步模拟请求过程根据之前的分析我们的脚本需要按顺序执行以下步骤第一步获取初始令牌Session Key通常这个令牌会在页面加载时通过一个不显眼的请求获取。我们需要在最初的网络请求列表中找到一个返回了类似token、sessionId或key的请求。模拟这个请求并从中提取出我们需要的值。const axios require(axios); const CryptoJS require(crypto-js); async function getSessionKey() { // 这里需要填写实际的初始化请求URL、Headers和可能的参数 const initUrl https://beta.cebupacificair.com/api/v1/init; const headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Accept: application/json, // 其他必要的Headers如Referer }; try { const response await axios.get(initUrl, { headers }); // 假设返回的JSON中有一个字段叫 sessionKey return response.data.sessionKey; } catch (error) { console.error(获取SessionKey失败:, error); return null; } }第二步构造搜索参数并生成签名假设我们分析出的签名规则是MD5(sessionKey origin destination departureDate)。function generateSignature(sessionKey, origin, destination, date) { const rawString ${sessionKey}|${origin}|${destination}|${date}; // 使用 crypto-js 计算 MD5并转为十六进制字符串小写 const signature CryptoJS.MD5(rawString).toString(CryptoJS.enc.Hex); return signature; }第三步组装最终请求并发送将明文参数和生成的签名一起以正确的格式通常是JSON发送到搜索接口。async function searchFlights(origin, destination, date) { const sessionKey await getSessionKey(); if (!sessionKey) { console.log(无法获取会话密钥终止搜索。); return; } const signature generateSignature(sessionKey, origin, destination, date); const searchUrl https://beta.cebupacificair.com/api/v1/availability; const payload { origin: origin, destination: destination, departureDate: date, adults: 1, // ... 其他必要参数 signature: signature, // 这是我们逆向的核心 // 可能还有其他动态参数如 timestamp, nonce 等 }; const headers { Content-Type: application/json, User-Agent: Mozilla/5.0 ..., Referer: https://beta.cebupacificair.com/, // 有时还需要特定的 X-Requested-With 或 X-CSRF-Token }; try { const response await axios.post(searchUrl, payload, { headers }); console.log(搜索成功); console.log(JSON.stringify(response.data, null, 2)); // 美化输出JSON // 这里可以解析 response.data提取航班信息 } catch (error) { console.error(搜索请求失败:, error.response?.data || error.message); } } // 执行搜索 searchFlights(MNL, CEB, 2023-10-27);4.3 请求头与反爬策略应对仅仅有正确的参数是不够的。服务器通常会检查HTTP请求头Headers来区分浏览器请求和脚本请求。我们的脚本必须模拟得足够像。User-Agent这是最基本的标识必须设置成一个常见的浏览器UA字符串。Referer表示请求是从哪个页面发起的通常需要设置为目标网站的搜索页URL。Content-Type对于POST JSON请求必须是application/json。Origin / Host这些通常由axios自动设置但有时也需要留意。自定义头有些API会要求携带自定义的头信息如X-API-Key、X-CSRF-Token。这些信息也需要从网页前期的请求或响应中获取。如果模拟请求后返回了403、404错误或者返回的数据是空或错误信息首先检查签名算法是否100%正确仔细核对拼接顺序、大小写、是否有额外的分隔符或盐值salt。请求头是否完整用开发者工具对比你的脚本请求和浏览器真实请求的Request Headers确保关键字段一致。sessionKey是否过期这类令牌通常有有效期可能需要更频繁地获取。5. 常见问题与调试技巧实录在实际操作中你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法。5.1 参数逆向不成功现象本地生成的签名与服务端验证不通过。排查字符串拼接细节检查空格、换行符、引号。有时拼接的字符串末尾可能有多余的空格。在JavaScript中使用模板字符串或连接时需格外小心。最好在调试器中将生成签名的原始字符串打印出来与你在代码中拼接的字符串进行逐字符对比。编码问题参数值是否需要URL编码或Base64编码在拼接前还是拼接后编码查看浏览器中实际发送的请求负载在Network面板中Payload有时会显示编码后的样子可以切换view source查看原始数据。算法差异你确定是MD5吗会不会是SHA1、SHA256或者是HMAC生成的签名是十六进制还是Base64格式大小写是否正确在调试时可以直接在Console中引用页面已有的加密函数如CryptoJS.MD5(test).toString()来验证你的理解。缺失盐值或时间戳签名算法可能混合了服务器下发的盐值salt和当前时间戳。确保你获取了所有必要的动态变量。5.2 请求被拒绝或返回空数据现象请求返回403 Forbidden、404 Not Found或者返回的JSON数据是{“status”: “error”}甚至是一个反爬虫的HTML页面。排查请求头完整性这是最常见的原因。除了User-Agent和Referer检查是否有Cookie。一些认证状态可能保存在Cookie中。你可以使用像axios-cookiejar-support这样的库来维持会话。此外关注Accept、Accept-Language、Accept-Encoding等头尽量与浏览器保持一致。请求频率脚本发送请求过快容易触发服务器的频率限制。在请求之间添加随机延迟例如setTimeout。IP限制某些接口可能对非正常用户行为的IP进行限制。这超出了纯JS逆向的范畴可能需要考虑使用代理IP池。参数格式确认你的请求体格式。如果是Form Data需要用application/x-www-form-urlencoded格式发送如果是Request PayloadJSON则用application/json。在axios中后者是默认的前者需要使用URLSearchParams或qs库来构建数据。5.3 代码压缩与混淆的应对现象代码被压缩成一行变量名都是a,b,c完全无法阅读。技巧美化代码首先使用开发者工具的“Pretty Print”功能。搜索关键常量即使变量名被混淆字符串常量、数字常量、API端点URL通常保持不变。在Sources面板中全局搜索CtrlShiftF像”signature“、”availability“、”MD5“这样的字符串可以快速定位到相关代码区域。关注函数调用寻找像JSON.stringify()、fetch()、axios.post()、Object.keys()这样的原生函数调用它们周围往往是处理数据的逻辑。使用AST工具进阶对于复杂混淆可以尝试使用像babel-parser这样的工具将代码解析成抽象语法树AST然后进行分析和还原。但这属于高阶技能对于本例这样的简单场景通常不需要。5.4 调试技巧速查表问题场景调试动作预期目标与技巧找不到关键请求1. 勾选 Preserve log2. 过滤 XHR/Fetch3. 按请求大小/时间排序找到携带表单数据且返回JSON的POST请求。关注api,search,query等关键词。断点不生效1. 确认代码已格式化2. 检查是否为异步代码3. 刷新页面重新触发在fetch、then、async函数内或setTimeout后可能需等待。使用“Event Listener Breakpoints”捕获事件。变量值看不清1. 在Console中打印2. 使用“Watch”表达式3. 鼠标悬停查看console.log(variable)是最直接的方法。对于对象使用JSON.stringify(var, null, 2)美化输出。算法逻辑复杂1. 单步执行F112. 关注调用栈Call Stack3. 记录输入输出一步步跟进记录每个转换步骤的输入和输出手动验证中间结果。调用栈能帮你理解函数层级关系。本地模拟失败1. 对比浏览器请求2. 检查网络工具如Postman3. 验证加密库输出用Postman重放浏览器捕获的原始请求cURL确保能成功。再逐一替换成自己的参数定位差异点。6. 扩展思考与安全启示完成这个逆向案例后我们得到的不仅仅是一个能获取航班数据的脚本。这个过程本身带来了更多关于Web应用安全和设计的思考。从防御者网站开发者的角度看这个案例的“安全措施”是比较初级的。签名算法暴露在前端意味着一旦被逆向防护即告失效。更健壮的做法应该是关键逻辑后置将核心的校验、计价逻辑放在服务器端。前端只负责展示和收集数据所有业务规则由后端API严格把控。使用非对称加密或动态令牌例如每次会话使用一次性令牌Nonce或利用时间戳和服务器密钥生成动态签名增加重放攻击的难度。增加行为验证引入验证码CAPTCHA或基于用户交互行为的风险分析对异常高频、模式固定的请求进行拦截。从学习者我们的角度看JS逆向是一个需要耐心、观察力和逻辑推理的过程。它强迫你去理解一个黑盒系统的运行方式。这个技能不仅用于爬虫在前端性能优化理解第三方脚本行为、安全审计检查自家网站接口安全性、甚至调试没有源码的遗留系统时都极其有用。最后关于这类技术的使用我必须强调一点所有的技术学习与研究都应在法律和网站服务条款允许的范围内进行。逆向分析的目的应是理解原理、提升技能而非进行未授权的数据抓取、干扰服务正常运行或侵犯他人权益。在实际项目中如果需要数据优先考虑联系官方获取API权限这才是长久之计。这个宿务太平洋航空的案例作为一个纯粹的技术学习样本已经很好地展示了从观察到分析再到模拟的完整闭环。掌握了这套方法你就有能力去探索和理解更多Web应用背后的数据逻辑了。

相关新闻