简介:这份火狐浏览器Firefox 49.0渗透插件集成版面向网络安全从业者、Web安全评估人员及渗透测试学习者,将常用测试扩展与浏览器环境打包整合,省去逐一手动查找、安装与版本搭配的麻烦,可用于信息收集、漏洞扫描、请求调试和漏洞验证等场景。压缩包共700个文件,约62.98MB,主要由xpi扩展包、dll动态库、js/jsm脚本、xul界面描述、properties与dtd本地化配置,以及jsonlz4、sqlite等缓存和配置数据构成,并包含png、ico等界面图标资源,整体保留较完整的浏览器运行目录与扩展配置。已有1271人学习或下载。对于需要快速搭建Web安全测试环境的人来说,这份整合包能减少工具准备时间,便于集中熟悉常用渗透插件的功能与配合方式;其目录结构也保留了扩展和浏览器组件的组织关系,适合教学演示、实验环境搭建及后续按需整理与扩展。
1. 一套 2018 年的火狐 Firefox 49.0 渗透插件集成版,今天还能在本地靶场里跑起来吗
第一次拿到这个包的人,情绪大多会经历两次变化。解压看到一个 49.0 的火狐浏览器 Firefox 目录,加上二十几个装在extensions/下的老扩展,很容易觉得“这不就是十年前的东西”;可等你在自建靶场里试过一次请求参数改写、一次多轮编码还原之后,又会发现新版浏览器反而做不顺手:57 之后那条扩展体系把大半测试面板挡在了门外。这套集成版真正解决的是“旧浏览器加旧扩展”的组合问题,不用你逐个找版本、试兼容。它适合做授权范围内安全测试的人、打 CTF 的人、维护老 Web 系统需要复现历史行为的人,以及想搞清楚 legacy 扩展到底怎么加载的工程师。下面的操作都在隔离环境里做,别拿它去碰没有书面授权的目标。
2. 版本选型:为什么锁死 49.0 而不是装个新版
2.1 legacy 扩展与 WebExtensions 的分水岭
Firefox 在 48 前后同时跑着两套扩展体系:一套是 XUL 覆盖型(legacy),扩展本质是一个带install.rdf的 XPI 包,能通过 XUL overlay 往浏览器主窗口注入按钮、侧边栏和菜单,能直接拿到contentWindow操作页面 DOM,还能调用 XPCOM 组件读写本地文件、发原始 socket 请求。另一套是 WebExtensions,权限声明在manifest.json里,能力被限制在浏览器给出的 API 白名单内。
安全测试类插件恰好是 legacy 体系的重度用户。原因很实际:它们要改请求头、要看响应原文、要在页面上挂悬浮面板、要维护自己的历史记录数据库,这些在 WebExtensions 里要么做不到,要么得绕一大圈。57 版本把 legacy 支持整体移除之后,这批插件里能迁移的没几个,大量在 2018 年前后停止维护的直接就废了。
所以集成包锁 49.0 不是图省事,是被插件反推出来的结论。49.0 处在两条体系并存的窗口期,legacy 扩展还能完整加载,同时又有了后来才普及的一些开发者工具改进。你手上这个包里的二十几个扩展,绝大多数是install.rdf格式,把浏览器升到 57 以上,它们会在启动时集体消失,界面上只留一句“已禁用不兼容的扩展”。
判断一个扩展属于哪套体系,不用打开浏览器:
# 逐个看压缩包里的元信息文件,有 install.rdf 的是 legacy,有 manifest.json 的是 WebExtensions for f in ~/lab/ff49/profile/extensions/*.xpi; do echo "== $(basename "$f")" unzip -l "$f" | grep -E "install\.rdf|manifest\.json" done这段命令只做一件事:列出每个 XPI 里是否存在那两份关键文件。输出里同时出现两个的扩展一般是双体系兼容包,只出现manifest.json的说明它是后来补进去的 WebExtensions,在 49.0 上能不能跑取决于它用了哪些 API。
2.2 签名校验在正式版里的那道墙
这一步最容易出问题。很多人知道要在about:config里把xpinstall.signatures.required改成false,改完发现 还是装不上自制的 XPI,于是开始怀疑扩展包坏了。真正的原因是这个开关在官方正式版(release 渠道)里从 48 起就不再生效,只有 Developer Edition、Nightly、ESR 和 unbranded 构建才认它。
集成包通常用两种方式绕开:一是直接带一个 unbranded 构建,二是把扩展预置进 profile,开机即加载,根本不走安装流程。要先确认手上这份属于哪种,读取应用目录下的application.ini比在界面里翻快得多:
# 看构建标识,unbranded 构建的 SourceRepository 和正式版不一样 cat ~/lab/ff49/app/firefox/application.ini | grep -E "^(Name|Version|BuildID|SourceRepository)"Version是 49.0 说明版本对得上,SourceRepository里带 unbranded 字样说明签名开关可用。如果这里是正式版,那就别折腾about:config了,老老实实走 profile 预置,把 XPI 复制到profile/extensions/下,文件名改成扩展 ID,浏览器启动时会自己扫描注册。
预置这一步有个细节:legacy 扩展的 ID 在install.rdf的<em:id>标签里,通常长得像{a1b2c3d4-...}或者hackbar@example.net这种形式,不是文件名。文件名和 ID 对不上的话,浏览器会认为这是一个新扩展然后重新分配 ID,结果就是每次启动都多一份记录。
2.3 用独立 profile 做环境隔离
这一条是安全习惯问题,比技术细节更重要。测试浏览器和日常浏览器共用 profile 会互相污染:日常浏览产生的 Cookie 和缓存会混进测试记录,测试里改的偏好项又会反过来影响日常使用,更麻烦的是两个实例抢同一个单例锁,先启动的那个活着,后面点启动器根本没反应。
隔离靠三个启动参数就能做到。-profile显式指定目录,-no-remote告诉进程不要去找已有的实例,-P在需要多套配置时用来切名字。注意-no-remote在新版本里的行为有过调整,但在 49.0 上是可靠可用的。
# 日常不这么启动,只在脚本里用;-no-remote 是避免和日常实例合并的关键 ~/lab/ff49/app/firefox/firefox -no-remote -profile ~/lab/ff49/profile我在做这类环境时一律把测试浏览器放在独立目录、独立用户下,退出时不会去动默认 profile 的任何文件。听起来有点过度,但等你遇到一次“测试把日常浏览器的登录态覆盖了”的情况,就会知道这个习惯值多少钱。
3. 目录骨架与 profile 预置:把集成版跑起来的四步
3.1 目录骨架与启动器
先把结构定下来,后面所有脚本都按这个路径写,换机器只要改一个变量。我的习惯是分成五块:应用本体、profile、基线备份、日志、测试用例。基线备份这一块很多人省掉,省掉的代价是某次测试把 profile 搞坏之后没法快速回到干净状态。
LAB="$HOME/lab/ff49" mkdir -p "$LAB"/{app,profile/extensions,baseline,logs,cases} # 应用包解到 app/ 下,保持 firefox 这个目录名,脚本里就不用改路径了 tar -xf firefox-49.0.en-US.linux-x86_64.tar.bz2 -C "$LAB/app" # 首次把集成包里的扩展拷进 profile(集成包一般已给 extensions 目录,按实际结构调整) cp -a /path/to/integrated/extensions/. "$LAB/profile/extensions/" # 做完立刻打一份基线,之后每次测试都从基线拉新的 profile cp -a "$LAB/profile" "$LAB/baseline/profile-clean"启动器单独写成脚本,不要每次手敲长命令:
#!/usr/bin/env bash # ~/lab/ff49/start.sh set -euo pipefail LAB="$HOME/lab/ff49" PROFILE="$LAB/profile" APP="$LAB/app/firefox/firefox" # 上一次非正常退出会留下 parent.lock,存在就直接报警,别硬启 if [ -e "$PROFILE/parent.lock" ]; then echo "profile locked: $PROFILE/parent.lock" >&2 exit 1 fi exec "$APP" -no-remote -profile "$PROFILE" "$@" >> "$LAB/logs/launch.log" 2>&1set -euo pipefail让脚本在任意一步失败时立刻退出,避免半启动状态。parent.lock的检查能挡掉一大类“点了没反应”的问题,这个文件是浏览器单例锁的标记,进程被强杀时会残留。$@透传参数,后面要临时加-setDefaultBrowser之类的开关不用改脚本。
3.2 user.js 里必须写死的偏好项
about:config里手改的东西存在prefs.js,浏览器退出时可能被重写,多人协作时更是没法复现。把关键项写进 profile 根目录的user.js,每次启动都会被强制写入,等于给这份测试环境加了一层声明式的配置。
// ~/lab/ff49/profile/user.js // 关闭更新:这一条是保命的,被升级到 57+ 之后所有 legacy 扩展直接失效 user_pref("app.update.auto", false); user_pref("app.update.enabled", false); user_pref("app.update.url", ""); // 签名校验开关,仅在 unbranded / Developer Edition 构建下生效,正式版里改它没用 user_pref("xpinstall.signatures.required", false); // 允许加载 profile / 应用目录 / 系统目录下所有来源的扩展,并阻止自动禁用 user_pref("extensions.enabledScopes", 15); user_pref("extensions.autoDisableScopes", 0); // 测试场景不要磁盘缓存,避免拿到旧响应导致复现结果对不上 user_pref("browser.cache.disk.enable", false); user_pref("browser.privatebrowsing.autostart", false); // 高 DPI 屏上老扩展的侧边栏会错位,把缩放钉死 user_pref("layout.css.devPixelsPerPx", "1.0"); user_pref("devtools.chrome.enabled", true);几个参数值得单独说清楚。extensions.enabledScopes是位掩码,控制哪些安装位置被允许加载,15表示全开;extensions.autoDisableScopes默认会把来自非 profile 位置的扩展自动禁用,预置扩展被判定成“来源未知”时就会被静默关掉,设成0才能避免。app.update.url置空是为了让更新检查直接失败,只关auto有时候还会走一次检查再弹窗。
| 偏好项 | 作用 | 不设的后果 |
|---|---|---|
app.update.auto/app.update.enabled | 挡住自动升级 | 某次启动后被升到 57+,扩展全废 |
app.update.url | 断掉更新检查入口 | 仍会发起检查、弹提示 |
xpinstall.signatures.required | 关闭签名校验 | 仅 unbranded 构建有效,正式版无效 |
extensions.enabledScopes | 放开扩展加载位置 | 预置目录里的扩展不加载 |
extensions.autoDisableScopes | 阻止来源未知自动禁用 | 扩展显示但灰掉不可用 |
browser.cache.disk.enable | 关磁盘缓存 | 复现结果和线上不一致 |
layout.css.devPixelsPerPx | 固定缩放比例 | 老扩展面板错位、按钮点不到 |
3.3 扩展清单自检脚本
profile 里的扩展到底注册上没有、ID 和文件名对不对得上,光靠界面看太慢。写个小脚本把install.rdf里的关键字段抽出来,输出一张对照表。
# ~/lab/ff49/check_ext.py import re import zipfile from pathlib import Path EXT_DIR = Path.home() / "lab" / "ff49" / "profile" / "extensions" def parse_xpi(p: Path) -> dict: """legacy 扩展的元信息在 install.rdf,用正则粗取,避免引入额外解析依赖""" info = {"file": p.name, "id": "", "name": "", "version": "", "kind": "unknown"} with zipfile.ZipFile(p) as zf: names = zf.namelist() if "install.rdf" in names: raw = zf.read("install.rdf").decode("utf-8", errors="ignore") info["kind"] = "legacy" for tag in ("id", "name", "version"): m = re.search(rf"<em:{tag}[^>]*>(.*?)</em:{tag}>", raw, re.S) if m: info[tag] = m.group(1).strip() elif "manifest.json" in names: info["kind"] = "webext" return info def main() -> None: rows = [parse_xpi(p) for p in sorted(EXT_DIR.glob("*.xpi"))] bad = [r for r in rows if not r["id"] and r["kind"] == "legacy"] for r in rows: flag = "" if r["id"] or r["kind"] != "legacy" else " <-- 元信息读取失败" print(f'{r["kind"]:8s} {r["id"]:40s} {r["version"]:10s} {r["file"]}{flag}') print(f"\n总计 {len(rows)} 个,异常 {len(bad)} 个") if __name__ == "__main__": main()errors="ignore"是因为不少老扩展的install.rdf是 Latin-1 或带 BOM 的 UTF-8,严格解码会直接抛异常。kind字段用来区分体系,输出里webext的条目在 49.0 上要额外验证。最后打印的异常数量是给自己看的:只要不归零,就说明有扩展放进去但根本没被识别,多半是文件名和<em:id>对不上,手工改成 ID 再重启。
3.4 首次启动的验收动作
启动之后不要急着开测试页面,先走三步验收。about:support里的“扩展”列表应该和脚本输出的数量一致,少一个就说明那句“来源未知已禁用”生效了。about:config搜app.update.enabled确认是false,确认user.js真的被读进去了。about:addons里看扩展是否处于启用态,灰色的点一下启用(如果点不动,回到autoDisableScopes)。
三步都过之后再打开一个本地靶场页面,随便点几个测试面板出来,确认 UI 注入成功。这一步能提前暴露高 DPI 缩放导致的面板错位问题,省得正式测试时手忙脚乱。
4. 插件能力拆解:请求改写、编码还原、身份态编辑
4.1 请求参数与请求头的可控改写
这类面板的核心作用是让一次请求在发出前被改掉,改完立刻能不能看到结果。它和有拦截能力的测试工具不是一回事:集成包里的插件大多工作在浏览器内核提供的请求钩子上,改的是浏览器自己发出的请求,所以只对你当前页面发起的流量有效,不覆盖系统里其他客户端。
实际用的时候我关注三件事。改哪一层,是改 URL 查询串、改 POST 体、还是改请求头;改完怎么确认,面板上看到的是不是最终发出去的那一份;以及重复请求怎么复用,能不能把一次改好的请求存下来反复打。第三点最影响效率,手工改十几次参数只为了找边界,不如把请求存成模板。
| 改写对象 | 常见用途 | 需要留意的点 |
|---|---|---|
| URL 查询参数 | 边界值、类型混淆、重复参数 | 参数被编码两次时,面板显示的和服务端收到的可能不同 |
| 请求体 | JSON 结构变形、表单字段增删 | Content-Length由浏览器重算,手改头没用 |
| 请求头 | 客户端标识、来源标识、内容类型 | 部分头是浏览器受保护头,改了会被丢弃 |
| Cookie | 身份态变更、越权验证 | 与浏览器自身 Cookie 存储会互相覆盖 |
“改了没生效”绝大多数是受保护头的问题。Firefox 里有一批请求头由内核直接管理,扩展写入会被静默丢弃,表现为面板上显示改成功、服务端日志里还是原值。遇到这种情况先换一个非受保护的头做对照实验,能改说明链路是通的,改不了就是权限层的问题。
4.2 多轮编码的还原链
编码转换面板看起来最没技术含量,实际是复现问题时最常用的一块。真实场景里一个参数可能经过 URL 编码、Base64、再做一次 URL 编码,手工一层层剥又慢又容易出错。写段脚本把常见组合跑一遍,比在面板里点十次可靠。
# ~/lab/ff49/decode_chain.py import base64 import urllib.parse def tries(raw: str) -> None: """对同一条输入并行尝试几种常见解码路径,输出最可能的几种结果""" cands = [] # 路径 1:URL 解码两层 s = raw for _ in range(2): s = urllib.parse.unquote(s) cands.append(("url x2", s)) # 路径 2:先 URL 解码,再当成 Base64 try: b = base64.b64decode(urllib.parse.unquote(raw) + "===", validate=False) cands.append(("url -> b64", b.decode("utf-8", errors="replace"))) except Exception: pass # 路径 3:直接 Base64 再 URL 解码(顺序反过来很常见) try: b = base64.b64decode(raw + "===", validate=False) cands.append(("b64 -> url", urllib.parse.unquote(b.decode("utf-8", errors="replace")))) except Exception: pass for name, val in cands: printable = sum(c.isprintable() for c in val) / max(len(val), 1) print(f"[{name}] 可打印率={printable:.2f} {val[:120]}") if __name__ == "__main__": import sys tries(sys.argv[1])补===是因为 Base64 的填充位经常在传输中被省掉,严格解码会直接报错,而validate=False配合补位能容错。可打印率这个指标是我自己加的判断依据:真正的明文可打印率通常在 0.95 以上,低于 0.7 基本说明解码路径选错了,别浪费时间去看内容。
在浏览器面板里做同样的操作时,注意面板的默认字符集。老插件很多默认按 Latin-1 处理,遇到 GBK 或 UTF-8 的中文会显示成乱码,这时候不是解码错了,是显示层错了。判断方法是看响应头的Content-Type里有没有charset,有就按它来,没有就手动切到 UTF-8 试一次、GBK 试一次,两次都不对再考虑是不是压缩流没解开。
4.3 Cookie 与身份态编辑
身份态编辑在越权验证里用得最多:拿一个低权限账号的会话,改掉其中的角色标识或租户标识,看服务端有没有重新校验。这个动作必须在授权范围内做,而且最好在自建靶场上先把手感练熟。
操作上有两个容易翻车的点。一是编辑器和浏览器自身 Cookie 存储会互相覆盖:你在编辑器里改了值,页面刷新时浏览器可能用内存里的旧值把它盖回去,表现为“改了没反应”。稳妥做法是改完之后强制刷新整个页面而不是局部刷新,或者先清掉该站点的会话再写入。
二是HttpOnly标记。带这个标记的 Cookie 在页面脚本里读不到,但浏览器扩展工作在更高的权限层,能改;改完之后如果服务端做了二次校验(比如把 Cookie 和某个请求头做绑定校验),就会看到“Cookie 对了但请求还是被拒”。这种情况别急着怀疑工具,先看请求头里有没有配套的标识需要同步改。
实际操作顺序我一般固定成:先记录原始 Cookie 全文,再改单项,改完立刻发一次最小请求看响应码,最后把结果连同原始值一起记到用例文件里。少记一步,回头复现就得重来。
4.4 页面结构与指纹信息采集
这类插件做的是被动信息收集:页面用了哪些前端框架、加载了哪些脚本、有没有暴露调试入口、响应头里带了什么技术标识。它不发起额外请求,所以对目标的影响最小,通常在测试最开始用它摸一遍底。
看输出的时候重点不在“识别出了什么框架”,而在“识别结果和实际行为是否一致”。比如页面识别成某个前端框架的旧版本,那就要去看这个版本对应的已知行为特征;识别出调试模式开着,那就要确认这个入口是不是只在内网可达。这些结论要靠人判断,工具只负责把线索摊开。
采集结果我习惯导成一份结构化清单,跟后面的用例编号对上:
| 字段 | 来源 | 用途 |
|---|---|---|
| 响应头技术标识 | 响应头 | 判断服务端组件与版本线索 |
| 脚本与样式资源列表 | 页面 DOM | 找未压缩源码、找调试入口 |
| 表单字段与提交地址 | 页面 DOM | 生成后续请求模板 |
| 客户端存储键名 | 本地存储 / 会话存储 | 判断身份态存放位置 |
5. 避坑与排查:五条踩过的记录
5.1 扩展全部变灰不可用
现象:启动后about:addons里扩展都在,但全是禁用态,手动点启用也没反应,重启后又变回去。
原因:extensions.autoDisableScopes保持默认值,浏览器把从 profile 外部复制进来的扩展判定为来源未知,启动时自动禁用;同时extensions.enabledScopes没放开对应位置。
解决:在user.js里同时写死extensions.enabledScopes = 15和extensions.autoDisableScopes = 0,然后删掉 profile 下的extensions.json让浏览器重建索引。重建这一步别省,索引文件里记着旧的禁用状态,光改偏好项不重建还是灰的。
5.2 改了签名开关仍然装不上 XPI
现象:about:config里xpinstall.signatures.required已经是false,拖入自制 XPI 依然提示签名无效。
原因:这个开关在 release 渠道从 48 起就不生效,只有 unbranded、Developer Edition、Nightly、ESR 才认。集成包如果带的是正式版构建,怎么改都没用。
解决:先按 2.2 的方法读application.ini确认构建类型。是正式版就放弃安装流程,改走预置:把 XPI 放进profile/extensions/,文件名改成<em:id>里的值,重启加载。这条我踩过不止一次,后来养成习惯,拿到包第一件事就是看SourceRepository。
5.3 中文内容显示成乱码,解码结果对不上
现象:编码转换面板解出来的内容一半是问号一半是方块,换几个算法都不对。
原因:面板默认字符集不是 UTF-8,或者响应体本身是压缩过的,直接按文本解当然乱。
解决:先看响应头Content-Type的charset,有就按它设;没有就依次试 UTF-8 和 GBK。如果整个响应看着像二进制,先确认是不是带Content-Encoding,需要先解压再解码。判断顺序别搞反,先字符集后算法,反了会浪费很多时间。
5.4 请求历史莫名其妙丢了一段
现象:测试做到一半,面板里的历史记录少了一截,前面的请求找不回来。
原因:browser.cache.disk.enable开着,或者中途开了隐私窗口,隐私窗口关闭时会清掉该会话的全部记录;也可能是插件的历史存储写在内存里,进程重启就没了。
解决:user.js里关掉磁盘缓存,全程用普通窗口,关键请求随手导出到cases/目录。我现在的要求是“凡是跑出结果的请求,必须落一份文件”,不落就等于没做,回头写报告只能靠回忆。
5.5 点了启动器没反应,或者启动了两个实例
现象:双击启动脚本,任务栏没变化;或者过一会儿发现开了两个窗口,profile 互相覆盖。
原因:上一次非正常退出留下parent.lock,新进程认为已有实例在跑;或者漏了-no-remote,新进程找上了日常实例并把自己合并进去。
解决:3.1 的启动脚本里已经加了锁文件检查,看到锁就手工确认进程是否真的还在,不在就删锁重来。-no-remote必须在,别嫌它麻烦。
6. 进阶:把集成版做成可交付的测试基线
前面几步做完,你手上是一个“能跑”的环境;再往前一步,是一个“换台机器十分钟能重建出来”的环境。这两者的差别在于有没有把可变部分全部提取出来。我在带人做这类环境时,要求提交三样东西:user.js、扩展清单、以及一个启动前体检脚本。
体检脚本干的事很朴素——在浏览器启动前把不该出现的情况拦住:
# ~/lab/ff49/preflight.py from pathlib import Path LAB = Path.home() / "lab" / "ff49" PROFILE = LAB / "profile" REQUIRED = { "app.update.auto": "false", "app.update.enabled": "false", "extensions.enabledScopes": "15", "extensions.autoDisableScopes": "0", "browser.cache.disk.enable": "false", } def check() -> int: errs = [] prefs = PROFILE / "prefs.js" text = prefs.read_text(encoding="utf-8", errors="ignore") if prefs.exists() else "" for k, v in REQUIRED.items(): if f'user_pref("{k}", {v});' not in text: errs.append(f"偏好项缺失或值不符: {k} 应为 {v}") if not (PROFILE / "extensions").is_dir(): errs.append("扩展目录不存在") if len(list((PROFILE / "extensions").glob("*.xpi"))) < 10: errs.append("扩展数量异常,低于预期") for e in errs: print("[FAIL]", e) return 1 if errs else 0 if __name__ == "__main__": raise SystemExit(check())prefs.js只有浏览器启动过一次之后才会生成,所以这个脚本要在基线打完、至少启动过一轮之后再用。偏好的值匹配用的是精确字符串,false不能写成False,user_pref的第二个参数是布尔还是字符串直接影响匹配,这一步我调过好几次才对上。
第二个习惯是扩展完整性校验。集成包里的 XPI 一旦被替换或损坏,界面上往往只是某个按钮点了没反应,很难定位。算一遍哈希存进基线目录,每次换机器重建后比对:
# 生成基线哈希清单,存进 baseline/,和 profile 一起版本管理 cd ~/lab/ff49/profile/extensions sha256sum *.xpi | sort -k2 > ~/lab/ff49/baseline/extensions.sha256 # 重建环境后比对,只输出差异行 sha256sum -c ~/lab/ff49/baseline/extensions.sha256 2>&1 | grep -v ": OK$"最后一件值得做的是把每次测试的 profile 快照留存。不是备份整个目录,太多了;只留prefs.js、extensions.json和cases/下的请求记录,压缩成一份带时间戳的包。出问题的时候能快速回答“当时是什么配置”,这是唯一可靠的后悔药——界面上的状态是会被下一次启动改掉的,磁盘上留痕的东西不会。
这套流程我吃过亏才固化下来。早些年搭测试环境图快,改完about:config就直接开跑,跑出结果后浏览器崩了一次,重启发现更新检查把版本推上去了,二十几个扩展集体消失,一整天的结果没法复现。从那以后我每次拉起这类老浏览器环境,都强制走一遍preflight.py加哈希比对,宁可多花两分钟。希望帮到你。
本文还有配套的精品资源,点击获取