使用 WebpackJsonp 与 jQuery 进行代码注入
¶0x00 Webpack
Webpack 是当下最流行的 Web 打包工具,允许前端开者发在项目中进行模块化开发,并将代码打包成一个代码包(bundle),在浏览器上加载使用。但随着引入的第三库越来越多,代码包会越来越大,影响页面加载时间。于是 Webpack 提供了一个分包功能,可以将共享的库拆分(code-spliting)到不同的包中,使得浏览器可以并行加载不同的包,加快页面打开的速度。
¶0x01 jQuery
jQuery 是一个老牌的前端组件,主要用于提供方便的 Dom 操作和 Ajax 请求。由于在前端项目中大量使用,所以在模块化开发中,常常将它作为全局组件使用。为了必避在每个模块中导入它:import $ from 'jquery';
。Webpack 提供了一个叫 webpack.ProvidePlugin 的插件,可以在配置中将 jQuery 全局化,在其它模块中直接使用而无需 import
:
new webpack.ProvidePlugin({
$: 'jquery',
});
¶0x02 webpackJsonp
如果不使用 Webpack 的分包功能,包中的模块全部存在 Webpack 闭包里的 installedModules = {}
对象中,只能通过闭包中的 __webpack_require__(moduleId)
来引用。在这种情况下 jQuery 将与前端逻辑放在一起,并且受到 Webpack 的闭包保护,而无法在网页上的其它代码里引用包中的 jQuery 实例。除非你在代码中强行把 jQuery 导入到 window 命名空间[1]。
但是如果启用了 Webpack 的分包功能,Webpack 生成一个 Manifest.js
并向 Window 对象注入一个 webpackJsonp(chunkIds, moreModules, executeModules)
接口用于多个分包之间共享数据。
但是如果仔细阅读这个接口的代码,会发现第三个参数有特别的作用,可以指定 moduleId 并返回该 Module,相当于可以在外部直接 require
包内的模块。
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
...
if(executeModules) {
for(i=0; i < executeModules.length; i++) {
result = __webpack_require__(__webpack_require__.s = executeModules[i]);
}
}
return result;
}
¶0x03 $.ajax()
在 jQuery 中,前端开发者可以用 $.ajax()
/ $.getJSON()
/ $.post()
等接口与服务端通讯。后两者其实是 $.ajax()
的封装。从 jQuery 的文档中可以看到 $.ajax()
的一般用法:
$.ajax({
url: '/path/to/api-endpoint',
data: {
/* request data */
},
method: 'GET',
success: (data) => {
/* handle response data */
}
}).done((data) => {
/* another way to handle response data */
});
以上代码向 /path/to/api-endpoint
请求数据,并带上 data 指定的参数,最后在 success 里处理返回结果。
¶0x04 $.ajaxPrefilter()
如果有个 API 返回了 {"successful": false}
,要是我们能拦截返回结果并修改它,我们就可以构造一些数据来影响前端逻辑。而 jQuery 确实提供了一个接口让我们有机会在数据被处理前将它改掉:$.ajaxPrefilter()
$.ajaxPrefilter((options) => {
const oldSuccess = options.success;
options.success = (data) => {
console.log('original', data);
data.successful = true;
console.log('injected', data);
oldSuccess && oldSuccess(data);
};
});
以上代码向 jQuery 注册了一个 ajax pre-filter,之后的所有 ajax 都被会它预处理。它在将原来的 success 回调进行封装,并修改了服务端返回的结果,从而影响了前端原有的逻辑。除此之外,ajaxPrefilter 还可以用来修改所有 $.ajax() 的 options 对象,包括 data
等。
ajaxPrefilter 这类的拦截器本意是让前端开发者有一些统一的地方对 API 通讯进行一些封装,但是如果被脚本小子利用就另当别论了。
¶0x05 Case
影响前端逻辑看起来似乎没什么用,但是却很方便来用绕过一些限制,实现一些 Web 自动化,甚至采集用户数据等等。
例如某电视剧网站,最近为了收割微信用户,强制用户观影前需要关注公众号,将观影码发送至公众号才可以进入观看影片。但其接口设计有问题,只是通过 jQuery 不断轮询 API,检查服务端是否收到观影码,如果收到就放行。
- 打开 Console 观察,发现不能直接访问 jQuery 对象,但网站使用 Webpack 打包并分包处理;
- 通过下载 Manifest.js 可以发现
webpackJsonp(chunkIds, moreModules, executeModules)
接口; - 通过下载 Vendor.js 可以找到混淆过的 jQuery 的 moduleId ;
- 通过
let jq = webpackJsonp([],[],[jqModuleId])
获取包内 jQuery ; - 通过 ajaxPrefilter 注入修改 API 返回的结果;
- 下一次轮询后成功进入观影。
¶0x06 受影响版本
由于 Webpack 的代码是开源的,跟踪提交记录可以看到 webpackJsonp(chunkIds, moreModules, executeModules)
在 2.0.0-beta 版被引入,并在 4.0.0-alpha.0 被移除。所以如果网站使用 Webpack 分包,最好升级到 Webpack 4.x 以上的版本。
另外尽可能不要将 jQuery 暴露到 window 全局空间,可以防范类似 ajaxPrefilter 的修改。
¶0x07 备注
有个有趣的细节是,如果在 webpack.ProvidePlugin 中添加 {'window.$': 'jquery'}
,即使在模块中“不小心”写了 window.$ = $
或者 window['$'] = $
,Webpack 会将其修改为 __webpack_provided_window_dot_ = $
,从而防止 $ 被暴露到全局空间。
window.$ = $; ↩︎