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类型的请求。 - 看请求URL:URL中很可能包含
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面板按Ctrl+Shift+F进行全局搜索,是快速定位相关函数的好方法。
实操心得:不要一上来就试图理解全部代码。逆向就像解谜,抓住一条主线(比如
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)和当前时间戳。确保你获取了所有必要的动态变量。
- 字符串拼接细节:检查空格、换行符、引号。有时拼接的字符串末尾可能有多余的空格。在JavaScript中,使用模板字符串或
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 Payload(JSON),则用application/json。在axios中,后者是默认的,前者需要使用URLSearchParams或qs库来构建数据。
- 请求头完整性:这是最常见的原因。除了
5.3 代码压缩与混淆的应对
- 现象:代码被压缩成一行,变量名都是a,b,c,完全无法阅读。
- 技巧:
- 美化代码:首先使用开发者工具的“Pretty Print”功能。
- 搜索关键常量:即使变量名被混淆,字符串常量、数字常量、API端点URL通常保持不变。在Sources面板中全局搜索(Ctrl+Shift+F)像
”signature“、”availability“、”MD5“这样的字符串,可以快速定位到相关代码区域。 - 关注函数调用:寻找像
JSON.stringify()、fetch()、axios.post()、Object.keys()这样的原生函数调用,它们周围往往是处理数据的逻辑。 - 使用AST工具(进阶):对于复杂混淆,可以尝试使用像
babel-parser这样的工具将代码解析成抽象语法树(AST),然后进行分析和还原。但这属于高阶技能,对于本例这样的简单场景通常不需要。
5.4 调试技巧速查表
| 问题场景 | 调试动作 | 预期目标与技巧 |
|---|---|---|
| 找不到关键请求 | 1. 勾选 Preserve log 2. 过滤 XHR/Fetch 3. 按请求大小/时间排序 | 找到携带表单数据且返回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. 单步执行(F11) 2. 关注调用栈(Call Stack) 3. 记录输入输出 | 一步步跟进,记录每个转换步骤的输入和输出,手动验证中间结果。调用栈能帮你理解函数层级关系。 |
| 本地模拟失败 | 1. 对比浏览器请求 2. 检查网络工具(如Postman) 3. 验证加密库输出 | 用Postman重放浏览器捕获的原始请求(cURL),确保能成功。再逐一替换成自己的参数,定位差异点。 |
6. 扩展思考与安全启示
完成这个逆向案例后,我们得到的不仅仅是一个能获取航班数据的脚本。这个过程本身带来了更多关于Web应用安全和设计的思考。
从防御者(网站开发者)的角度看,这个案例的“安全措施”是比较初级的。签名算法暴露在前端,意味着一旦被逆向,防护即告失效。更健壮的做法应该是:
- 关键逻辑后置:将核心的校验、计价逻辑放在服务器端。前端只负责展示和收集数据,所有业务规则由后端API严格把控。
- 使用非对称加密或动态令牌:例如,每次会话使用一次性令牌(Nonce),或利用时间戳和服务器密钥生成动态签名,增加重放攻击的难度。
- 增加行为验证:引入验证码(CAPTCHA)或基于用户交互行为的风险分析,对异常高频、模式固定的请求进行拦截。
从学习者(我们)的角度看,JS逆向是一个需要耐心、观察力和逻辑推理的过程。它强迫你去理解一个黑盒系统的运行方式。这个技能不仅用于爬虫,在前端性能优化(理解第三方脚本行为)、安全审计(检查自家网站接口安全性)、甚至调试没有源码的遗留系统时,都极其有用。
最后,关于这类技术的使用,我必须强调一点:所有的技术学习与研究都应在法律和网站服务条款允许的范围内进行。逆向分析的目的应是理解原理、提升技能,而非进行未授权的数据抓取、干扰服务正常运行或侵犯他人权益。在实际项目中,如果需要数据,优先考虑联系官方获取API权限,这才是长久之计。这个宿务太平洋航空的案例,作为一个纯粹的技术学习样本,已经很好地展示了从观察到分析,再到模拟的完整闭环。掌握了这套方法,你就有能力去探索和理解更多Web应用背后的数据逻辑了。