线上验收发现两个「能被指纹识别」的破绽,都不影响原有功能但削弱伪装: 一、/xmlrpc.php 回的是 HTML 登录页(P1) 我们自己发的响应头里就有 X-Pingback: /xmlrpc.php —— 扫描器顺着跟过来, 一看是 HTML 就知道整站是假的(真实 WordPress 在这里回 text/xml)。 更可惜的是 system.multicall 这条通道被浪费:一次请求能带上千组用户名/密码、 且绕开登录页限速插件,是 WP 爆破里产出最高的入口。 新增 xmlrpc.go: - 非 POST → 405 + fault「XML-RPC services are disabled on this site.」(与真实 WP 一致) - POST methodCall → 200 + fault「Incorrect username or password.」(不区分用户是否存在) - POST system.listMethods → 200 + 方法列表(枚举类工具靠它确认对面是真 WP) - 凭据提取刻意不上 encoding/xml(蜜罐被高频命中):抓所有 <string>、剔除 methodName 与站点 URL 后两两配对。wp.getUsersBlogs 的 blog_id 若是短数字会让 配对错位一位,故「奇数长度 + 首项短数字」时丢首项;另外原始串全量写进 fields(xmlrpc.0/1/…),配对失败也不丢原文。 multicall 的第一组进标准字段,其余进 cred2/cred3… —— 一个用户名配一长串 密码是常见字典形态,只留第一组等于丢掉整份字典。 - 封禁与表单提交同规则:带凭据走 RecordHoneypot(立即封),否则走常规阈值。 二、进入页面的隐藏值每次请求都一样(P2) 两次请求拿到同一个 pma token / csrf_token / X-Jenkins-Session,任何脚本都能 判定对面是伪造的。改为按请求生成 32 位 hex(math/rand/v2 —— 这些是假 token, 不需要不可预测,只需要每次不同;crypto/rand 在 Windows 要走系统调用,贵一个量级), 页头、hidden 与 Set-Cookie 共用同一套。 顺带: - X-Pingback 改绝对地址($base/xmlrpc.php)。真实 WP 给绝对 URL,相对路径本身 就是破绽;没配 external_url 时干脆不发这个头。 - Cache-Control 从 no-store 改 no-cache, must-revalidate, max-age=0 (no-store 更像安全设备/代理加的,不像登录页)。 - Grafana / GitLab 补上真实产品会种的会话 cookie(grafana_sess、_gitlab_session)。 - Match 增加 .php 变体兜底:清单里是 /phpinfo,扫描器打 /phpinfo.php, 原来漏掉 → 302 回首页,等于明确告诉对方「这个文件不存在」。 测试:xmlrpc_test(IsXMLRPCPath / getUsersBlogs / multicall / 数字 blog_id / 非 XML-RPC body / fault 与 listMethods 输出)、family_test(动态 token 两次不同且 是合法 hex、X-Jenkins-Session 与 correlation_id 动态、Pingback 绝对地址与缺省不发、 所有家族渲染后不残留 $ 占位符)、route 层(POST multicall 捕获凭据 + 立即封 + 回 text/xml;GET 回 405 fault 而非登录页)、honeypot(.php 变体命中且不牵连业务路径)。 四处变异验证均按预期变红:固定 token、xmlrpc 分支失效、Pingback 退化成相对路径、 .php 兜底失效。 性能:RenderFamily 3.7µs→4.18µs、2→3 allocs(多的一次是 hex 编码), AssetFor 仍 0 分配。
350 lines
11 KiB
Go
350 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", "$base/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", "$hex32"},
|
||
},
|
||
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", "$hex8"},
|
||
},
|
||
},
|
||
{
|
||
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"},
|
||
},
|
||
cookies: []string{"grafana_sess=$hex32; Path=/; HttpOnly"},
|
||
},
|
||
{
|
||
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":"$hex32"}`},
|
||
},
|
||
cookies: []string{"_gitlab_session=$hex32; path=/; HttpOnly"},
|
||
},
|
||
{
|
||
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", "$hex16"}},
|
||
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
|
||
}
|