Files
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

78 lines
3.0 KiB
Go
Raw Permalink 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.
// 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"))
}