原来所有诱饵路径都回同一个 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 → 页面缺特征字段。两项均按预期变红。
348 lines
11 KiB
Go
348 lines
11 KiB
Go
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 · 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 · 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 · 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 · 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 · 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 · 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 · 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
|
||
}
|