- autoban: 新增 RecordHoneypot 独立通道(不受 enabled 约束)+ 独立阈值/时长; IsBanned 不再因 enable_auto_ban=false 而忽略已有封禁 - honeypot: 新增 honeypotSuffixes 后缀匹配,覆盖 /backup.zip、/db.sql、/id_rsa.pem - 新增配置 honeypot_threshold / honeypot_ban_minutes - 补测试:autoban/honeypot_test.go、route 侧禁用自动封禁仍封禁
107 lines
10 KiB
Markdown
107 lines
10 KiB
Markdown
# 2026-10-21
|
||
|
||
## 扫描者处置方案设计(未实施,仅设计文档)
|
||
|
||
- 触发:线上频繁收到「疑似批量扫描已自动封禁」告警,命中路径以 wp 探测 + `/.ssh/**` 为主,来源多 Azure 云 IP 池。
|
||
- **实测纠正一处错误认知**:`internal/route/route.go:432` 的注释称「flamego 的 NotFound 会先写 404 状态再 Redirect」——
|
||
**错的**。flamego v1.9.4 的 `NotFound` 只执行 handler,不写状态码;`ctx.Redirect("/")` 实测输出 **302 → "/"`。
|
||
最小复现验证:`/wp-admin/includes/...` 与 `/.ssh/id_rsa` 均得 302 + Location:/。
|
||
后果:扫描器跟随重定向 → 每个探测路径都触发一次首页渲染(1C1G 纯浪费),且被判为「站点有价值」。
|
||
同时告警邮件文案「产生大量 404」与实际 302 不符。
|
||
- 方案分五层(L0 CDN 路径/UA 规则 → L1 真 404 分流 → L2 蜜罐假文件/假凭据 → L3 tarpit + 递进封禁 → L4 上游 abuse 举报):
|
||
- 核心判断:**对云厂商僵尸网络做 IP 封禁无效**,有效的是按**路径**在 CDN 拦 + 蜜罐取证 + 上游举报。
|
||
- 明确排除反向攻击/DoS/回打(法律风险 + Azure 共享出口伤及无辜 + 招致 CDN 封站)。
|
||
- 蜜罐:命中 `/.ssh/id_rsa`、`/.env`、`/wp-login.php` 等诱饵路径 → 短路返回假内容(含 canary 标记)+ 永久封禁 + 留证。
|
||
- 封禁需落库(当前内存 map,重启清零)+ 时长递进 15m→1h→6h→24h→7d→永久 + 多维(IP / IPv6 /64 / UA 家族)。
|
||
- tarpit 必须带全局并发上限 + 单连接超时,否则 1C1G 自身变 DoS 入口。
|
||
- 设计文档:`outputs/scanner-defense-design-2026-10-21.md`(含分级变更清单、文件级改动点、验收指标)。
|
||
- 未动任何生产代码,等用户决定实施范围。
|
||
|
||
## 蜜罐(虚假后台登录界面 + 破解凭据记录)已实施
|
||
|
||
用户明确需求原文:「能不能把这些页面指向一个简单的虚假后台登录界面,然后记录这些破解的用户名密码等信息」。
|
||
|
||
### 落点决策(关键)
|
||
- **蜜罐不改写成新中间件,而是改写 `internal/route/route.go` 的 `f.NotFound`**:
|
||
- NotFound 只在「路由表完全落空」时触发 → 与 `/login` `/register` `/_/{domain}` `/user/**` `/api/v1/**` 天然零交集;
|
||
- 位于全局 `f.Use` 链之后 → `requestBodyLimitMiddleware` 已给 body 加 12 MiB 上限;
|
||
- 组级中间件(含 CSRF)只挂具体路由,不作用于 NotFound → POST 表单可安全解析;
|
||
- flamego `context.run()` 在 `ResponseWriter().Written()` 为真时 return → 写完响应不调 `ctx.Next()` 即安全短路。
|
||
- **双重计数坑**:autoban 中间件对敏感路径已 `Record` 一次,蜜罐再记一次会让阈值实际减半。
|
||
解决:`autobanMiddleware(b, hp)` 传入蜜罐匹配器,蜜罐命中的路径由蜜罐自己计数。
|
||
|
||
### 新增文件
|
||
- `internal/security/honeypot/honeypot.go` — 匹配器(段前缀 + 片段子串 + 双重保险 `forbiddenPrefixs` 排除业务路由)、
|
||
凭据提取(表单 / JSON / BasicAuth / 查询串)、`CanaryToken = "tb-hp-7f3a1c9e"`。
|
||
- `internal/security/honeypot/page.go` — 仿 WordPress 假登录页(经典字段 `log`/`pwd`/`redirect_to`),内联 CSS 不依赖站点静态资源。
|
||
- `internal/db/honeypot.go` — `HoneypotHit`(**不嵌 dbutil.Model**,审计证据只追加)+ `HoneypotStore`。
|
||
- `internal/route/honeypot.go` — 处理器:GET 假登录页 / POST 假失败响应 / 落库 / 复用 autoban 封禁 / 每 IP 告警一次。
|
||
- `internal/cmd/honeypot.go` — `TamaBox honeypot hits|stats|prune`,密码默认脱敏(`--show-password` 显示明文)。
|
||
- `templates/mail/honeypot-alert.html` — 告警邮件模板(`mail/` 已在 embed 清单,无需改 embed.go)。
|
||
|
||
### 改动文件
|
||
- `internal/route/route.go`(CRLF):NotFound 分流 + 蜜罐实例装配 + `hph.sendAlert`。
|
||
- `internal/route/autoban.go`(LF):`autobanMiddleware(b, hp)`,蜜罐路径跳过自身 Record。
|
||
- `internal/db/db.go`(LF):`AllTables` 加 `&HoneypotHit{}`,`Init()` 加 `Honeypots = NewHoneypotStore(db)`。
|
||
- `internal/conf/static.go`(CRLF):`enable_honeypot` / `honeypot_paths` / `honeypot_retain_days`。
|
||
- `conf/app.sample.ini`(CRLF):对应说明段。
|
||
- `internal/mail/mail.go`(LF):`SendHoneypotAlertMail` + `HoneypotAlert`。
|
||
- `cmd/tamabox.go` + 根 `tamabox.go`:注册 `cmd.Honeypot`(两处必须同步)。
|
||
|
||
### 踩坑
|
||
- `normalize()` 里 `for len(p)>1 { TrimSuffix(p,"/") }` **无限循环**(路径不含尾斜杠时字符串不变)→ 改成 `HasSuffix` 判断。
|
||
- `url.ParseQuery` 遇非法百分号编码返回 error 但仍给出部分字段 → 蜜罐要「尽可能多拿凭据」,忽略 error 继续用部分结果。
|
||
- JSON 凭据提取:手写解析器又脏又易错 → 改用 `jsonPairRe` 正则扫描扁平字段。
|
||
- `templates/mail/` 已在 embed 清单内 → 新增邮件模板**不需要**改 `templates/embed.go`(顶层文件才需要)。
|
||
|
||
### 验证
|
||
- `go build ./...` 通过;`go test ./...` 全绿(含 DB 测试,postgres 门控)。
|
||
- 新增测试:`internal/security/honeypot/honeypot_test.go`(匹配/业务路由零交集/编码绕过/凭据提取)、
|
||
`internal/route/honeypot_test.go`(假页含 canary、业务路由不被劫持、关闭时回落 302、POST 返回失败页)、
|
||
`internal/db/honeypot_test.go`(写入/倒序/三张聚合表/保留期清理)。
|
||
|
||
## 测试站黑盒验证 + P0 缺陷修复(蜜罐封禁被 enable_auto_ban 连坐)
|
||
|
||
测试站:`https://box-dev-api.337845818.best`(Cloudflare Tunnel + CF 边缘,`cf-cache-status: DYNAMIC`、无 `Age` → 不缓存,请求确实回源)。
|
||
生产站 `https://box.tama.guru` 强制 301 → www,本机不可达,无法做对照。
|
||
|
||
### 黑盒验证结果(全部通过)
|
||
- 诱饵路径全部 200 + `text/html`:`/wp-login.php` 2529B、`/wp-admin/` 2517B、`/.ssh/id_rsa` 2525B、`/.env` 2497B、`/administrator` 2533B。
|
||
- 响应体含 canary `tb-hp-7f3a1c9e`(2 处)、`<title>Log In ‹ WordPress</title>`、`name="log"`/`name="pwd"`/`name="redirect_to"`。
|
||
- 响应头:`Cache-Control: no-store, no-cache, must-revalidate`、`x-robots-tag: noindex, nofollow`、`x-frame-options: DENY`。
|
||
- POST 凭据提交 → 200 + `id="login_error">ERROR: Invalid username or password.`。
|
||
- 路径变体全命中:`/WP-ADMIN`(大小写)、`/wp-admin%2f..%2fetc`(编码绕过)、`//wp-admin//x.php`(双斜杠)、`/.git/config`、`/.aws/credentials`、`/phpmyadmin/`、`/actuator/env`、`/xmlrpc.php`、`/vendor/phpunit/x.php`、`/console`、`/cgi-bin/test.cgi`、`/wp-content/plugins/x.php`、`/.env.bak`、`/wp-json/wp/v2/users`。
|
||
- 凭据来源全覆盖:表单 POST、JSON body、BasicAuth、查询串、POST 到 `/.ssh/id_rsa`。
|
||
- **业务路由零交集(红线)**:`/` 34192B、`/login` 23420B、`/register` 25194B、`/_/admin6` 39358B、`/user` 302、`/api/v1/questions` 302、`/robots.txt` 145B;非诱饵随机路径 `/nonexistent-random-xyz` 仍 302→`/`。
|
||
|
||
### P0 缺陷:蜜罐命中 70+ 次但站点零封禁
|
||
- 根因:`autoban.Record()` 第一行 `if !b.enabled { return }`,`b.enabled` 来自 `enable_auto_ban`。
|
||
运维为避免共享出口误伤而关掉 `enable_auto_ban` → **蜜罐被另一个开关连坐**,只落库不封禁。
|
||
- 对照组证明非蜜罐专属:`/index.php`(autoban 敏感、蜜罐不匹配)连打 40 次 + 30 并发同样零封禁。
|
||
- 排除项:CF 缓存(DYNAMIC/无 Age)、flamego NotFound 链(源码确认全局 Use 链生效)、蜜罐匹配器、阈值配置。
|
||
|
||
**修复(已实施,测试全绿)**:
|
||
- `autoban.Options` 新增 `HoneypotEnabled` / `HoneypotThreshold`(默认 1)/ `HoneypotDuration`(默认复用常规)。
|
||
- 新增 `Banner.RecordHoneypot(ip, path) bool`:独立计数、独立阈值,**不受 `enabled` 约束**;键与 `Record` 一致(同样 `shared.LimitKey` 归一化),否则 `IsBanned` 判不出来。
|
||
- `IsBanned()` 移除 `!b.enabled` 早退 —— `enabled` 只控制「是否主动创建封禁」,不控制「是否尊重已有封禁」。
|
||
- `internal/route/honeypot.go`:`banned = h.ab.RecordHoneypot(ip, path)`。
|
||
- `internal/route/route.go`(CRLF)装配三个蜜罐字段;`conf/static.go` + `conf/app.sample.ini`(CRLF)新增 `honeypot_threshold` / `honeypot_ban_minutes`。
|
||
- 告警:仍走既有 `sendAlert`(「已封禁」)+ 蜜罐自己的告警邮件(「捕获了什么凭据」),互补。
|
||
|
||
### 后缀匹配补缺
|
||
- `/backup.zip`、`/db.sql`、`/id_rsa.pem` 这类泄露扫描文件名主体随机,**只有后缀稳定**。
|
||
- 原 `honeypotFragments` 用 `"/.zip"` 子串匹配 → `.zip` 前是 `k` 不是 `/`,全漏网。
|
||
- 新增 `honeypotSuffixes`(`.zip .tar .tar.gz .tgz .gz .rar .7z .bz2 .sql .sql.gz .dump .bak .old .orig .save .swp .env .env.bak .env.local .pem .key .p12 .pfx .jks .kdbx .ovpn`),按 `path.Base()` 结尾匹配。
|
||
|
||
### 踩坑
|
||
- ⚠️ **测试里 `Duration: 60` = 60 纳秒**,封禁瞬间过期 → 表现为「时序巧合的 200/200/403」,测试假失败。一律写 `60 * time.Minute`。
|
||
- ⚠️ 路由测试只注册路由 + NotFound 会被绕过:执行 `IsBanned` → 403 的是 **`autobanMiddleware`**,测试服务器必须 `f.Use(autobanMiddleware(ab, hp))`。
|
||
- ⚠️ `internal/route/route.go`、`internal/conf/static.go`、`conf/app.sample.ini` **实际是全 CRLF**;`grep | cat -A` 不显示 `^M`,极易误判为 LF。Python 脚本按 `\n` 锚点替换会静默失败 → 用 `\r\n` 锚点 + 写后统计 `b'\r\n'` 复核。
|
||
- 假页纯静态 HTML(内联 CSS,不落盘)、告警走 SMTP → **取证数据只在 `honeypot_hits` 一张表**,服务器无文件副本;autoban 封禁表是内存态(重启清零)。
|
||
- 测试阶段产生的噪声凭据(`testadmin/S3cr3tP@ss` 等)可用 `TamaBox honeypot prune` 清理。
|
||
|
||
### 遗留(需用户侧)
|
||
- 本轮修复需**重新部署**才在测试站生效;部署后 `curl` 一次 `/wp-login.php` 应 200、再一次 403。
|
||
- 需确认测试环境 `conf/app.ini` 的 `[security] enable_auto_ban`(零封禁直接原因)+ `enable_honeypot = true`。
|
||
- 无法 SSH(`100.79.185.45:19999` 全部账号 Permission denied),落库验证需用户执行 `TamaBox honeypot hits --since 24h --show-password`。
|