简介:中国蚁剑(AntSword)2.1.9 版本完整源码包,面向网络安全从业者、渗透测试学习者及希望研究 Web 安全工具实现原理的开发者。蚁剑是一款轻量级开源 Web 安全测试平台,支持远程连接管理、插件扩展、跨平台运行,并可在更新源延迟时作为备用加载方案。压缩包共 2777 个文件,约 15.51MB,以 1471 个 js 脚本、120 个 css 样式、40 个 html 页面构成核心前端与插件逻辑,另有 204 个 md 文档、175 个 json 配置、98 个 license 及多种字体、图标资源,目录结构完整。已有 1621 人学习下载。通过阅读源码,可深入理解 HTTP 协议交互、Web 服务器通信机制与漏洞探测思路,学习插件系统的设计与扩展方式,并提升 Python、JavaScript 等语言在安全工具开发中的实际运用能力,适合作为安全研究与二次开发的参考素材。
1. 拿到一份安全工具源码,我习惯先看它怎么组织通信层
很多开发者拿到安全工具源码的第一反应是直接翻 payload 或者 shell 管理模块,但我自己的习惯是先看通信层怎么设计的。原因很简单:这类工具的核心链路是「客户端 → 传输层 → 目标端」,通信层决定了整个项目的架构走向,也决定了你二次开发时能改什么、不能改什么。antSword-2.1.9 这份源码包,解压后是一个完整的 Node.js + Electron 桌面应用工程,核心能力围绕 Shell 管理、编码器、解码器、插件体系展开。它适合两类人:一是想研究桌面安全工具架构的 Node.js 开发者,二是需要在现有工具基础上做二次开发、定制编码器或插件体系的安全方向同学。整份源码结构清晰,模块边界明确,拿来当 Electron 工程的学习样本也不亏。
2. 工程结构与技术选型:一个 Electron 壳里塞了哪些模块
2.1 目录结构拆解
解压 antSword-2.1.9.zip 之后,根目录大致是这样的布局:
antSword-2.1.9/ ├── package.json # 项目描述与依赖声明 ├── main.js # Electron 主进程入口 ├── renderer/ # 渲染进程(前端界面) │ ├── index.html │ ├── js/ │ │ ├── app.js # 前端主逻辑 │ │ ├── shell.js # Shell 管理模块 │ │ ├── encoder.js # 编码器调度 │ │ └── decoder.js # 解码器调度 │ └── css/ ├── modules/ # 核心功能模块 │ ├── request.js # 请求封装层 │ ├── encoder/ # 各类编码器实现 │ ├── decoder/ # 各类解码器实现 │ └── plugin/ # 插件加载与管理 ├── source/ # 核心 Shell 脚本模板 │ ├── php/ │ ├── asp/ │ ├── aspx/ │ └── jsp/ ├── static/ # 静态资源 └── node_modules/ # 依赖包这个结构里最值得关注的是modules/和source/两个目录。modules/承载了通信、编解码、插件三大核心能力,source/则是不同服务端语言的 Shell 模板。前端renderer/负责 UI 交互,主进程main.js负责窗口管理和系统级调用。
2.2 技术选型分析
| 层面 | 技术选择 | 选型理由 |
|---|---|---|
| 桌面框架 | Electron | 跨平台,前端技术栈直接复用 |
| 运行时 | Node.js | 与 Electron 天然集成,npm 生态丰富 |
| 前端 | 原生 JS + HTML/CSS | 轻量,不引入框架减少打包体积 |
| 通信 | HTTP/HTTPS 请求 | 通过 request 模块统一封装 |
| 编码体系 | 可插拔编码器/解码器 | 支持自定义扩展 |
| 插件系统 | 动态加载 | 按需加载,降低启动开销 |
常见做法是 Electron 项目用 Webpack 打包,但这个项目选择了原生 JS 直接加载,好处是调试方便、改完刷新即可,代价是没有模块热更新和 tree-shaking。对于安全工具这类需要频繁调试请求链路的场景,这个取舍是合理的。
2.3 主进程与渲染进程的分工
main.js是 Electron 的主进程入口,负责创建 BrowserWindow、注册全局快捷键、处理应用生命周期。渲染进程里的app.js才是真正的业务入口,它初始化 Shell 列表、加载插件、绑定 UI 事件。
// main.js 核心逻辑(简化示意) const { app, BrowserWindow } = require('electron'); const path = require('path'); let mainWindow = null; function createWindow() { mainWindow = new BrowserWindow({ width: 1200, height: 800, webPreferences: { nodeIntegration: true, // 渲染进程可直接用 Node API contextIsolation: false // 关闭上下文隔离,方便模块调用 } }); mainWindow.loadFile(path.join(__dirname, 'renderer/index.html')); } app.on('ready', createWindow); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });这里nodeIntegration: true和contextIsolation: false是两个关键配置。前者让渲染进程能直接requireNode 模块,后者关闭了上下文隔离,使得前端代码可以自由调用 Node API。这种配置在安全工具里很常见,因为需要在前端直接发起网络请求、读写文件。代价是安全性降低,但对于本地运行的桌面工具来说可以接受。
3. 核心模块拆解:请求封装、编码器与 Shell 管理
3.1 请求封装层
modules/request.js是整个工具的通信基石。所有与目标端的交互都经过这一层,它统一处理了 HTTP 请求的构造、发送、超时和错误。
// modules/request.js 核心逻辑(简化示意) const http = require('http'); const https = require('https'); const url = require('url'); /** * 发送请求到目标 Shell * @param {Object} options - 请求配置 * @param {string} options.url - 目标地址 * @param {string} options.method - 请求方法 GET/POST * @param {Object} options.headers - 自定义请求头 * @param {string} options.body - 请求体(POST 时使用) * @param {number} options.timeout - 超时毫秒数,默认 30000 * @returns {Promise<Object>} 响应对象 */ function sendRequest(options) { return new Promise((resolve, reject) => { const parsed = url.parse(options.url); const transport = parsed.protocol === 'https:' ? https : http; const reqOptions = { hostname: parsed.hostname, port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80), path: parsed.path, method: options.method || 'POST', headers: options.headers || {}, timeout: options.timeout || 30000 }; const req = transport.request(reqOptions, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { resolve({ statusCode: res.statusCode, headers: res.headers, body: data }); }); }); req.on('timeout', () => { req.destroy(); reject(new Error('REQUEST_TIMEOUT')); }); req.on('error', (err) => { reject(err); }); if (options.body) { req.write(options.body); } req.end(); }); } module.exports = { sendRequest };这段代码有几个关键点。第一,transport根据协议自动选择http或https模块,不需要调用方关心。第二,超时处理用了req.destroy()而不是简单 reject,确保底层 socket 被释放,避免连接泄漏。第三,返回的响应对象包含statusCode、headers、body三个字段,调用方拿到后自行解析。
参数说明:options.url必填,options.method默认 POST,options.timeout默认 30000 毫秒。如果你在二次开发中需要支持自定义证书或代理,可以在这里扩展reqOptions的agent字段。
3.2 编码器与解码器体系
编码器和解码器是这类工具的核心扩展点。modules/encoder/和modules/decoder/下各自有多个实现文件,每个文件导出一个统一的接口。
// modules/encoder/base64.js 编码器示例 /** * Base64 编码器 * 将原始 payload 编码为 Base64 字符串 */ module.exports = { name: 'base64', /** * @param {Buffer|string} data - 待编码数据 * @returns {string} 编码后的字符串 */ encode: function(data) { if (Buffer.isBuffer(data)) { return data.toString('base64'); } return Buffer.from(data, 'utf8').toString('base64'); } };// modules/decoder/base64.js 解码器示例 module.exports = { name: 'base64', /** * @param {string} data - 待解码字符串 * @returns {Buffer} 解码后的 Buffer */ decode: function(data) { return Buffer.from(data, 'base64'); } };编码器和解码器的接口设计非常简洁:编码器暴露encode方法,解码器暴露decode方法,各自带一个name属性用于 UI 展示和选择。这种设计的好处是新增一个编码方式只需要在对应目录下加一个文件,然后在加载时注册即可。
实际使用中,编码器和解码器是成对出现的。你在 UI 上选择某个编码器,工具会在发送请求前调用encode处理 payload,收到响应后调用对应的decode还原数据。如果编码解码不匹配,就会出现乱码或者请求失败。
3.3 Shell 管理模块
renderer/js/shell.js负责 Shell 的增删改查和状态管理。每个 Shell 对象包含目标 URL、密码、编码器选择、解码器选择、备注等字段。
// renderer/js/shell.js 核心数据结构(简化示意) class ShellManager { constructor() { this.shells = []; // 所有 Shell 列表 this.activeShell = null; // 当前选中的 Shell } /** * 添加一个 Shell * @param {Object} config * @param {string} config.url - 目标地址 * @param {string} config.password - 连接密码 * @param {string} config encoder - 编码器名称 * @param {string} config.decoder - 解码器名称 * @param {string} config.note - 备注 */ addShell(config) { const shell = { id: Date.now().toString(36), url: config.url, password: config.password, encoder: config.encoder || 'default', decoder: config.decoder || 'default', note: config.note || '', status: 'disconnected', createdAt: new Date().toISOString() }; this.shells.push(shell); this.persist(); return shell; } /** * 移除指定 Shell * @param {string} id - Shell 唯一标识 */ removeShell(id) { this.shells = this.shells.filter(s => s.id !== id); if (this.activeShell && this.activeShell.id === id) { this.activeShell = null; } this.persist(); } /** * 持久化到本地存储 */ persist() { const data = JSON.stringify(this.shells); localStorage.setItem('antsword_shells', data); } /** * 从本地存储恢复 */ restore() { const data = localStorage.getItem('antsword_shells'); if (data) { try { this.shells = JSON.parse(data); } catch (e) { this.shells = []; } } } }这个模块的设计要点:每个 Shell 用时间戳生成唯一 ID,状态字段status跟踪连接状态,persist和restore用 localStorage 做持久化。二次开发时如果你想换成文件存储或者数据库存储,只需要改这两个方法。
4. 避坑与常见问题:跑起来之前先看这几条
4.1 依赖安装失败
现象:执行npm install时卡在某个包上,或者报node-gyp编译错误。
原因:Electron 相关依赖需要编译原生模块,如果本机没有安装 Python 和 C++ 编译工具链就会失败。另外某些依赖包的版本与当前 Node.js 版本不兼容。
解决:先确认 Node.js 版本在 14.x 或 16.x(太新的版本可能不兼容老依赖),然后安装编译工具链。Windows 上装windows-build-tools,macOS 上装 Xcode Command Line Tools,Linux 上装build-essential。如果还是不行,试试npm install --ignore-scripts跳过编译脚本,然后手动处理需要编译的模块。
4.2 Electron 启动白屏
现象:npm start之后窗口打开了,但内容是空白的。
原因:通常是renderer/index.html里的资源路径不对,或者渲染进程报错导致页面没渲染出来。
解决:打开开发者工具(快捷键 Ctrl+Shift+I 或 Cmd+Option+I),看 Console 面板有没有报错。常见的是require找不到模块,检查nodeIntegration是否设为true。另外确认index.html里引用的 JS 和 CSS 路径是相对路径还是绝对路径,Electron 加载本地文件时相对路径的基准是 HTML 文件所在目录。
4.3 编码器选择后请求乱码
现象:在 UI 上选了某个编码器,但发出去的请求在目标端解析失败,或者返回的数据解码后是乱码。
原因:编码器和解码器不匹配。比如发送时用了 Base64 编码,但接收时没有用 Base64 解码,或者目标端的 Shell 模板不支持这种编码方式。
解决:确认编码器和解码器是成对使用的。检查source/目录下对应语言的 Shell 模板是否支持你选的编码方式。如果不确定,先用default编码器测试连通性,确认链路通了再换其他编码器。
4.4 插件加载后不生效
现象:把插件文件放到了modules/plugin/目录下,但 UI 上看不到插件入口。
原因:插件需要在某个注册表或配置文件中声明,不是放进去就自动加载的。
解决:检查modules/plugin/下是否有index.js或类似的注册文件,确认你的插件被正确require并注册。有些插件还需要在package.json里声明入口。如果项目用的是动态扫描目录的方式加载,确认文件命名和导出格式符合规范。
4.5 请求超时但目标正常
现象:目标地址在浏览器里能正常访问,但工具里一直报超时。
原因:可能是请求头缺失导致目标端拒绝了请求,或者工具的默认超时时间太短。
解决:在modules/request.js里把timeout默认值调大,比如改成 60000。同时检查请求头是否完整,有些目标端会校验User-Agent或Content-Type。可以在sendRequest调用处打印完整的reqOptions来排查。
5. 二次开发上手:从改一个编码器到加一套插件体系
5.1 新增自定义编码器
假设你需要一个 URL 编码器,在modules/encoder/下新建urlencode.js:
// modules/encoder/urlencode.js module.exports = { name: 'urlencode', encode: function(data) { const str = Buffer.isBuffer(data) ? data.toString('utf8') : data; return encodeURIComponent(str); } };然后在编码器加载的地方注册这个文件。常见做法是有一个modules/encoder/index.js统一导出所有编码器,你需要在里面加上require('./urlencode')。改完之后重启应用,UI 的编码器下拉列表里就会出现urlencode选项。
对应的解码器在modules/decoder/urlencode.js:
module.exports = { name: 'urlencode', decode: function(data) { return Buffer.from(decodeURIComponent(data), 'utf8'); } };验证方法:选一个测试 Shell,编码器选urlencode,解码器也选urlencode,发一个简单请求看返回是否正常。如果返回乱码,检查decodeURIComponent是否被正确调用。
5.2 扩展 Shell 模板
source/目录下按语言分了php/、asp/、aspx/、jsp/四个子目录。每个目录里的文件是服务端脚本模板,工具会把用户输入的 payload 嵌入模板后发送到目标端执行。
如果你想支持一种新的服务端语言,比如 Python 的 CGI 模式,需要在source/下新建python/目录,然后按照现有模板的格式写一个接收 payload 并执行的脚本。模板里通常会有占位符,工具在发送请求时会替换这些占位符。
5.3 插件体系的扩展思路
插件体系是这类工具最灵活的扩展点。一个典型的插件结构如下:
// modules/plugin/example/index.js module.exports = { name: 'example', version: '1.0.0', description: '示例插件', /** * 插件初始化,注册菜单项和事件 * @param {Object} context - 插件上下文,包含 shellManager 等 */ init: function(context) { context.registerMenuItem({ label: '示例功能', handler: function(shell) { // 在这里实现你的功能 console.log('当前 Shell:', shell.url); } }); } };插件通过init方法拿到上下文对象,然后注册菜单项或事件处理器。二次开发时你可以通过插件实现批量操作、自定义扫描逻辑、数据导出等功能,而不需要改动核心代码。
5.4 改造时的验证清单
| 改造点 | 验证方法 | 常见失败原因 |
|---|---|---|
| 新增编码器 | UI 下拉列表出现,请求返回正常 | 未注册到 index.js |
| 新增解码器 | 与编码器配对使用无乱码 | 解码逻辑与编码不匹配 |
| 新增 Shell 模板 | 目标端能正确执行 payload | 占位符格式不对 |
| 新增插件 | UI 出现插件菜单,功能可触发 | init 未调用或注册失败 |
| 修改请求层 | 请求能发出且响应正常解析 | 超时或请求头缺失 |
改造完成之后,建议先用一个本地搭建的测试环境验证,确认链路通了再上真实目标。每次改完编码器或请求层,我都会强制走一遍「发请求 → 看响应 → 解码 → 确认数据完整」这个流程,少一步都可能留下隐患。
5.5 一个实用技巧:用日志定位通信问题
在modules/request.js的sendRequest函数里加一行日志,把完整的请求配置和响应状态打出来:
function sendRequest(options) { return new Promise((resolve, reject) => { // ... 前面的代码不变 // 在发送前打印请求配置 console.log('[REQUEST]', JSON.stringify({ url: options.url, method: options.method, headers: options.headers, bodyLength: options.body ? options.body.length : 0 })); const req = transport.request(reqOptions, (res) => { let data = ''; res.on('data', chunk => { data += chunk; }); res.on('end', () => { // 打印响应状态和长度 console.log('[RESPONSE]', res.statusCode, 'bodyLength:', data.length); resolve({ statusCode: res.statusCode, headers: res.headers, body: data }); }); }); // ... 后面的代码不变 }); }这行日志在排查「请求发出去了但没响应」或者「响应回来了但解析失败」这类问题时特别管用。你能一眼看出是请求没发出去、还是发出去了但目标端返回了异常状态码、还是响应体为空。从那以后我每次改通信层都先把日志加上,确认链路通了再删掉,省得来回猜。希望帮到你。
本文还有配套的精品资源,点击获取