feat(honeypot): 按扫描目标伪装成 9 类后台,静态资源不落库
原来所有诱饵路径都回同一个 WordPress 登录页。爆破工具的标准流程是 「先按路径判断这是什么后台,再拿它认识的字段名填表」——在 /phpmyadmin/index.php 上回 WP 表单,等于告诉对方「这里没有 PMA」,对方直接换目标,既拿不到凭据 也没有取证价值。 新增 family.go(皮肤表)+ asset.go(静态资源占位),page.go 改为按家族渲染: wordpress /wp-login.php /wp-admin /wp-json log/pwd、wordpress_test_cookie phpmyadmin /phpmyadmin /pma /adminer.php pma_username/pma_password、hidden token tomcat /manager/html /host-manager 401 + WWW-Authenticate: Basic(无表单) jenkins /jenkins /jnlpJars j_username/j_password、X-Jenkins grafana /grafana user/password、深色主题 gitlab /users/sign_in user[login]/user[password] luci /cgi-bin/luci /luci luci_username/luci_password joomla /administrator username/passwd、hidden option=com_login generic /admin /console /zabbix /jira… username/password Tomcat 刻意不给 HTML 表单:真实 Manager 未认证就是 401 + Basic 弹框,给表单 反而更假;Authorization: Basic 本就在 ExtractCredentials 覆盖范围内,捕获能力不减。 凭据字段: - credentialKeys 扩 pma_* / j_* / luci_* / user[login] / user[password]; - token 只用于判 HadFields,**移出 isPasswordKey** —— PMA/GitLab 的登录表单带 hidden CSRF token,而 Fields 是 map、遍历无序,原来的实现会让 token 随机 顶掉真密码,记录下来的「密码」时对时错。 资源占用(这是本次主要约束): - 渲染一页 3.7µs / 2 次分配 / 5.2KB(改前 12µs / 22 次 / 19.5KB): 不用 html/template(每次 ParseFS 是毫秒级 IO+编译),改单次 strings.Builder 并避免 "a"+b+"c" 反复分配,Replacer 提到包级; - 静态资源请求 55ns / 0 分配:CSS/JS 回常量片段、图片回真 1×1 PNG, **不解析 IP、不判封禁、不写库**——真实后台页一次加载 5–10 个资源请求, 否则等于把页面浏览放大成十几条 INSERT。插件目录下的 .php 回 404(webshell 探测)。 honeypot paths 增加家族汇总,便于确认实际生效的分布。 新增 dev/preview-honeypot:把 9 个家族渲染到 outputs/honeypot-preview/index.html 再配 headless 截图核对外观——本轮两个「源码看不出、截图一眼假」的问题 (Tomcat 忘配 css 导致按钮白字无底色、WP 登录页底部挂着 404 文案)都是它发现的。 测试:TestFamilyOf / TestEveryFamilyRendersItsOwnFields / TestTomcatUses401Basic / TestAssetFor / TestRenderIsCheap(4KB 上限)/ BenchmarkRenderFamily / BenchmarkAssetFor,route 层 TestHoneypotServesFamilySpecificPage / TestHoneypotCapturesFamilyFields / TestHoneypotAssetRequestsAreNotRecorded。 变异验证:资源短路失效 → 3 个资源全变 text/html 且落库;FamilyOf 恒返回 Generic → 页面缺特征字段。两项均按预期变红。
This commit is contained in:
@@ -58,6 +58,13 @@ var honeypotPrefixes = []string{
|
||||
"/.config", "/.npm", "/.kube", "/.vscode", "/.idea",
|
||||
"/actuator", "/swagger", "/swagger-ui", "/openapi", "/v2/api-docs", "/v3/api-docs",
|
||||
"/api-docs", "/druid", "/nacos", "/solr", "/elastic", "/apollo",
|
||||
// 其它常见后台/运维面板:每条都对应 family.go 里的一个伪装家族,
|
||||
// 这些前缀同样与业务路由零交集(业务只有 /user、/login、/_/ 等)。
|
||||
"/jenkins", "/jnlpJars", "/grafana", "/users/sign_in", "/users/password",
|
||||
"/users/sign_up", "/luci", "/monitoring", "/zabbix", "/nagios",
|
||||
"/jira", "/confluence", "/gitlab", "/nextcloud", "/owncloud",
|
||||
"/webmail", "/roundcube", "/whm", "/host-manager", "/jmx-console",
|
||||
"/web-console", "/invoker",
|
||||
"/server-status", "/server-info", "/info.php", "/phpinfo", "/test.php",
|
||||
"/backup", "/backups", "/dump", "/database", "/db", "/sql", "/mysql",
|
||||
"/config", "/configuration", "/vendor", "/storage", "/tmp", "/temp",
|
||||
@@ -274,6 +281,11 @@ var credentialKeys = map[string]bool{
|
||||
"password": true, "passwd": true, "pw": true, "email": true,
|
||||
"account": true, "login": true, "wp_user": true, "user_login": true,
|
||||
"wp_password": true, "uname": true, "nickname": true, "token": true,
|
||||
// 其它家族的表单字段名(见 family.go):
|
||||
"pma_username": true, "pma_password": true, // phpMyAdmin
|
||||
"j_username": true, "j_password": true, // Jenkins / Tomcat
|
||||
"luci_username": true, "luci_password": true, // OpenWrt LuCI
|
||||
"user[login]": true, "user[password]": true, // GitLab
|
||||
}
|
||||
|
||||
// ExtractCredentials 从请求中提取破解凭据。
|
||||
@@ -302,12 +314,18 @@ func (h *Honeypot) ExtractCredentials(r *http.Request) Credentials {
|
||||
if _, exists := out.Fields[k]; !exists {
|
||||
out.Fields[k] = v
|
||||
}
|
||||
// 注意:token 只算「凭据字段」(用于判定 HadFields),不算密码。
|
||||
// phpMyAdmin / GitLab 的登录表单会带一个 hidden CSRF token,
|
||||
// 把它当密码会让真正的 pma_password 被随机覆盖
|
||||
// (map 遍历无固定顺序,"密码"会时对时错)。
|
||||
switch k {
|
||||
case "log", "username", "user", "login", "user_login", "email", "account", "uname", "wp_user", "nickname":
|
||||
case "log", "username", "user", "login", "user_login", "email", "account", "uname", "wp_user", "nickname",
|
||||
"pma_username", "j_username", "luci_username", "user[login]":
|
||||
if out.Username == "" {
|
||||
out.Username = v
|
||||
}
|
||||
case "pwd", "password", "passwd", "pw", "pass", "wp_password", "token":
|
||||
case "pwd", "password", "passwd", "pw", "pass", "wp_password",
|
||||
"pma_password", "j_password", "luci_password", "user[password]":
|
||||
if out.Password == "" {
|
||||
out.Password = v
|
||||
}
|
||||
@@ -413,7 +431,8 @@ func (c *Credentials) finish() {
|
||||
|
||||
func isPasswordKey(k string) bool {
|
||||
switch k {
|
||||
case "pwd", "password", "passwd", "pw", "pass", "wp_password", "token":
|
||||
case "pwd", "password", "passwd", "pw", "pass", "wp_password",
|
||||
"pma_password", "j_password", "luci_password", "user[password]":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user