package honeypot import ( "encoding/base64" "net/http" "strings" ) // 静态资源伪装。 // // 为什么必须做:真实后台登录页一定会加载 CSS/JS/图标。如果这些请求返回 302 或 // 登录页 HTML,有经验的扫描器立刻能判定「页面是假的」,后面的爆破就不会发生。 // // 成本控制:内容全是包级常量(首次调用时解码一次),请求时零分配、零 IO。 // 这类请求**不落库、不计数** —— 一个页面 5–10 个资源请求,如果每个都 INSERT, // 等于帮攻击者把写入放大了十倍(见 route 层 Serve 的短路)。 // assetPNG 是 1x1 透明 PNG(67 字节)。用 base64 常量而不是手写字节数组: // 手写十六进制一位写错就是一个坏图,扫描器加载失败会暴露。 var assetPNG = mustBase64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==") var ( assetCSS = []byte("/*! This file is auto-generated. */\n" + ".wp-core-ui,.pma-header{box-sizing:border-box}\n" + "body.login #login{width:320px;margin:0 auto}\n") assetJS = []byte("/*! generated */\n" + "var wp=window.wp||{};wp.hooks={addFilter:function(){return 0}};\n") assetSVG = []byte(``) assetMap = []byte(`{"version":3,"sources":[],"mappings":""}`) assetEmpty = []byte{} ) func mustBase64(s string) []byte { b, err := base64.StdEncoding.DecodeString(s) if err != nil { // 常量写错属于编译期错误,这里 panic 是正确的:静默降级会让蜜罐返回坏图。 panic("honeypot: invalid embedded png: " + err.Error()) } return b } // assetPrefixes 只有这些前缀下的请求才会被当作「页面资源」。 // // 刻意不含 /static、/assets、/js、/css:这些是站点的真实静态资源前缀 // (route 层的 forbiddenPrefixes 会先剔除它们),纳入进来没有意义还可能误伤。 var assetPrefixes = []string{ "/wp-admin/css/", "/wp-admin/js/", "/wp-admin/images/", "/wp-admin/fonts/", "/wp-includes/css/", "/wp-includes/js/", "/wp-includes/images/", "/wp-content/plugins/", "/wp-content/themes/", "/wp-content/uploads/", "/phpmyadmin/themes/", "/phpmyadmin/js/", "/phpmyadmin/img/", "/jenkins/static/", "/jenkins/css/", "/jenkins/scripts/", "/jenkins/images/", "/grafana/public/build/", "/grafana/public/img/", "/cgi-bin/luci/static/", "/luci-static/", "/administrator/templates/", "/administrator/media/", } // AssetFor 判断请求是否为静态资源,并返回占位内容。 // // 返回值: // - ok=false:不是资源请求,调用方按登录页处理; // - ok=true:直接写出 body(status 为 200 或 404),不记录、不计数。 // // 已知后缀以外的文件(例如 /wp-content/plugins/x.php)返回 404: // 这类请求本身是 webshell 探测,回一个「找不到」比回登录页更符合真实站点。 func AssetFor(p string) (body []byte, ctype string, status int, ok bool) { isAsset := false for _, pre := range assetPrefixes { if strings.HasPrefix(p, pre) { isAsset = true break } } if !isAsset { return nil, "", 0, false } lower := strings.ToLower(p) switch { case strings.HasSuffix(lower, ".css"): return assetCSS, "text/css; charset=utf-8", http.StatusOK, true case strings.HasSuffix(lower, ".js"): return assetJS, "application/javascript; charset=utf-8", http.StatusOK, true case strings.HasSuffix(lower, ".png"), strings.HasSuffix(lower, ".gif"), strings.HasSuffix(lower, ".jpg"), strings.HasSuffix(lower, ".jpeg"), strings.HasSuffix(lower, ".ico"): return assetPNG, "image/png", http.StatusOK, true case strings.HasSuffix(lower, ".svg"): return assetSVG, "image/svg+xml", http.StatusOK, true case strings.HasSuffix(lower, ".map"), strings.HasSuffix(lower, ".json"): return assetMap, "application/json; charset=utf-8", http.StatusOK, true case strings.HasSuffix(lower, ".woff"), strings.HasSuffix(lower, ".woff2"), strings.HasSuffix(lower, ".ttf"), strings.HasSuffix(lower, ".eot"): // 字体不需要真内容:浏览器放弃渲染只会退化成默认字体,肉眼与扫描器都看不出来。 return assetEmpty, "font/woff2", http.StatusOK, true default: return assetEmpty, "text/html; charset=utf-8", http.StatusNotFound, true } }