fix(honeypot): cleanPath 处理零宽字符,并在清洗生效时打诊断日志
上一版只用 strings.TrimSpace,实测漏掉「看不见但不算 White_Space」的字符:
cleanPath("\u200b/wp-login.php") = "/\u200b/wp-login.php"
(零宽没被删,还因为首字符不是 '/' 多补了一个斜杠,拼出更怪的路径)
改为 TrimFunc 同时匹配 unicode.IsSpace 与 U+200B/200C/200D/FEFF,且只处理首尾
—— 中间的零宽字符可能是攻击 payload,取证要保留原样。
另加诊断日志:清洗前后不一致时 WARN,用 %q 打出原始值。
测试环境出现过「记录里 path 带前导空白、本地链路却复现不出来」的情况,
不可见字符必须转义输出才定位得到。
测试:TestCleanPath 补零宽空格 / BOM / 中间零宽字符保留三个用例。
This commit is contained in:
@@ -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`)。
|
||||
|
||||
@@ -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` 会因端口被占直接退出。
|
||||
|
||||
@@ -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 "/"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user