Files
tamabox/internal/security/honeypot/family.go
T
tamakyi 811578dcf1 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 → 页面缺特征字段。两项均按预期变红。
2026-10-23 12:38:35 +08:00

348 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package honeypot
import "strings"
// Family 是诱饵页面模拟的产品类别。
//
// 为什么要分类:扫描器与爆破脚本几乎都是「先按路径判断这是什么后台,
// 再用它认识的字段名去填表」。所有路径都回同一个 WordPress 页面,等于在
// /phpmyadmin/index.php 上告诉对方「这里不是 PMA」—— 对方直接换目标,
// 我们既拿不到凭据,也留不下有价值的取证。
//
// 刻意只做「登录页」这一层:不需要真实的业务逻辑,只需要让对方向表单提交。
type Family int
const (
// FamilyGeneric 是兜底:/admin、/console 这类通用后台。
FamilyGeneric Family = iota
FamilyWordPress
FamilyPhpMyAdmin
FamilyTomcat
FamilyJenkins
FamilyGrafana
FamilyGitLab
FamilyLuCI
FamilyJoomla
)
func (f Family) String() string {
switch f {
case FamilyWordPress:
return "wordpress"
case FamilyPhpMyAdmin:
return "phpmyadmin"
case FamilyTomcat:
return "tomcat"
case FamilyJenkins:
return "jenkins"
case FamilyGrafana:
return "grafana"
case FamilyGitLab:
return "gitlab"
case FamilyLuCI:
return "luci"
case FamilyJoomla:
return "joomla"
default:
return "generic"
}
}
// skinField 是登录表单里的一个输入框。
type skinField struct {
name string
label string
secret bool
}
// loginSkin 是一个家族的「伪装设定」:外观、字段名、失败文案、响应头。
//
// 全部是编译期常量,渲染时只做一次字符串拼接 —— 没有模板解析、没有文件 IO。
type loginSkin struct {
family Family
routes []string // 段前缀,命中即归入该家族(顺序敏感,先精确后泛化)
title string
brand string
tagline string // 品牌下方的版本行,扫描器常拿它判断产品
submit string
class string
form string // 表单 name/id
gen string // <meta name="generator">,空则不输出
css string // 家族配色(叠加在 baseLoginCSS 之后)
fields []skinField
hidden [][2]string // 值写成 "$redirect" 表示回填原始查询串
failure string
footer string
headers [][2]string
cookies []string
// basicRealm 非空时改用 HTTP 401 + Basic Auth(Tomcat Manager 的真实行为)。
// 凭据从 Authorization 头取,ExtractCredentials 已支持。
basicRealm string
}
// skins 按顺序匹配:越具体的路径要排在越前面。
//
// 所有 routes 都必须与业务路由零交集(route 层的 forbiddenPrefixes 是第二道保险)。
var skins = []loginSkin{
{
family: FamilyWordPress,
routes: []string{
"/wp-login.php", "/wp-login", "/wp-admin", "/wp-content", "/wp-includes",
"/wp-json", "/xmlrpc.php", "/xmlrpc", "/wp-cron.php", "/wp-trackback.php",
"/wp-comments-post.php", "/wp-config", "/wp-mail.php", "/wp-signup.php",
"/wp-activate.php", "/wp-links-opml.php", "/wp-blog-header.php",
"/wp-load.php", "/wp-settings.php",
},
title: LoginPageTitle,
brand: "WordPress",
tagline: "",
submit: "Log In",
class: "login",
form: "loginform",
gen: "WordPress 6.4.3",
css: `h1 a{color:#3c434a;text-decoration:none}button{background:#2271b1}`,
fields: []skinField{
{name: "log", label: "Username"},
{name: "pwd", label: "Password", secret: true},
},
hidden: [][2]string{
{"redirect_to", "$redirect"},
{"testcookie", "1"},
},
failure: `ERROR: The password you entered for the username is incorrect.`,
// 真实 wp-login.php 底部没有 404 文案,只有「← 返回站点」;
// 之前挂 NotFoundFooter 反而在一张登录页上写了「资源不存在」,很扎眼。
footer: "",
headers: [][2]string{
{"X-Powered-By", "PHP/7.4.33"},
{"X-Pingback", "/xmlrpc.php"},
},
cookies: []string{"wordpress_test_cookie=WP%20Cookie%20check; path=/"},
},
{
family: FamilyPhpMyAdmin,
routes: []string{"/phpmyadmin", "/pma", "/myadmin", "/dbadmin", "/sqladmin", "/mysqladmin", "/adminer.php", "/adminer"},
title: "phpMyAdmin",
brand: "phpMyAdmin",
tagline: "Welcome to phpMyAdmin 5.2.1",
submit: "Log in",
class: "pma",
form: "login_form",
gen: "",
css: `.card{width:360px}h1{color:#235a81;font-weight:700}.tag{color:#235a81;font-size:12px}button{background:#235a81}`,
fields: []skinField{
{name: "pma_username", label: "Username:"},
{name: "pma_password", label: "Password:", secret: true},
},
hidden: [][2]string{
{"server", "1"},
{"target", "index.php"},
{"token", "e1a4c0b7d2f58g93"},
},
failure: `#1045 - Cannot log in to the MySQL server`,
footer: "Language: English &middot; Theme: pmahomme",
headers: [][2]string{
{"X-Powered-By", "PHP/7.4.33"},
{"Set-Cookie", "pma_lang=en; path=/; HttpOnly"},
},
},
{
family: FamilyTomcat,
routes: []string{"/manager/html", "/manager", "/host-manager", "/examples", "/jmx-console", "/web-console", "/invoker"},
title: "401 Unauthorized",
brand: "Tomcat Manager Application",
tagline: "Apache Tomcat/9.0.83",
submit: "",
class: "tomcat",
form: "login",
css: `h1{font-size:19px;font-weight:700;text-align:left}.tag{text-align:left}button{background:#5b6b73}button:hover{background:#4a575e}`,
footer: "401 Unauthorized &middot; this request requires HTTP authentication",
basicRealm: "Tomcat Manager Application",
headers: [][2]string{
{"X-Powered-By", "Servlet/4.0 JSP/2.3"},
},
},
{
family: FamilyJenkins,
routes: []string{"/jenkins", "/jnlpJars", "/jenkins/", "/script", "/administrativeMonitor"},
title: "Jenkins",
brand: "Jenkins",
tagline: "Welcome to Jenkins 2.401.3",
submit: "Sign in",
class: "jenkins",
form: "login",
gen: "",
css: `.card{width:420px}h1{text-align:left;font-weight:700;color:#333}.tag{text-align:left}button{background:#4477aa}`,
fields: []skinField{
{name: "j_username", label: "Username"},
{name: "j_password", label: "Password", secret: true},
},
hidden: [][2]string{{"from", "/"}, {"Submit", "Sign in"}},
failure: "Invalid username or password",
footer: "Jenkins ver. 2.401.3",
headers: [][2]string{
{"X-Jenkins", "2.401.3"},
{"X-Jenkins-Session", "8b1f2c4d"},
},
},
{
family: FamilyGrafana,
routes: []string{"/grafana"},
title: "Grafana",
brand: "Grafana",
tagline: "Welcome to Grafana 10.2.3",
submit: "Log in",
class: "grafana",
form: "loginForm",
gen: "",
css: `body{background:#111217;color:#d8d9da}.card{background:#181b1f;border-color:#2c3235}h1{color:#f4f5f5}.tag{color:#9da3a8}input[type=text],input[type=password]{background:#0b0c0e;color:#d8d9da;border-color:#3d444b}button{background:#3d71d9}.foot{color:#8e9297}`,
fields: []skinField{
{name: "user", label: "Email or username"},
{name: "password", label: "Password", secret: true},
},
hidden: [][2]string{{"userlang", "en-US"}},
failure: "Invalid username or password",
footer: "Grafana Labs &middot; v10.2.3",
headers: [][2]string{
{"X-Grafana-Org-Id", "1"},
},
},
{
family: FamilyGitLab,
routes: []string{"/users/sign_in", "/users/password", "/users/sign_up"},
title: "Sign in &middot; GitLab",
brand: "GitLab",
tagline: "Open source DevSecOps platform",
submit: "Sign in",
class: "gitlab",
form: "new_user",
gen: "",
css: `.card{width:380px;border-color:#e5e5e5}h1{font-weight:700;color:#1f1e24}.tag{text-align:left}button{background:#1f75cb}`,
fields: []skinField{
{name: "user[login]", label: "Username or email"},
{name: "user[password]", label: "Password", secret: true},
},
hidden: [][2]string{{"user[remember_me]", "0"}},
failure: "Invalid Login or password.",
footer: "GitLab Community Edition 16.8.1",
headers: [][2]string{
{"X-Gitlab-Meta", `{"correlation_id":"a1b2c3"}`},
},
},
{
family: FamilyLuCI,
routes: []string{"/cgi-bin/luci", "/luci", "/luci-static"},
title: "LuCI - Lua Configuration Interface",
brand: "LuCI",
tagline: "OpenWrt 23.05 &middot; Lua Configuration Interface",
submit: "Login",
class: "luci",
form: "login",
gen: "",
css: `.card{width:360px;border-top:3px solid #4a9c3f}h1{color:#4a9c3f;font-size:19px}button{background:#4a9c3f}`,
fields: []skinField{
{name: "luci_username", label: "Username"},
{name: "luci_password", label: "Password", secret: true},
},
failure: "Invalid username and/or password! Please try again.",
footer: "Powered by LuCI openwrt-23.05 branch",
headers: [][2]string{
{"X-Powered-By", "PHP/7.4.33"},
},
},
{
family: FamilyJoomla,
routes: []string{"/administrator"},
title: "Joomla! Administration Login",
brand: "Joomla!",
tagline: "Administration Login &middot; Joomla! 4.4.2",
submit: "Log in",
class: "joomla",
form: "form-login",
gen: "Joomla! - Open Source Content Management",
css: `.card{border-top:3px solid #1a3867}.card h1{color:#1a3867}button{background:#1a3867}`,
fields: []skinField{
{name: "username", label: "Username"},
{name: "passwd", label: "Password", secret: true},
},
hidden: [][2]string{
{"option", "com_login"},
{"task", "login"},
{"return", "aW5kZXgucGhw"},
},
failure: "Username and password do not match or you do not have an account yet.",
footer: "Joomla! 4.4.2 &middot; Copyright (C) Open Source Matters",
headers: [][2]string{
{"X-Powered-By", "PHP/7.4.33"},
},
},
{
family: FamilyGeneric,
routes: []string{"/admin", "/console", "/webadmin", "/login.php", "/admin.php", "/admin/login", "/admin/index.php", "/cpanel", "/whm", "/plesk", "/webmail", "/roundcube", "/zabbix", "/nagios", "/monitoring", "/jira", "/confluence", "/gitlab", "/nextcloud", "/owncloud"},
title: "Administration",
brand: "Administration Console",
tagline: "Sign in to continue",
submit: "Sign in",
class: "generic",
form: "loginForm",
gen: "",
css: `.card{width:360px}h1{font-weight:600;font-size:19px}button{background:#3c434a}`,
fields: []skinField{
{name: "username", label: "Username"},
{name: "password", label: "Password", secret: true},
},
hidden: [][2]string{{"csrf_token", "a7f3c1e9b2d4"}},
failure: "Invalid username or password.",
footer: "Restricted area. Authorized personnel only.",
headers: [][2]string{
{"X-Powered-By", "PHP/7.4.33"},
},
},
}
// FamilyOf 判断路径属于哪个家族(p 需已 normalize:小写、斜杠已折叠)。
//
// 段前缀语义:/wp-admin 匹配 /wp-admin 与 /wp-admin/setup-config.php,
// 但不匹配 /wp-administrator。'.' 也算边界,兼容 /adminer.php 这类写法。
func FamilyOf(p string) Family {
for i := range skins {
for _, r := range skins[i].routes {
if hasSegPrefix(p, r) {
return skins[i].family
}
}
}
return FamilyGeneric
}
func hasSegPrefix(p, prefix string) bool {
if p == prefix {
return true
}
if !strings.HasPrefix(p, prefix) {
return false
}
next := p[len(prefix)]
return next == '/' || next == '.'
}
// skinOf 返回家族对应的伪装设定。
func skinOf(f Family) *loginSkin {
for i := range skins {
if skins[i].family == f {
return &skins[i]
}
}
return &skins[len(skins)-1] // FamilyGeneric 永远在最后
}
// Families 返回全部家族(供 CLI / 测试枚举)。
func Families() []Family {
out := make([]Family, 0, len(skins))
for i := range skins {
out = append(out, skins[i].family)
}
return out
}