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:
tamakyi
2026-10-23 12:38:35 +08:00
parent edde075406
commit 811578dcf1
9 changed files with 1067 additions and 108 deletions
+77
View File
@@ -0,0 +1,77 @@
// Command preview-honeypot 把全部伪装家族渲染成静态 HTML,供人工核对外观。
//
// 用法:go run ./dev/preview-honeypot
// 产物:outputs/honeypot-preview/index.html(9 个 iframe 并排)
//
// 改动 internal/security/honeypot/family.go 的皮肤后重新跑一次,
// 比对着源码想象 CSS 可靠得多 —— 上一版就是靠它发现 Tomcat 按钮没背景色。
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/tamakyi/TamaBox/internal/security/honeypot"
)
type item struct {
fam honeypot.Family
action string
note string
}
func main() {
dir := "outputs/honeypot-preview"
if err := os.MkdirAll(dir, 0o755); err != nil {
panic(err)
}
items := []item{
{honeypot.FamilyWordPress, "/wp-login.php", "/wp-login.php · /wp-admin"},
{honeypot.FamilyPhpMyAdmin, "/phpmyadmin/index.php", "/phpmyadmin · /pma"},
{honeypot.FamilyTomcat, "/manager/html", "/manager/html · 401 Basic"},
{honeypot.FamilyJenkins, "/jenkins/login", "/jenkins"},
{honeypot.FamilyGrafana, "/grafana/login", "/grafana"},
{honeypot.FamilyGitLab, "/users/sign_in", "/users/sign_in"},
{honeypot.FamilyLuCI, "/cgi-bin/luci", "/cgi-bin/luci"},
{honeypot.FamilyJoomla, "/administrator/index.php", "/administrator"},
{honeypot.FamilyGeneric, "/admin", "/admin · /console"},
}
var cards strings.Builder
for _, it := range items {
name := it.fam.String()
body := honeypot.RenderFamily(it.fam, honeypot.LoginPageVars{
Action: it.action,
Redir: "/wp-admin/",
Failed: false,
})
if err := os.WriteFile(filepath.Join(dir, name+".html"), body, 0o644); err != nil {
panic(err)
}
fmt.Fprintf(&cards, `<figure><figcaption><b>%s</b><span>%s</span></figcaption><iframe src="%s.html" title="%s"></iframe></figure>`,
name, it.note, name, name)
}
page := `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<title>蜜罐伪装家族预览</title>
<style>
body{margin:0;padding:28px 24px 40px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f6f7f9;color:#1d2327}
h1{font-size:18px;font-weight:600;margin:0 0 6px}
p.lead{margin:0 0 22px;font-size:13px;color:#646970}
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:18px}
figure{margin:0;background:#fff;border:1px solid #dcdcde;border-radius:8px;overflow:hidden}
figcaption{display:flex;justify-content:space-between;gap:8px;padding:9px 12px;font-size:12px;border-bottom:1px solid #f0f0f1}
figcaption span{color:#787c82}
iframe{width:100%;height:520px;border:0;display:block}
</style></head><body>
<h1>蜜罐:9 个伪装家族</h1>
<p class="lead">每个 iframe 是 /wp-admin 这类诱饵路径的真实响应(同一份 RenderFamily 输出,未做任何美化)。Tomcat 家族在真实响应里是 401 + Basic Auth,这里有页面是为了看内容。</p>
<div class="grid">` + cards.String() + `</div></body></html>`
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(page), 0o644); err != nil {
panic(err)
}
fmt.Println("预览已生成:", filepath.Join(dir, "index.html"))
}