Webpack 渗透思路
目前了解到的就是这些 Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。当 webpack 处理应用程序时,它会在内部构建一个依赖关系图,映射项目需要的每个模块,然后生成一个或多个 bundle。
所以说整体思路就是
“识别特征—还原源码—定位关键逻辑—安全分析”
如何分辨 webpack 打包网站
fofa 语句
body="chunk.js" || body="runtime" || body="
__webpack_require__
"
body="/static/js/" && body=".chunk.js"
典型的案例如下
<!-- webpack 打包后的典型 HTML 结构 -->
<!DOCTYPE html>
<html>
<head>
<title>App</title>
<!-- webpack 生成的 CSS 文件,通常带有 hash -->
<link href="/static/css/main.a1b2c3d4.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<!-- webpack 生成的 JS 文件,通常带有 hash -->
<script src="/static/js/runtime-main.1a2b3c4d.js"></script>
<script src="/static/js/2.e5f6g7h8.chunk.js"></script>
<script src="/static/js/main.i9j0k1l2.chunk.js"></script>
</body>
</html>
- 文件名带有 hash 值:如
main.a1b2c3d4.js
- chunk 文件:文件名包含
chunk
关键字 - runtime 文件:通常有
runtime
文件 - 数字命名的文件:如
2.e5f6g7h8.chunk.js
例如
控制台源代码就有Webpack://
webpack 目前支持多种调试模式,不同模式的区别在于编译后代码和源码之间的映射方式不同。具体来说,源码是通过不同的方式来映射的。有些模式会生成一个 .map 文件(用于源码映射),有些模式会通过注释的形式包含源码映射信息,还有些模式会使用 DataUrl 的方式(即把映射信息以 data URI 的形式嵌入到生成的文件中)
几种常见的映射打包方式
- source-map
生成独立的 .map 文件,方便调试,调试时可以还原到源代码。
适合用于生产环境,便于排查问题,但会暴露源码。
- inline-source-map
将 source map 以 DataUrl 的形式内联到编译后的文件中,不生成单独的 .map 文件。
适合开发环境,方便查看源码但文件体积较大。
- eval-source-map
将 source map 以 DataUrl 的形式嵌入到每个模块中(通过 eval),构建速度快,调试体验好。
适合开发环境,不推荐用于生产环境。
- cheap-module-source-map
提供行映射而不是列映射,忽略模块中的 loader source map,生成速度较快。
适用于开发环境,如果只关心行号,可以提升效率。
- hidden-source-map
生成 .map 文件,但不会在编译后的文件中引用,适合用于只需要 sourcemap 供错误收集工具使用,不直接暴露给用户。
存在泄露
Sources—> Page—> Webpack://中查看到Webpack项目源码
可以采用下面的反解 webpack
然后就可以分析项目结构和源代码审计等等了
自动化工具检测
https://github.com/rtcatc/Packer-Fuzzer
这个工具可以对 webpack 网站进行一个扫描,漏洞探查
python .\PackerFuzzer.py -u "..."
会生成一个 doc 文档,扫描报告
Webpack 案例
把相关代码放入 GitHub 仓库上面了
https://github.com/bx33661/Webpack-demo/tree/main
npm run dev
npm run build
会在 dist 文件夹中生成两个文件
- bundle.js
bundle.js 是webpack打包后生成的JavaScript文件,它包含了项目中所有JavaScript模块和依赖的集合。
主要特点:
- 代码压缩 :文件被压缩和混淆,以减小文件大小,提高加载速度。这就是为什么它看起来很难读懂。
- 模块整合 :webpack将所有模块(包括 index.js 和 greeting.js )打包成一个文件,解决了模块依赖问题。
- 样式注入 :CSS文件( style.css )也被打包到了这个文件中,通过JavaScript动态创建style标签注入到页面。
- 自执行函数 :整个代码被包裹在一个自执行函数中 (()=>{…})() ,创建了一个独立的作用域。
- index.html
- 自动引入bundle :自动添加了
<script defer="defer" src="bundle.js"></script>
标签,引入打包后的JavaScript文件。 - 压缩HTML :HTML代码被压缩,移除了不必要的空格和换行。
- 保留内容结构 :保留了原始模板中的DOM结构,包括标题、按钮和结果显示区域。
- 元数据保留 :保留了原始模板中的meta标签和其他头部信息。
(() => {
"use strict";
var n = {
56: (n, e, t) => {
n.exports = function(n) {
var e = t.nc;
e && n.setAttribute("nonce", e)
}
},
72: n => {
var e = [];
function t(n) {
for (var t = -1, r = 0; r < e.length; r++)
if (e[r].identifier === n) {
t = r;
break
} return t
}
function r(n, r) {
for (var a = {}, i = [], c = 0; c < n.length; c++) {
var s = n[c],
d = r.base ? s[0] + r.base : s[0],
u = a[d] || 0,
p = "".concat(d, " ").concat(u);
a[d] = u + 1;
var l = t(p),
f = {
css: s[1],
media: s[2],
sourceMap: s[3],
supports: s[4],
layer: s[5]
};
if (-1 !== l) e[l].references++, e[l].updater(f);
else {
var v = o(f, r);
r.byIndex = c, e.splice(c, 0, {
identifier: p,
updater: v,
references: 1
})
}
i.push(p)
}
return i
}
function o(n, e) {
var t = e.domAPI(e);
return t.update(n),
function(e) {
if (e) {
if (e.css === n.css && e.media === n.media && e.sourceMap === n.sourceMap && e.supports === n.supports && e.layer === n.layer) return;
t.update(n = e)
} else t.remove()
}
}
n.exports = function(n, o) {
var a = r(n = n || [], o = o || {});
return function(n) {
n = n || [];
for (var i = 0; i < a.length; i++) {
var c = t(a[i]);
e[c].references--
}
for (var s = r(n, o), d = 0; d < a.length; d++) {
var u = t(a[d]);
0 === e[u].references && (e[u].updater(), e.splice(u, 1))
}
a = s
}
}
},
113: n => {
n.exports = function(n, e) {
if (e.styleSheet) e.styleSheet.cssText = n;
else {
for (; e.firstChild;) e.removeChild(e.firstChild);
e.appendChild(document.createTextNode(n))
}
}
},
208: (n, e, t) => {
t.d(e, {
A: () => c
});
var r = t(601),
o = t.n(r),
a = t(314),
i = t.n(a)()(o());
i.push([n.id, "* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n font-family: Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n background-color: #f4f4f4;\n}\n\n#app {\n max-width: 800px;\n margin: 0 auto;\n padding: 20px;\n}\n\n.title {\n color: #2c3e50;\n text-align: center;\n margin-bottom: 20px;\n padding-bottom: 10px;\n border-bottom: 1px solid #eee;\n}\n\n.content {\n background: white;\n padding: 20px;\n border-radius: 5px;\n box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n}\n\nbutton {\n background: #3498db;\n color: white;\n border: none;\n padding: 10px 15px;\n border-radius: 4px;\n cursor: pointer;\n margin-top: 10px;\n font-size: 16px;\n transition: background 0.3s;\n}\n\nbutton:hover {\n background: #2980b9;\n}\n\n#result {\n margin-top: 20px;\n padding: 15px;\n background: #f9f9f9;\n border-left: 4px solid #3498db;\n display: none;\n}\n\n#result.active {\n display: block;\n animation: fadeIn 0.5s;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}", ""]);
const c = i
},
314: n => {
n.exports = function(n) {
var e = [];
return e.toString = function() {
return this.map((function(e) {
var t = "",
r = void 0 !== e[5];
return e[4] && (t += "@supports (".concat(e[4], ") {")), e[2] && (t += "@media ".concat(e[2], " {")), r && (t += "@layer".concat(e[5].length > 0 ? " ".concat(e[5]) : "", " {")), t += n(e), r && (t += "}"), e[2] && (t += "}"), e[4] && (t += "}"), t
})).join("")
}, e.i = function(n, t, r, o, a) {
"string" == typeof n && (n = [
[null, n, void 0]
]);
var i = {};
if (r)
for (var c = 0; c < this.length; c++) {
var s = this[c][0];
null != s && (i[s] = !0)
}
for (var d = 0; d < n.length; d++) {
var u = [].concat(n[d]);
r && i[u[0]] || (void 0 !== a && (void 0 === u[5] || (u[1] = "@layer".concat(u[5].length > 0 ? " ".concat(u[5]) : "", " {").concat(u[1], "}")), u[5] = a), t && (u[2] ? (u[1] = "@media ".concat(u[2], " {").concat(u[1], "}"), u[2] = t) : u[2] = t), o && (u[4] ? (u[1] = "@supports (".concat(u[4], ") {").concat(u[1], "}"), u[4] = o) : u[4] = "".concat(o)), e.push(u))
}
}, e
}
},
540: n => {
n.exports = function(n) {
var e = document.createElement("style");
return n.setAttributes(e, n.attributes), n.insert(e, n.options), e
}
},
601: n => {
n.exports = function(n) {
return n[1]
}
},
659: n => {
var e = {};
n.exports = function(n, t) {
var r = function(n) {
if (void 0 === e[n]) {
var t = document.querySelector(n);
if (window.HTMLIFrameElement && t instanceof window.HTMLIFrameElement) try {
t = t.contentDocument.head
} catch (n) {
t = null
}
e[n] = t
}
return e[n]
}(n);
if (!r) throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
r.appendChild(t)
}
},
825: n => {
n.exports = function(n) {
if ("undefined" == typeof document) return {
update: function() {},
remove: function() {}
};
var e = n.insertStyleElement(n);
return {
update: function(t) {
! function(n, e, t) {
var r = "";
t.supports && (r += "@supports (".concat(t.supports, ") {")), t.media && (r += "@media ".concat(t.media, " {"));
var o = void 0 !== t.layer;
o && (r += "@layer".concat(t.layer.length > 0 ? " ".concat(t.layer) : "", " {")), r += t.css, o && (r += "}"), t.media && (r += "}"), t.supports && (r += "}");
var a = t.sourceMap;
a && "undefined" != typeof btoa && (r += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(a)))), " */")), e.styleTagTransform(r, n, e.options)
}(e, n, t)
},
remove: function() {
! function(n) {
if (null === n.parentNode) return !1;
n.parentNode.removeChild(n)
}(e)
}
}
}
}
},
e = {};
function t(r) {
var o = e[r];
if (void 0 !== o) return o.exports;
var a = e[r] = {
id: r,
exports: {}
};
return n[r](a, a.exports, t), a.exports
}
t.n = n => {
var e = n && n.__esModule ? () => n.default : () => n;
return t.d(e, {
a: e
}), e
}, t.d = (n, e) => {
for (var r in e) t.o(e, r) && !t.o(n, r) && Object.defineProperty(n, r, {
enumerable: !0,
get: e[r]
})
}, t.o = (n, e) => Object.prototype.hasOwnProperty.call(n, e), t.nc = void 0;
var r = t(72),
o = t.n(r),
a = t(825),
i = t.n(a),
c = t(659),
s = t.n(c),
d = t(56),
u = t.n(d),
p = t(540),
l = t.n(p),
f = t(113),
v = t.n(f),
m = t(208),
b = {};
b.styleTagTransform = v(), b.setAttributes = u(), b.insert = s().bind(null, "head"), b.domAPI = i(), b.insertStyleElement = l(), o()(m.A, b), m.A && m.A.locals && m.A.locals, document.addEventListener("DOMContentLoaded", (() => {
console.log("页面已加载");
const n = document.getElementById("clickBtn"),
e = document.getElementById("result");
n && e && n.addEventListener("click", (() => {
const n =
你好,Webpack! 当前时间: ${(new Date).toLocaleTimeString()}
;
e.textContent = n, e.classList.add("active")
}))
}))
})();
修改一下,添加devtool 选项:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口文件
entry: './src/index.js',
// 添加devtool配置来启用source map
devtool: 'source-map', // 这将生成完整的source map文件
// 输出配置
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true // 每次构建前清理dist文件夹
},
// ... 其余配置保持不变 ...
};
这样的话我们
❯ npm run build
这样可以生成一个bundle.js.map
文件
反解 webpack
有很多工具,这里使用reverse-sourcemap
npm install --global reverse-sourcemap
看一下用法
❯ reverse-sourcemap -h
reverse-sourcemap - Reverse engineering JavaScript and CSS sources from sourcemaps
Usage: reverse-sourcemap [options] <file|directory>
-h, --help Help and usage instructions
-V, --version Version number
-v, --verbose Verbose output, will print which file is currently being
processed
-o, --output-dir String Output directory - default: .
-M, --match String Regular expression for matching and filtering files -
default: \.map$
-r, --recursive Recursively search matching files
Version 1.0.4
直接逆向出来
❯ reverse-sourcemap --output-dir ./ bundle.js.map
效果如下
可以看到与我们原本项目结构相比,结构基本一致
效果还是不错的
所以说有的时候,我们会发现一些网站泄露了一些”map”文件,我们可以达到意想不到的利用