当前位置: 首页 > news >正文

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请求。我们的目标是找到那个真正携带了你的搜索条件、向服务器查询航班列表的请求。

如何快速定位它?有几个技巧:

  1. 看请求类型:重点关注FetchXHR类型的请求。
  2. 看请求URL:URL中很可能包含searchflightavailabilityapi等关键词。对于这个网站,我观察到的一个关键请求是向https://beta.cebupacificair.com/api/v1/...这样的域名路径发起的。
  3. 看请求负载(Payload):点击疑似请求,查看Headers选项卡下的Request PayloadForm Data,以及Preview选项卡看返回的数据结构。真正的搜索请求,其Payload必然包含你输入的出发地、目的地、日期等信息,而返回的Preview应该是结构化的航班数据(如航班号、时间、价格等)。

通过筛选,我找到了一个名为availability的POST请求。它的请求负载是一个JSON对象,里面包含了origindestinationdepartureDate等明文信息,但同时,也包含了一些看起来是哈希或令牌的字段,比如signaturetokenrequestId。这些就是我们需要逆向的目标——网站用它们来防止简单的脚本直接调用接口。

3. 核心参数逆向分析

找到关键请求后,逆向工作就正式开始了。我们的目标是找到像signature这类参数的计算方法。

3.1 逆向入口:从请求发起处打断点

我们知道了目标请求的终点,现在要找到它的起点——究竟是哪一段JavaScript代码发起了这个请求。

Network面板中,找到那个关键的availability请求,右键点击它,选择“Copy” -> “Copy as cURL”“Copy as fetch”。不过,这里我们更关心调用栈。在请求的Headers选项卡最下方,有一个“Initiator”列。它显示了是哪个脚本文件、哪一行代码发起了这个请求。点击那个文件名链接,它会直接跳转到Sources面板对应的代码行。

这就是我们的第一个突破口。但是,生产环境的代码通常是被压缩(minify)过的,变量名都是a, b, c,单行代码极长,可读性为零。别慌,Chrome提供了“Pretty print”功能(那个{}图标),点击它可以将代码格式化,恢复一定的结构,虽然变量名无法恢复,但至少有了换行和缩进。

在发起请求的代码行(通常是fetchaxios.post语句)左侧的行号处点击,设置一个断点。然后,回到网页,再次点击搜索按钮。此时,代码执行会在断点处暂停。

3.2 关键参数生成逻辑追踪

当代码在断点处暂停时,我们的“侦探”工作就进入了微观层面。在右侧的Scope调试器面板中,你可以看到当前作用域内所有变量的值。重点查看即将被用作请求参数的那个对象。

signature为例,你需要向上追溯它的值是怎么来的。在格式化后的代码中,搜索signature:这个赋值语句。找到后,观察它的值是一个变量(如e)还是一个函数调用的结果(如o.getSignature())。

  • 如果是变量:你需要查看这个变量在何处被赋值。可以在这个变量被赋值的地方再打一个断点,然后刷新页面或重新触发搜索,追踪其来源。
  • 如果是函数调用:这更常见。将鼠标悬停在函数名上,或者在该函数调用处打上断点,然后单步步入(F11)这个函数内部。

在宿务太平洋航空的这个案例中,经过追踪,我发现signature参数并非由复杂的加密算法生成。它更像是一个请求校验令牌,其生成逻辑可能如下:

  1. 在页面加载初期,网站可能从一个初始化接口获取了一个初始的tokenseed
  2. 在发起搜索请求时,会将这个token与你输入的搜索条件(如航线、日期)按特定顺序拼接成一个字符串。
  3. 对这个拼接后的字符串执行一个简单的哈希运算(比如 MD5 或 SHA1,但在前端更常见的是对字符串进行某种自定义的变换)。
  4. 最终得到的哈希值或变换后的字符串,就作为signature随请求发出。

为了验证,我需要在代码中寻找类似concat+(字符串拼接)、md5CryptoJScreateHash等关键词。在Sources面板按Ctrl+Shift+F进行全局搜索,是快速定位相关函数的好方法。

实操心得:不要一上来就试图理解全部代码。逆向就像解谜,抓住一条主线(比如signature)深挖下去。利用好调试器的“单步执行”、“步入”、“步出”功能,并时刻关注“调用堆栈(Call Stack)”,它能告诉你当前执行的函数是如何被一层层调用的,帮助你理解代码的执行脉络。

3.3 算法还原与本地模拟

经过一步步调试和观察变量值的变化,我逐渐摸清了signature的生成规律。它可能类似于:signature = md5( token + ‘|’ + origin + ‘|’ + destination + ‘|’ + departureDate )的某种变体。

为了在本地复现,我需要做两件事:

  1. 提取关键函数:在开发者工具的Console面板中,你可以直接访问当前页面上下文中的任何全局函数或对象。尝试输入你怀疑的函数名,比如window.getSignature,看看它是否存在。如果存在,你可以尝试调用它,传入一些参数,看输出是否与网络请求中的一致。如果函数是某个模块内部的,你可能需要找到该模块的引用路径。
  2. 重写算法:如果函数逻辑清晰且不依赖太多内部状态,我更喜欢用纯净的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-js
  • axios:一个优秀的HTTP客户端,用于发送请求,比原生的fetchhttp模块更易用。
  • crypto-js:一个JavaScript加密算法库,如果逆向发现使用了MD5、SHA256等,用它来实现非常方便。

创建一个名为cebu_search.js的文件作为我们的主脚本。

4.2 分步模拟请求过程

根据之前的分析,我们的脚本需要按顺序执行以下步骤:

第一步:获取初始令牌(Session Key)通常,这个令牌会在页面加载时通过一个不显眼的请求获取。我们需要在最初的网络请求列表中,找到一个返回了类似tokensessionIdkey的请求。模拟这个请求,并从中提取出我们需要的值。

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-KeyX-CSRF-Token。这些信息也需要从网页前期的请求或响应中获取。

如果模拟请求后返回了403、404错误,或者返回的数据是空或错误信息,首先检查:

  1. 签名算法是否100%正确?仔细核对拼接顺序、大小写、是否有额外的分隔符或盐值(salt)。
  2. 请求头是否完整?用开发者工具对比你的脚本请求和浏览器真实请求的Request Headers,确保关键字段一致。
  3. sessionKey是否过期?这类令牌通常有有效期,可能需要更频繁地获取。

5. 常见问题与调试技巧实录

在实际操作中,你几乎一定会遇到各种问题。下面是我踩过的一些坑和解决方法。

5.1 参数逆向不成功

  • 现象:本地生成的签名与服务端验证不通过。
  • 排查
    1. 字符串拼接细节:检查空格、换行符、引号。有时拼接的字符串末尾可能有多余的空格。在JavaScript中,使用模板字符串或+连接时需格外小心。最好在调试器中,将生成签名的原始字符串打印出来,与你在代码中拼接的字符串进行逐字符对比。
    2. 编码问题:参数值是否需要URL编码或Base64编码?在拼接前还是拼接后编码?查看浏览器中实际发送的请求负载(在Network面板中,Payload有时会显示编码后的样子,可以切换view source查看原始数据)。
    3. 算法差异:你确定是MD5吗?会不会是SHA1、SHA256,或者是HMAC?生成的签名是十六进制还是Base64格式?大小写是否正确?在调试时,可以直接在Console中引用页面已有的加密函数(如CryptoJS.MD5('test').toString())来验证你的理解。
    4. 缺失盐值或时间戳:签名算法可能混合了服务器下发的盐值(salt)和当前时间戳。确保你获取了所有必要的动态变量。

5.2 请求被拒绝或返回空数据

  • 现象:请求返回403 Forbidden、404 Not Found,或者返回的JSON数据是{“status”: “error”},甚至是一个反爬虫的HTML页面。
  • 排查
    1. 请求头完整性:这是最常见的原因。除了User-AgentReferer,检查是否有Cookie。一些认证状态可能保存在Cookie中。你可以使用像axios-cookiejar-support这样的库来维持会话。此外,关注AcceptAccept-LanguageAccept-Encoding等头,尽量与浏览器保持一致。
    2. 请求频率:脚本发送请求过快,容易触发服务器的频率限制。在请求之间添加随机延迟(例如setTimeout)。
    3. IP限制:某些接口可能对非正常用户行为的IP进行限制。这超出了纯JS逆向的范畴,可能需要考虑使用代理IP池。
    4. 参数格式:确认你的请求体格式。如果是Form Data,需要用application/x-www-form-urlencoded格式发送;如果是Request Payload(JSON),则用application/json。在axios中,后者是默认的,前者需要使用URLSearchParamsqs库来构建数据。

5.3 代码压缩与混淆的应对

  • 现象:代码被压缩成一行,变量名都是a,b,c,完全无法阅读。
  • 技巧
    1. 美化代码:首先使用开发者工具的“Pretty Print”功能。
    2. 搜索关键常量:即使变量名被混淆,字符串常量、数字常量、API端点URL通常保持不变。在Sources面板中全局搜索(Ctrl+Shift+F)像”signature“”availability“”MD5“这样的字符串,可以快速定位到相关代码区域。
    3. 关注函数调用:寻找像JSON.stringify()fetch()axios.post()Object.keys()这样的原生函数调用,它们周围往往是处理数据的逻辑。
    4. 使用AST工具(进阶):对于复杂混淆,可以尝试使用像babel-parser这样的工具将代码解析成抽象语法树(AST),然后进行分析和还原。但这属于高阶技能,对于本例这样的简单场景通常不需要。

5.4 调试技巧速查表

问题场景调试动作预期目标与技巧
找不到关键请求1. 勾选 Preserve log
2. 过滤 XHR/Fetch
3. 按请求大小/时间排序
找到携带表单数据且返回JSON的POST请求。关注api,search,query等关键词。
断点不生效1. 确认代码已格式化
2. 检查是否为异步代码
3. 刷新页面重新触发
fetchthenasync函数内,或setTimeout后可能需等待。使用“Event Listener Breakpoints”捕获事件。
变量值看不清1. 在Console中打印
2. 使用“Watch”表达式
3. 鼠标悬停查看
console.log(variable)是最直接的方法。对于对象,使用JSON.stringify(var, null, 2)美化输出。
算法逻辑复杂1. 单步执行(F11)
2. 关注调用栈(Call Stack)
3. 记录输入输出
一步步跟进,记录每个转换步骤的输入和输出,手动验证中间结果。调用栈能帮你理解函数层级关系。
本地模拟失败1. 对比浏览器请求
2. 检查网络工具(如Postman)
3. 验证加密库输出
用Postman重放浏览器捕获的原始请求(cURL),确保能成功。再逐一替换成自己的参数,定位差异点。

6. 扩展思考与安全启示

完成这个逆向案例后,我们得到的不仅仅是一个能获取航班数据的脚本。这个过程本身带来了更多关于Web应用安全和设计的思考。

从防御者(网站开发者)的角度看,这个案例的“安全措施”是比较初级的。签名算法暴露在前端,意味着一旦被逆向,防护即告失效。更健壮的做法应该是:

  • 关键逻辑后置:将核心的校验、计价逻辑放在服务器端。前端只负责展示和收集数据,所有业务规则由后端API严格把控。
  • 使用非对称加密或动态令牌:例如,每次会话使用一次性令牌(Nonce),或利用时间戳和服务器密钥生成动态签名,增加重放攻击的难度。
  • 增加行为验证:引入验证码(CAPTCHA)或基于用户交互行为的风险分析,对异常高频、模式固定的请求进行拦截。

从学习者(我们)的角度看,JS逆向是一个需要耐心、观察力和逻辑推理的过程。它强迫你去理解一个黑盒系统的运行方式。这个技能不仅用于爬虫,在前端性能优化(理解第三方脚本行为)、安全审计(检查自家网站接口安全性)、甚至调试没有源码的遗留系统时,都极其有用。

最后,关于这类技术的使用,我必须强调一点:所有的技术学习与研究都应在法律和网站服务条款允许的范围内进行。逆向分析的目的应是理解原理、提升技能,而非进行未授权的数据抓取、干扰服务正常运行或侵犯他人权益。在实际项目中,如果需要数据,优先考虑联系官方获取API权限,这才是长久之计。这个宿务太平洋航空的案例,作为一个纯粹的技术学习样本,已经很好地展示了从观察到分析,再到模拟的完整闭环。掌握了这套方法,你就有能力去探索和理解更多Web应用背后的数据逻辑了。

http://www.jsqmd.com/news/1111770/

相关文章:

  • D3.js实战包:全球超市销售数据的交互式地图与图表可视化
  • UI自动化测试五大核心挑战与实战解决方案
  • 学位论文质量护航!2026智能AI论文软件推荐指南
  • 从零构建企业级接口自动化测试框架:以叮当书城项目为例
  • Web开发安全实战:MVC架构与会话管理中的纵深防御策略
  • 虚拟化安全盲区:应急响应实战指南
  • 5分钟掌握B站视频永久保存技巧:m4s-converter完全指南
  • C语言从零实现AES-128:深入理解算法原理与嵌入式优化实践
  • 手把手实现前后端RSA加密通信:Python与JavaScript实战指南
  • 生成式AI质量保障:从断言式到评估式自动化测试的实战演进
  • 如何快速掌握SPT-AKI Profile Editor:逃离塔科夫离线存档修改器终极指南
  • 5分钟掌握专业视频去水印:基于梯度分析的智能解决方案
  • Coze工作流HTTP请求安全指南:六大陷阱与实战防护
  • Cypress Testing Library 查询失败与超时错误排查指南
  • 国产化环境下Dify配置失效排查:JDK签名与SM4兼容性深度解析
  • elfin-parser与DWARF5支持:最新调试信息格式的完整实现解析
  • 5分钟快速上手:BepInEx终极Unity游戏插件框架指南
  • 基于混沌算法的图像加密:Matlab实现与安全性分析
  • 如何永久保存微信聊天记录:开源工具的终极解决方案
  • 模型网关迁移别一刀切:用影子流量、分批切流与回滚控制风险
  • Claude Science 入门教程
  • PhotoGIMP终极指南:3分钟免费实现从Photoshop到开源图像编辑的无缝切换
  • 收藏必备!小白程序员快速入门大模型核心概念(轻松理解并上手用)
  • Web自动化实战:从Selenium到Playwright的工程化架构与稳定性设计
  • Dify高危权限漏洞CVE-2024-XXXX应急响应:原理、复现与热补丁修复
  • Java Selenium自动化投递猎聘简历:绕过限制与拟人化实战
  • 国密算法SM2/SM3/SM4源码解析与Java/Vue集成实战指南
  • 企业级Playwright自动化测试框架:从POM设计到CI/CD集成实战
  • C++开发者如何驯服AI?内存安全、SIMD指令与实时推理场景下的代码生成心法
  • iOS内存优化:基于Appium与XCTrace的自动化归因实践