rclone 批量上传下载脚本:重试 + 日志 + 分享链接
rclone 一条命令就能传单个文件到 Google Drive / OneDrive / S3 / OSS。但生产环境要批量、重试、日志、结果链接——这就要写脚本。 前置:rclone 配好 remote rclone config跟着交互命令走,配好一个叫 gdrive 的 remote(可以是 Google Drive、S3、OSS、Backblaze 都行,脚本不区分)。 验证: rclone lsd gdrive:能列出根目录说明认证 OK。 批量上传脚本 upload.sh: #!/usr/bin/env bash set -uLOG_FILE="./upload.log" RETRY=3 REMOTE_DIR="gdrive:backup"for file in "$@"; do echo "上传: $file" | tee -a "$LOG_FILE" if [ ! -e "$file" ]; then echo "文件不存在: $file" | tee -a "$LOG_FILE" continue fi filename=$(basename "$file") remote_path="$REMOTE_DIR/$filename" count=0 success=0 while [ $count -lt $RETRY ]; do printf "第 %d 次尝试\n" $((count + 1)) | tee -a "$LOG_FILE" rclone copy "$file" "$REMOTE_DIR" \ --stats 5s --stats-one-line \ 2>&1 | tee -a "$LOG_FILE" if [ ${PIPESTATUS[0]} -eq 0 ]; then success=1 break fi count=$((count + 1)) sleep 2 done if [ $success -eq 1 ]; then echo "成功: $file" | tee -a "$LOG_FILE" # 生成分享链接(Google Drive / OneDrive 支持) link=$(rclone link "$remote_path" 2>/dev/null) if [ -n "$link" ]; then echo "分享链接: $link" | tee -a "$LOG_FILE" fi else echo "失败: $file" | tee -a "$LOG_FILE" fi done用法: chmod +x upload.sh ./upload.sh a.zip b.tar.gz c.iso关键点:$@ — 传所有命令行参数 ${PIPESTATUS[0]} — 拿管道第一个命令的退出码(tee 会覆盖 $?,这里必须显式取 rclone 的) --stats 5s --stats-one-line — 每 5 秒一行简洁进度,log 好看 rclone link — 生成分享链接,不支持的后端会返回空对称的下载脚本 download.sh: #!/usr/bin/env bash set -uLOG_FILE="./download.log" RETRY=3 REMOTE_DIR="gdrive:backup"for remote_file in "$@"; do echo "下载: $remote_file" | tee -a "$LOG_FILE" remote_path="$REMOTE_DIR/$remote_file" count=0 success=0 while [ $count -lt $RETRY ]; do printf "第 %d 次尝试\n" $((count + 1)) | tee -a "$LOG_FILE" rclone copy "$remote_path" ./ \ --stats 5s --stats-one-line \ 2>&1 | tee -a "$LOG_FILE" if [ ${PIPESTATUS[0]} -eq 0 ]; then success=1 break fi count=$((count + 1)) sleep 2 done if [ $success -eq 1 ]; then echo "成功: $remote_file" | tee -a "$LOG_FILE" else echo "失败: $remote_file" | tee -a "$LOG_FILE" fi done用法: ./download.sh a.zip b.tar.gz加分项 并发上传:rclone copy 支持 --transfers 参数,一次传多个文件: rclone copy "$file" "$REMOTE_DIR" --transfers 8 --checkers 16限速(别把家里带宽打满): rclone copy "$file" "$REMOTE_DIR" --bwlimit 10M校验完整性: rclone check "$file" "$REMOTE_DIR/$filename"断点续传:rclone copy 天生支持,直接重跑同一命令即可。已经完成的文件会跳过。 多线程分块(大文件): rclone copy "$file" "$REMOTE_DIR" \ --multi-thread-streams 4 \ --multi-thread-cutoff 100M挂 cron 定时备份 0 3 * * * /home/me/upload.sh /var/backups/db-*.sql.gz >> /var/log/backup.log 2>&1配合 find 只传新文件: find /var/backups -mmin -60 -name "*.sql.gz" -exec /home/me/upload.sh {} +一句话总结 rclone copy + Bash 循环 + ${PIPESTATUS[0]} 判成功 = 稳的批量传输脚本。加个 --stats 看进度、rclone link 抓分享链接就够生产用。
为什么 Hook eval 会让 webpack 报 __webpack_require__ is not defined
问题复现 油猴脚本 hook eval 后报错: ReferenceError: __webpack_require__ is not defined at eval (service-mark.js:1:1) at eval (<anonymous>) at unsafeWindow.eval (my-script.user.js:28:24) at ./javascript/common/service-mark.js (task-common.js?61:240:1)hook 写法: const oldEval = window.eval;window.eval = function (code) { console.log(code); return oldEval.apply(this, args); // ← 问题在这里 };根本原因:直接调用 vs 间接调用 JS 中只有一种 direct eval(直接调用): eval(code)字面上写 eval(...) 才是直接调用,执行时能访问当前词法作用域。 其他所有形式都是 indirect eval(间接调用),在全局作用域执行: const e = eval; e(code); // indirecteval.call(null, code); // indirect eval.apply(this, [code]); // indirect (0, eval)(code); // indirect(常见技巧)webpack 模块代码是在模块工厂函数的作用域里 eval 执行的: function(module, exports, __webpack_require__) { eval("代码内容..."); // direct eval,能访问 __webpack_require__ }当你 hook 了 window.eval 并用 oldEval.apply(...) 转发时: window.eval = function(code) { return oldEval.apply(this, args); // indirect eval // ↑ 变成了间接调用,在全局作用域执行 };__webpack_require__ 是工厂函数的局部变量,全局作用域里不存在,所以报错。 为什么 return eval(code) 也不行 即使改成字面调用: unsafeWindow.eval = function(code) { return eval(code); // direct eval };这里的 eval(code) 是在你的 hook 函数作用域里执行的,不是在 webpack 工厂函数里。作用域仍然不对。 结论:无法通过 hook eval 的方式同时拦截代码和保持 webpack 的模块作用域。 这是 JS 引擎层面的限制,不是 hook 写法问题。 正确方案:hook webpackChunk.push 如果你需要修改 webpack 模块的源码(比如删掉 debugger),正确方式是在模块执行前 hook 模块定义,而不是 hook eval: const chunkKey = Object.keys(window).find(k => k.startsWith('webpackChunk'));const oldPush = window[chunkKey].push;window[chunkKey].push = function(chunk) { const modules = chunk[1]; for (const id in modules) { const fn = modules[id]; modules[id] = function(module, exports, __webpack_require__) { // 在这里可以拿到模块源码字符串 let code = fn.toString(); // 修改源码(例如移除 debugger) code = code.replace(/\bdebugger\b/g, ''); // 重建函数并在正确的参数上下文里执行 const newFn = eval('(' + code + ')'); return newFn(module, exports, __webpack_require__); }; } return oldPush.call(this, chunk); };这个方法的优势:在模块函数还没执行之前修改,不破坏作用域 __webpack_require__ 仍然由 webpack 正常传入 可以针对特定模块 ID 修改,不影响其他模块只是想看 eval 执行了什么代码 如果目的只是监听而不是转发执行,可以用 Object.defineProperty 劫持赋值时机: let _eval = window.eval;Object.defineProperty(window, 'eval', { get() { return _eval; }, set(v) { console.log('eval 被替换', v); _eval = v; }, configurable: true });或者用 Proxy 记录参数但不干预执行: const rawEval = window.eval;// 只记录,不修改执行上下文 const logFn = function(code) { console.log('[eval]', typeof code === 'string' ? code.slice(0, 200) : code); };// 在外层包一层,直接调用原始 eval window.__logEval = function(code) { logFn(code); return rawEval.call(this, code); };但真正的"无损 hook eval"做不到,只能选择:记录参数(丢失 webpack 作用域)或不 hook(不记录)。
JavaScript 函数 Hook:拦截 fetch、XHR、Function.prototype.call 与 setInterval
Hook 的核心模式 JavaScript 函数 Hook 的通用模式: const original = window.targetFunction;window.targetFunction = function (...args) { // 前置处理 console.log('before call:', args); const result = original.apply(this, args); // 后置处理 console.log('after call:', result); return result; };apply(this, args) 保持原始的 this 上下文,...args 透传所有参数。 Hook fetch const rawFetch = window.fetch;window.fetch = async function (...args) { console.log('[fetch]', args[0]); const response = await rawFetch.apply(this, args); // clone() 避免 body 被消费后无法再读 const clone = response.clone(); clone.text().then(body => { console.log('[fetch response]', body.slice(0, 200)); }); return response; };Hook XMLHttpRequest const RawXHR = window.XMLHttpRequest;window.XMLHttpRequest = function () { const xhr = new RawXHR(); const rawOpen = xhr.open.bind(xhr); xhr.open = function (method, url, ...args) { console.log('[XHR]', method, url); return rawOpen(method, url, ...args); }; const rawSend = xhr.send.bind(xhr); xhr.send = function (body) { xhr.addEventListener('load', function () { console.log('[XHR response]', xhr.responseText.slice(0, 200)); }); return rawSend(body); }; return xhr; };Hook Function.prototype.call(监听所有函数调用) (function () { const rawCall = Function.prototype.call; Function.prototype.call = function (...args) { // this 就是被调用的函数 if (this.name === 'targetFunction') { console.log('[call hook]', this.name, args); } return rawCall.apply(this, args); }; })();影响范围极大,生产环境谨慎使用,建议只在调试时临时注入。 Hook 某个对象的方法 const original = window.api.submit;window.api.submit = function (...args) { console.log('[hook api.submit]', args); const result = original.apply(this, args); console.log('[result]', result); return result; };Hook setInterval(拦截 debugger) 部分网页在 setInterval 里运行 debugger 语句来阻止调试: (() => { const rawSetInterval = window.setInterval; const rawClearInterval = window.clearInterval; const fakeTimers = new Set(); let fakeId = 1_000_000; function shouldBlock(fn) { try { let code = typeof fn === 'function' ? Function.prototype.toString.call(fn) : String(fn); if (/\bdebugger\b/.test(code)) { console.warn('[blocked debugger interval]'); return true; } } catch (e) {} return false; } window.setInterval = function (fn, delay, ...args) { if (shouldBlock(fn)) { fakeTimers.add(++fakeId); return fakeId; } return rawSetInterval(fn, delay, ...args); }; window.clearInterval = function (id) { if (fakeTimers.has(id)) { fakeTimers.delete(id); return; } return rawClearInterval(id); }; })();fakeId 返回一个虚假的 timer ID,避免调用 clearInterval 时报错。 Hook WebAssembly.instantiate const rawInstantiate = WebAssembly.instantiate;WebAssembly.instantiate = async function (...args) { const result = await rawInstantiate.apply(this, args); // 拦截导出函数 const exports = result.instance.exports; for (const key of Object.keys(exports)) { if (typeof exports[key] === 'function') { const original = exports[key]; exports[key] = function (...callArgs) { console.log(`[wasm] ${key}(`, callArgs, ')'); return original.apply(this, callArgs); }; } } return result; };油猴中的注入时机 // ==UserScript== // @run-at document-start // ==/UserScript==document-start 是最早的时机,在页面任何脚本执行之前运行,适合 hook 全局 API。如果用 document-end 或 document-idle,页面原始代码可能已经运行了。
漏洞管理平台架构:main.db + 每应用独立 SQLite 的多库设计
数据库拆分设计 /database main.db ← 用户、管理员、应用列表 java.db ← Java 漏洞、CVE、POC、EXP mysql.db ← MySQL 漏洞、CVE、POC、EXP redis.db nginx.db每个应用独立 SQLite 的优点:某应用数据损坏不影响整个系统 可单独备份或迁移到 PostgreSQL 漏洞数据量大时不会拖慢其他应用的查询main.db 表结构 -- 用户表 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password TEXT, -- bcrypt hash role TEXT, -- superadmin / admin / user status INTEGER, -- 0=禁用 1=正常 created_at DATETIME );-- 应用注册表 CREATE TABLE applications ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, -- "java" db_name TEXT, -- "java.db" db_path TEXT, -- "/database/java.db" created_at DATETIME );应用数据库(每个应用独立) CREATE TABLE vulnerabilities ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, description TEXT, severity TEXT, -- critical/high/medium/low affected_version TEXT, fixed_version TEXT, created_at DATETIME );CREATE TABLE cves ( id INTEGER PRIMARY KEY AUTOINCREMENT, cve_id TEXT, -- CVE-2024-12345 cvss REAL, description TEXT );CREATE TABLE pocs ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_path TEXT, -- /uploads/java/uuid.py description TEXT );CREATE TABLE exps ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, file_path TEXT, description TEXT );-- 多对多关联 CREATE TABLE vulnerability_pocs (vuln_id INT, poc_id INT); CREATE TABLE vulnerability_exps (vuln_id INT, exp_id INT); CREATE TABLE vulnerability_cves (vuln_id INT, cve_id INT);动态创建应用数据库 添加应用时,后端自动创建 SQLite 文件并初始化表: // NestJS service async createApplication(name: string) { const dbPath = path.join("database", `${name}.db`); const db = new Database(dbPath); // 初始化表结构 db.exec(` CREATE TABLE IF NOT EXISTS vulnerabilities (...); CREATE TABLE IF NOT EXISTS cves (...); CREATE TABLE IF NOT EXISTS pocs (...); CREATE TABLE IF NOT EXISTS exps (...); `); db.close(); // 记录到 main.db await this.mainDb.run( "INSERT INTO applications (name, db_name, db_path) VALUES (?, ?, ?)", [name, `${name}.db`, dbPath] ); }RBAC 三角色权限角色 权限superadmin 全部权限,管理用户和管理员admin 管理漏洞/CVE/POC/EXP,不能添加管理员user 只能查询漏洞和下载 POC/EXPJWT 鉴权: // NestJS Guard @Injectable() export class RolesGuard implements CanActivate { canActivate(context: ExecutionContext): boolean { const { user } = context.switchToHttp().getRequest(); const requiredRoles = this.reflector.get<string[]>("roles", context.getHandler()); return requiredRoles.includes(user.role); } }文件安全 POC/EXP 文件禁止通过静态目录暴露: ❌ GET /uploads/java/poc.py (直接静态访问) ✅ GET /api/download/poc/:id (JWT 鉴权后返回文件流)@Get("download/poc/:id") @UseGuards(JwtAuthGuard) async downloadPoc(@Param("id") id: string, @Res() res: Response) { const poc = await this.pocService.findById(id); res.download(poc.file_path, poc.name); }上传文件名用 UUID 避免路径遍历: const filename = `${uuidv4()}${path.extname(file.originalname)}`;版本匹配建议 不要简单字符串比对,用 semver 范围查询: import semver from "semver";function isVulnerable(version: string, affected: string): boolean { // affected: ">=8.0.0 <8.0.31" return semver.satisfies(version, affected); }"8.0.9" < "8.0.31" 用字符串比较会得到错误结果(字典序 9 > 3),必须用 semver 数字化比较。 推荐技术栈层 推荐后端框架 NestJS 或 Next.js App RouterORM Prisma(SQLite 支持好,类型安全)鉴权 JWT + bcrypt前端 Vue3 + Element Plus 或 shadcn/ui部署 PM2 + Nginx 反代
Word 通配符替换方括号数字:\[[0-9]@\]
Word 的"查找和替换"不支持标准正则表达式,而是一套叫通配符的专用语法,差异很大。 匹配方括号内的数字 目标:匹配 [1]、[23]、[456] 这类引用编号。 步骤:Ctrl + H 打开查找和替换 点击"更多" 勾选使用通配符 查找内容输入:\[[0-9]@\]表达式 含义\[ 字面量 [[0-9] 一个数字字符@ 前一个表达式重复一次或多次(相当于 +)\] 字面量 ]能匹配 [1]、[12]、[999],不能匹配 [a]、[]。 Word 通配符 vs 标准正则对比功能 标准正则 Word 通配符一个或多个 + @零个或多个 * *分组 (...) (...)引用分组 \1 \1或 | 部分版本支持数字 \d [0-9]转义 [ \[ \[Word 不支持 \d,数字必须写 [0-9]。| 在部分 Word 版本可用但不稳定。 带捕获组的替换 把 [12] 替换成 (12):查找:\[([0-9]@)\] 替换为:(\1)\1 引用第一个捕获组的内容。 处理 ^[ 符号的坑 如果文本里含有 ^[4] 这样的字符串,Word 会把 ^ 当作特殊字符前缀解析,直接手打会报错。 解决方法:从正文复制 ^[ 粘贴到查找框,不要手动键入。 如果是引文工具(NoteExpress、EndNote 等)生成的引用编号,可能是域代码或上标,不是普通文本,普通查找替换匹配不到。处理方法:选中引用 → 取消"上标"格式 或按 Ctrl + Shift + F9 将域代码转为纯文本不使用通配符的替换 不勾选"使用通配符"时,Word 按纯文本匹配(部分特殊字符组合除外),适合匹配固定字符串。如果想匹配的字符串包含 ^,关闭通配符后直接输入 ^[ 通常可以正常查找。 常用 Word 通配符速查 [0-9] 一个数字 [a-z] 一个小写字母 [A-Za-z] 一个字母 [0-9]@ 一个或多个数字 {2} 精确重复 2 次 {1,3} 重复 1~3 次 < 单词开头 > 单词结尾
