diff --git a/.workbuddy/memory/2026-10-21.md b/.workbuddy/memory/2026-10-21.md index bc92783..4578e29 100644 --- a/.workbuddy/memory/2026-10-21.md +++ b/.workbuddy/memory/2026-10-21.md @@ -315,3 +315,17 @@ CLI 端到端手动验证:`honeypot hits` / `--ip` / `--show-password` 输出 - `566b13e` fix(conf): 样例配置去掉行尾注释(2 文件) - `c29e1d0` fix(honeypot): 补齐封禁装配与诱捕语义,凭据外置为文件不再入库(21 文件) - `main` ahead of `origin/main` 2;用户要部署测试环境验证后再决定推送。 + +### 测试环境实测反馈(同日晚,已修并推送) +现象:purge-creds 删 4 列成功、GET 200、POST 后 GET 403、文件已生成。 +但凭据文件里 `"path":" /wp-login.php"`(前导空格)、`"ip":"127.0.0.1"`。 + +- **path 脏字符**:反代链路(CF → nginx → 源站)传出的 `req.URL.Path` 带前导空白。 + `Match` 内部 normalize 会 TrimSpace 所以仍命中,但落库与假表单 action 用原始值 + → 取证数据脏、爆破脚本可能提交到错误路径。本地 httptest 复现不出来,是环境相关。 + 修复:`cleanPath()` 在 Serve 入口 trim + 补前导斜杠(不改大小写)。 +- **IP = 127.0.0.1**:说明 `trust_proxy` 未开。后果不只是取证失真 —— + **一次蜜罐封禁会封掉 127.0.0.1,等于封掉整站所有访客**(封禁是内存态,重启即恢复)。 + 修复:命中回环地址且 `!trust_proxy` 时打一次性 WARN(24h 冷却),不改封禁行为。 + sample.ini 的 trust_proxy 段补了蜜罐相关说明。 +- `846bb8a` 已推送(`5e8deca..846bb8a`)。 diff --git a/.workbuddy/memory/topics/build.md b/.workbuddy/memory/topics/build.md index 7621a1c..0593bdb 100644 --- a/.workbuddy/memory/topics/build.md +++ b/.workbuddy/memory/topics/build.md @@ -15,3 +15,9 @@ 探测端口 200 可能是别的进程,必须看日志确认 `Starting web server` 且无 panic。 - ⚠️ **本机 python urllib 不稳定**(代理下偶发读超时,curl 正常);python 别用 `/tmp` (Windows 解析成 `C:\tmp`,与 MSYS 不通)。PIL 在 `~/.workbuddy/binaries/python/envs/default`。 +- ⚠️ **验证 Go 二进制里有没有某个中文字符串,不能用 `strings`**:GNU strings 默认只输出 + 可打印 ASCII(0x20–0x7E),UTF-8 中文(0xE4+ 开头)整段被丢弃,`strings x | grep 中文` + 恒为 0 —— 曾据此误判「用户没重新编译」,实际二进制是新的。 + 正确做法:`grep -ac "中文字符串" 二进制文件`(实测同一个二进制:strings=0、grep -a=1)。 +- ⚠️ 替换二进制后**必须重启进程**:`go build -o X` 只改磁盘文件,正在跑的进程仍是旧映像。 + 若服务由 systemd/supervisor 管理,再手动 `./X web` 会因端口被占直接退出。 diff --git a/internal/route/honeypot.go b/internal/route/honeypot.go index 6acfbe8..1253674 100644 --- a/internal/route/honeypot.go +++ b/internal/route/honeypot.go @@ -6,10 +6,12 @@ package route import ( "context" + "fmt" "net/http" "strings" "sync" "time" + "unicode" "github.com/flamego/flamego" "github.com/sirupsen/logrus" @@ -135,6 +137,15 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) { // 但落库与假表单的 action 用的是原始值 —— 会留下脏取证数据, // 甚至让爆破脚本把表单提交到错误路径。这里统一清洗一次。 path := cleanPath(req.URL.Path) + // 诊断:清洗过说明上游注入了空白/零宽字符。用 %q 打出来 —— + // 这类字符肉眼不可见,裸打印会看不出哪里不同(曾经为此排查了一轮)。 + if raw := req.URL.Path; raw != path { + logrus.WithFields(logrus.Fields{ + "ip": ip, + "raw": fmt.Sprintf("%q", raw), + "used": fmt.Sprintf("%q", path), + }).Warn("蜜罐路径含上游注入的空白/零宽字符,已清洗后使用") + } // 来源 IP 是回环地址 + 未开 trust_proxy,是「站点在反代后面但没配信任」的 // 典型信号。此时所有访客在源站眼里都是同一个 IP: @@ -184,11 +195,23 @@ func (h *honeypotHandler) Serve(ctx flamego.Context) { h.record(ip, req, path, creds, banned) } -// cleanPath 去掉路径首尾空白并保证前导斜杠。 +// cleanPath 去掉路径首尾的空白与零宽字符,并保证前导斜杠。 // -// 刻意不改大小写:URL 路径是大小写敏感的,取证必须保留原始形态。 +// 为什么不能只用 strings.TrimSpace:它只认 Unicode White_Space, +// 而 U+200B(零宽空格)/ U+200C / U+200D / U+FEFF(BOM)都属于「看不见但不算空白」 +// 的字符 —— 反代或某些 WAF 改写 URI 时可能注入它们,TrimSpace 会原样放过, +// 甚至因为首字符不是 '/' 而额外补一个斜杠,拼出 "/\u200b/wp-login.php" 这种怪路径。 +// +// 刻意只处理首尾:中间的零宽字符可能是攻击 payload 的一部分,取证要保留原样。 +// 也刻意不改大小写:URL 路径大小写敏感。 func cleanPath(p string) string { - p = strings.TrimSpace(p) + p = strings.TrimFunc(p, func(r rune) bool { + switch r { + case '\u200b', '\u200c', '\u200d', '\ufeff': + return true + } + return unicode.IsSpace(r) + }) if p == "" { return "/" } diff --git a/internal/route/honeypot_test.go b/internal/route/honeypot_test.go index b36eab1..9031270 100644 --- a/internal/route/honeypot_test.go +++ b/internal/route/honeypot_test.go @@ -81,13 +81,16 @@ func newHoneypotTestServerFull(t *testing.T, enabled, autoBan bool, cs honeypot. // 取证数据变脏,爆破脚本还可能把表单提交到错误路径。 func TestCleanPath(t *testing.T) { cases := map[string]string{ - "/wp-login.php": "/wp-login.php", - " /wp-login.php": "/wp-login.php", - "\t/wp-login.php": "/wp-login.php", - "wp-login.php": "/wp-login.php", - "": "/", - " ": "/", - "/ADMIN": "/ADMIN", // 不改大小写:URL 路径大小写敏感 + "/wp-login.php": "/wp-login.php", + " /wp-login.php": "/wp-login.php", + "\t/wp-login.php": "/wp-login.php", + "wp-login.php": "/wp-login.php", + "": "/", + " ": "/", + "/ADMIN": "/ADMIN", // 不改大小写:URL 路径大小写敏感 + "\u200b/wp-login.php": "/wp-login.php", // 零宽空格(TrimSpace 不认) + "\ufeff/wp-login.php": "/wp-login.php", // BOM + "/wp\u200b-login.php": "/wp\u200b-login.php", // 中间的零宽字符保留(可能是 payload) } for in, want := range cases { if got := cleanPath(in); got != want {