在漏洞研究流程中,检索公开利用代码往往比单纯查看漏洞公告更能判断实际风险。拿到一个 CVE 编号后,需要快速确认 ExploitDB 等公开库中是否已存在对应利用脚本。Node.js 凭借异步网络请求和成熟的 SQLite 绑定,很适合实现一个本地化的检索工具 ExploitSearch。下面从项目结构开始拆解,给出可运行的代码。

一、初始化项目与依赖选择
创建项目目录并初始化 package.json。核心依赖包括 axios 用于 HTTP 请求,better-sqlite3 用于本地索引存储,commander 用于命令行参数解析。better-sqlite3 的优势在于同步 API 简单直观,执行写入和查询都不需要处理回调队列,适合单机小工具。csv-parse 用来解析 ExploitDB 提供的 files.csv 数据文件,如果不想安装 csv-parse,也可以自己写一个简易的 CSV 行解析器,但考虑到描述字段中可能包含逗号和引号,用成熟的解析库更稳妥。
安装命令如下。执行 npm init 后安装四个依赖,better-sqlite3 需要编译原生模块,如果网络较慢可以提前配置 npm 镜像。
mkdir exploit-search && cd exploit-search npm init -y npm install axios better-sqlite3 commander csv-parse
安装完成后,在 package.json 的 scripts 中添加一个启动命令,把入口指向 src/index.js。这样后续调试时直接运行 npm run search 即可。目录建议拆分为 src/fetcher.js、src/database.js、src/search.js 和 src/index.js 四个模块,分别负责抓取、存储、检索和命令行交互。
二、数据抓取与解析核心实现
漏洞利用库的数据源选 ExploitDB 的 files.csv,它包含 id、file、description、date、author、type、platform、port 等字段,覆盖了绝大多数公开 Exploit 条目。抓取时用 axios 发起 GET 请求,设置超时和 User-Agent,避免被目标服务器当作脚本拦截。下载完成后将 CSV 字符串交给 csv-parse 处理,得到对象数组。
const axios = require('axios');
const { parse } = require('csv-parse/sync');
const EXPLOITDB_CSV = 'https://www.exploit-db.com/files.csv';
async function fetchExploitDB() {
const response = await axios.get(EXPLOITDB_CSV, {
timeout: 15000,
headers: {
'User-Agent': 'ExploitSearch/1.0 (+local research tool)'
}
});
const records = parse(response.data, {
columns: true,
skip_empty_lines: true,
relax_column_count: true
});
return records.map((row) => ({
edbId: parseInt(row.id, 10),
file: row.file || '',
description: (row.description || '').trim(),
date: row.date || '',
author: row.author || '',
type: row.type || '',
platform: row.platform || '',
port: row.port || ''
}));
}
module.exports = { fetchExploitDB };
解析后的数据写入 SQLite。创建 exploits 表时给 edbId 加上唯一索引,description 字段保留全文检索的原始文本。这里不用单独清理 HTML 实体,因为 files.csv 的描述通常是纯文本,只有少量 HTML 编码。若后续接入网页抓取,再用 he 库统一解码。
const Database = require('better-sqlite3');
function createDatabase(dbPath = './exploits.db') {
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.exec(`
CREATE TABLE IF NOT EXISTS exploits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
edb_id INTEGER UNIQUE NOT NULL,
file TEXT,
description TEXT,
date TEXT,
author TEXT,
type TEXT,
platform TEXT,
port TEXT
);
`);
const insert = db.prepare(`
INSERT OR REPLACE INTO exploits
(edb_id, file, description, date, author, type, platform, port)
VALUES
(@edbId, @file, @description, @date, @author, @type, @platform, @port)
`);
return { db, insert };
}
function saveRecords(db, insert, records) {
const insertAll = db.transaction((rows) => {
for (const row of rows) {
insert.run(row);
}
});
insertAll(records);
}
module.exports = { createDatabase, saveRecords };
这段代码使用事务批量写入,SQLite 在单事务内连续插入数万条记录也能在几秒内完成。WAL 模式可以提升读写并发,命令行查询时不会因为未提交事务而锁库。主键自增与 edb_id 唯一索引并存,便于后续扩展多数据源时用自增主键做关联。
三、检索索引与查询优化
如果只对 description 字段执行 LIKE 模糊查询,当记录超过五千条时每次检索都要全表扫描,响应时间会明显上升。SQLite 自带的 FTS5 全文索引可以解决这个问题。FTS5 会按词元切分文本,对英文描述和 CVE 编号有较好的匹配效果。建表时使用 content 参数指向 exploits 表,FTS5 只保存索引,原始数据仍然留在主表。
function createFtsIndex(db) {
db.exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS exploits_fts
USING fts5(
description,
content='exploits',
content_rowid='id',
tokenize='unicode61 remove_diacritics 2'
);
`);
db.exec(`
INSERT INTO exploits_fts(exploits_fts)
VALUES('rebuild');
`);
}
function searchByKeywords(db, keywords) {
const terms = keywords
.split(/\s+/)
.filter(Boolean)
.map((term) => {
const safe = term.replace(/"/g, '""');
return '"' + safe + '"';
})
.join(' AND ');
const rows = db.prepare(`
SELECT
e.edb_id,
e.file,
e.description,
e.date,
e.author,
e.type,
e.platform,
highlight(exploits_fts, 0, '[[', ']]') AS snippet
FROM exploits_fts
JOIN exploits e ON e.id = exploits_fts.rowid
WHERE exploits_fts MATCH ?
ORDER BY e.date DESC
LIMIT 50
`).all(terms);
return rows;
}
module.exports = { createFtsIndex, searchByKeywords };
上面的查询把多个关键词用 AND 连接,要求结果同时包含所有词。highlight 函数会在匹配片段前后加上占位符,方便命令行输出时替换成 ANSI 颜色。CVE 编号的精确检索可以直接查 edb_id 或再用一个普通索引,不必依赖 FTS 的模糊行为。对于中文关键词,FTS5 默认 unicode61 分词会把连续汉字视为一个词元,所以需要确保查询词与描述中写法一致。
如果希望支持 CVE 编号的部分匹配,比如只输入 CVE-2023 而不是完整编号,可以在代码里先检测输入是否匹配 CVE 格式,再决定走 FTS 还是 LIKE。LIKE 查询范围可以利用日期过滤,减少扫描数据量。实际测试中,五千条数据用 FTS5 检索常见关键词如 remote code execution,耗时通常低于 5ms。
四、命令行封装与安全使用
命令行入口用 commander 定义三个参数:--query 接收关键词,--cve 接收 CVE 编号,--limit 控制返回条数。程序启动时检查 SQLite 文件是否存在,如果不存在则先抓取数据并建索引。抓取过程默认强制执行,也可以加一个 --update 参数手动刷新本地库。
const { Command } = require('commander');
const { fetchExploitDB } = require('./fetcher');
const { createDatabase, saveRecords } = require('./database');
const { createFtsIndex, searchByKeywords } = require('./search');
const fs = require('fs');
const program = new Command();
program
.option('-q, --query <keywords>', 'search keywords')
.option('-c, --cve <cve>', 'exact CVE identifier')
.option('-l, --limit <number>', 'result limit', '20');
program.parse(process.argv);
const options = program.opts();
(async () => {
const dbPath = './exploits.db';
const needFetch = !fs.existsSync(dbPath) || options.update;
const { db, insert } = createDatabase(dbPath);
if (needFetch) {
const records = await fetchExploitDB();
saveRecords(db, insert, records);
createFtsIndex(db);
console.log(`已导入 ${records.length} 条记录`);
}
let results;
if (options.cve) {
results = db.prepare(`
SELECT edb_id, file, description, date, author, type, platform
FROM exploits
WHERE description LIKE ?
ORDER BY date DESC
LIMIT ?
`).all(`%${options.cve}%`, Number(options.limit));
} else if (options.query) {
results = searchByKeywords(db, options.query);
} else {
console.log('请提供 --query 或 --cve 参数');
process.exit(1);
}
for (const row of results.slice(0, Number(options.limit))) {
console.log(`[${row.edb_id}] ${row.file}`);
console.log(`类型: ${row.type} 平台: ${row.platform}`);
console.log(row.snippet || row.description);
console.log('');
}
})();
代码里注意异步主函数需要用 IIFE 包裹,因为 commander 的 parse 是同步的。LIKE 查询中的百分号在 SQL 参数中不需要转义,但描述字段如果包含用户输入的特殊字符,使用参数绑定可以防止 SQL 注入。
ExploitSearch 仅用于授权测试和内部漏洞研究。公开 Exploit 代码通常带有攻击利用性质,运行前必须在隔离环境中检查代码逻辑,避免对未授权系统执行。抓取 ExploitDB 时应降低请求频率,并遵守目标站点的 robots 与服务条款。本地缓存可以减少对外部服务的依赖,也方便离线检索。
Node.jsExploitSearch漏洞利用检索修改时间:2026-10-04 12:18:19