BrowserLaboratory/wxl-template
GitHub: BrowserLaboratory/wxl-template
一个无需服务器的纯前端 Web 漏洞利用 CTF 训练平台模板,通过 WebAssembly 在浏览器中模拟真实后端环境。
Stars: 0 | Forks: 0
# Web eXploitation Laboratory (WXL)
[](https://github.com/BrowserLaboratory/wxl-template/actions/workflows/quality-gates.yml)
[](LICENSE)
[](https://vitepress.dev)
[](https://pnpm.io)
## 概述
**Web eXploitation Laboratory (WXL)** 是一个 CTF 风格的 Web 漏洞利用训练平台。每个挑战都在浏览器中完全运行:WebAssembly 模拟了真实的后端环境,因此该平台无需任何服务器基础设施即可部署和使用。
### 核心功能
- **纯前端执行**:Service Worker 拦截 HTTP 请求并在浏览器中模拟后端行为。
- **多种后端 runtime**:支持 Python Flask / FastAPI(通过 Pyodide)以及 PHP(通过 php-wasm)。
- **加密的虚拟文件系统**:flag 和应用资产在 AES-GCM-256 加密下存储,防止直接读取。
- **静态部署**:构建输出为纯静态文件,可托管于任何静态服务(GitHub Pages、Cloudflare Pages 等)。
## 技术栈
| 层级 | 技术 |
|------|------|
| 文档框架 | [VitePress](https://vitepress.dev) 2.0.0-alpha.16 |
| UI 框架 | [Vue 3](https://vuejs.org) 3.5 + [UnoCSS](https://unocss.dev) |
| 状态管理 | [Pinia](https://pinia.vuejs.org) 3 |
| Python runtime | [Pyodide](https://pyodide.org) 0.29 |
| PHP runtime | [php-wasm](https://github.com/seanmorris/php-wasm) |
| WASM 模块 | Rust 2021 + [wasm-pack](https://rustwasm.github.io/wasm-pack/) |
| 攻击会话追踪 | IndexedDB (`idb` 包) attack-session 持久化 |
| 包管理器 | [pnpm](https://pnpm.io) 10.28 |
## 前置条件
- **Node.js** >= 22.6 — `challenge:keygen`、`create:challenge`、`challenge:validate` 和 `challenge:analyze` 通过 `node --experimental-strip-types` 运行,这在早期版本中不可用。其余 TypeScript 脚本通过内置的 `tsx` 运行,没有此版本限制。CI 在 Node 24 上构建。
- **pnpm** >= 10 (`npm install -g pnpm`)
- **Rust** 工具链(通过 [rustup](https://rustup.rs/) 安装)
- **wasm-pack** (`cargo install wasm-pack`) — 不是包依赖项;请将其安装到你的 Rust 工具链中
- **wasm-tools** (`cargo install wasm-tools` 或 `pnpm wasm:tools`) — `pnpm challenge:verify` 的 L2 阶段所需,该阶段会对生成的 payload 运行 `wasm-tools validate`。`pnpm challenge:keygen` 也会在 strip 和 mutate 过程中使用它,但如果不存在,则会降级为警告。
- **Chromium for Playwright** — 在首次运行 `pnpm challenge:verify` 或 `pnpm test:smoke` 之前需要。在 `pnpm install` 之后,使用以下命令安装一次浏览器二进制文件:
pnpm exec playwright install chromium
## 快速开始
```
# 1. Clone the project
git clone https://github.com/BrowserLaboratory/wxl-template.git
cd wxl-template
# 2. 安装 Node.js 依赖
pnpm install
# 3. 构建 WASM 模块并启动 dev server
pnpm dev
```
开发服务器默认在 `http://localhost:5173` 启动。
## 可用脚本
| 命令 | 描述 |
|------|------|
| `pnpm dev` | 构建 WASM 模块并启动开发服务器 |
| `pnpm build` | 构建 WASM 模块并输出静态站点 |
| `pnpm docs:dev` | 仅启动 VitePress 开发服务器(跳过 WASM 构建) |
| `pnpm docs:build` | 仅构建 VitePress 静态站点 |
| `pnpm docs:preview` | 预览构建好的静态站点 |
| `pnpm test` | 运行 TypeScript / JavaScript 单元测试 (Vitest)。直接运行会停留在 watch 模式 — 使用 `pnpm test --run` 进行单次运行 |
| `pnpm test:smoke` | 针对构建好的站点运行 Playwright 冒烟测试 |
| `pnpm wasm:build` | 构建所有 Rust WASM 模块 |
| `pnpm wasm:test` | 运行 Rust 单元测试 (`cargo test`) |
| `pnpm wasm:tools` | 尝试将 `wasm-tools` 安装到 Rust 工具链中;它会屏蔽失败并总是以退出码 0 退出,因此请通过 `wasm-tools --version` 进行确认 |
| `pnpm fork:init` | 在 fork 后重写项目身份(作者、GitHub URL,以及可选的 VitePress `base`)。需要 `--author` 和 `--repo`;包名仅在带有 `--name` 或 `--rebrand` 时更改,且 `deploy.yml` 中的 `SITE_BASE` 永远不会被修改 |
| `pnpm challenge:keygen` | 为每个挑战生成加密的 WASM 模块 |
| `pnpm create:challenge` | 根据 flag 脚手架生成新挑战;必须提供 `--name `,直接运行会以退出码 1 退出并显示用法说明 |
| `pnpm challenge:validate` | 验证每个挑战的 frontmatter 和文件布局 |
| `pnpm challenge:analyze` | 报告挑战的内容和配置 |
| `pnpm challenge:retype` | 修改现有挑战的后端 / 难度 / 标签 / 分类 |
| `pnpm challenge:verify` | 对挑战运行分层验证门控(L1 lint,L2 构建,L3 Playwright e2e) |
| `pnpm challenge:verify:blind` | 独立运行 L4 盲解子程序(也可通过 `pnpm challenge:verify --blind` 触发) |
| `pnpm challenge:verify:cross` | 仅限维护者的 L4 多智能体交叉检查 — 针对 `claude,codex,gemini` 运行盲测门控并汇总判定结果 |
| `pnpm prepare` | 安装 git hooks (`simple-git-hooks`);在 `pnpm install` 之后自动运行 |
## 架构
```
Browser
├── VitePress site (Vue 3 + UnoCSS)
│ ├── Challenge pages (Markdown + YAML frontmatter)
│ └── IndexedDB (attack-session persistence + tool state)
├── Service Worker (docs/public/challenge-sw.js)
│ └── Intercepts HTTP requests and routes them to the matching WASM runtime
└── WASM runtimes
├── virtual-fs Encrypted virtual filesystem (Rust)
├── asgi-bridge Python ASGI/WSGI bridge layer (Rust)
├── wxlsh-parser wxlsh terminal command parser and native commands (Rust)
├── python-bridge Pyodide integration (TypeScript)
└── php-bridge php-wasm integration (TypeScript)
```
这三个 Rust crate 位于 `chall-wasm/` 目录下,由 `pnpm wasm:build` 构建。
### 请求流程
1. 用户与挑战页面交互,触发对“后端”的 HTTP 请求。
2. Service Worker 拦截请求,并根据挑战配置将其路由到正确的 runtime。
3. Python runtime 或 PHP runtime 处理请求并返回 HTTP 响应。
4. 挑战页面渲染结果。
### 挑战配置格式
每个挑战都是一个带有 YAML frontmatter 块的 Markdown 文件,用于声明其配置:
```
---
title: Door Is Open # required
layout: challenge # selects the challenge UI; without it the page renders as a plain docs page
backend: fastapi # required — flask | fastapi | php
app: app.py # required — path relative to the challenge's src/ root
difficulty: easy
category: web
packages: [] # extra Python packages to install via micropip
tools: [ browser, network, repeater, code ] # omit the field to get exactly these four;
# Terminal appears only when you list it, and
# browser is added back if you leave it out
source_visible: false # true = white-box, false = black-box (default)
wasmModule: /challenge/door-is-open/runtime.wasm # produced by keygen
---
```
挑战源文件通过扫描挑战的 `src/` 目录来获取。出于兼容性考虑,仍然接受传统的 `fs:` 映射,但验证器会发出弃用警告 — 新挑战 SHOULD 依赖于 `src/` 扫描。
## 部署
构建输出位于 `.vitepress/dist/` 中,作为纯静态文件,可以部署到任何静态托管服务。
### 构建步骤
```
# 1. 安装依赖
pnpm install
# 2. 完整构建 (WASM + keygen + VitePress)
pnpm build
```
### 部署到 GitHub Pages
本仓库提供了一个可直接使用的工作流文件 [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) — 直接使用它,而不是自己编写。其结构如下:
- **触发器**:推送 `v*` 发布标签,以及手动触发 `workflow_dispatch`。推送到 `main` 分支不会触发部署。
- **Pages 来源**:采用 GitHub Actions 部署方法(`actions/upload-pages-artifact` + `actions/deploy-pages`)。没有 `gh-pages` 分支。
- **工具链**:Node 24 和 SHA 锁定的 `wasm-pack` 0.14.0,与 `release.yml` 匹配。
- **Base path**:构建步骤设置 `SITE_BASE: /wxl-template/`。
`SITE_BASE` 是使项目站点正常工作的关键。VitePress 会将其嵌入到每个资产 URL 中,因此从 `https://.github.io//` 提供服务的站点需要设置 `SITE_BASE: //` — 如果没有它,部署的页面会从域根目录请求资产,导致所有资产都出现 404 错误。仅在部署工作流中设置它;不设置它可使本地和 CI 构建的根目录保持在 `/`。
有两个设置位于 GitHub UI 中,而不是在本仓库中:
1. **Settings → Pages → Source** 必须设置为 **GitHub Actions**。
2. **`github-pages` 环境**必须允许来自 `v*` 标签的部署(除了授权 `workflow_dispatch` 运行的 `main` 分支之外)。如果没有该规则,标签触发的部署作业将被拒绝。
在 fork 之后,运行带有必需标志的 `fork:init` 以重写项目身份:
```
pnpm fork:init --author "" --repo /
```
它会重写 `package.json` 和 GitHub URL。**它不会修改 `SITE_BASE`。** 该脚本拒绝覆盖现有的 `.github/workflows/deploy.yml` — 而 fork 出来的仓库总是包含该文件 — 因此它会打印 `left untouched` 并继续执行,保留 `SITE_BASE: /wxl-template/` 不变。你需要自己将该值修改为 `//`,否则部署将完全如上所述出现 404 错误。
示例中故意省略了 `--base`:在缺少该标志的情况下,脚本不会修改 `.vitepress/config.mts`,因此环境条件判断 `base: process.env.SITE_BASE ?? '/'` 得以保留,`SITE_BASE` 仍然掌控全局。如果传递 `--base //`,则会用硬编码的字面量替换该行,此后 `SITE_BASE` 将不再影响构建。不要试图使用 `--base none` 来期望获得环境条件判断的形式 — `none` 会直接删除 `base` 声明,因此 VitePress 会回退到 `/`,而 `SITE_BASE` 将变得无效,这正是上述描述的 404 情况。
### 部署到 Cloudflare Pages
1. 在 Cloudflare Pages 上创建一个新项目并关联 GitHub 仓库。
2. 设置构建命令:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal \
&& . "$HOME/.cargo/env" \
&& cargo install wasm-pack \
&& pnpm install \
&& pnpm build
3. 将输出目录设置为 `.vitepress/dist`。
4. 将 `NODE_VERSION=24` 添加到环境变量中。
5. 如果站点是从子路径提供服务的,请添加带有该路径的 `SITE_BASE`(如果是根域名部署,则保持不设置)。
## 许可证
本项目基于 [Educational Community License, Version 2.0 (ECL-2.0)](LICENSE) 授权。
标签:CTF平台, VitePress, WebAssembly, Web漏洞利用, 可视化界面, 特征检测, 自动化攻击, 静态部署