Files
tamabox/internal/security/honeypot/page.go
T
2026-10-21 12:09:28 +08:00

111 lines
5.7 KiB
Go
Raw 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.
package honeypot
import (
"html"
"net/http"
"strings"
)
// LoginPageVars 是假登录页的渲染参数。
type LoginPageVars struct {
// Action 是表单提交地址(当前诱饵路径本身)。
Action string
// Title / Heading 是页面标题与表单标题。
Title string
Heading string
// Notice 是提交失败后的提示语。
Notice string
// Redir 为原始请求的查询串(WordPress 风格的 ?redirect_to=),原样回填。
Redir string
}
// RenderLoginPage 输出仿 WordPress 后台风格的假登录页(200,text/html)。
//
// 页面刻意包含:
// - 隐藏 canary 标记(HTML 注释 + meta),便于日后溯源;
// - 经典 WP 字段名 log / pwd / redirect_to —— 让暴力破解脚本原样提交,
// 我们就能拿到最干净的用户名与密码对;
// - 一个 nginx/Apache 风格的 404 页脚,暗示「这个后台真的存在」,
// 但正文是纯静态 HTML,不泄露任何版本、路径或真实凭据。
func RenderLoginPage(vars LoginPageVars) []byte {
action := html.EscapeString(vars.Action)
title := vars.Title
if title == "" {
title = "Log In ‹ WordPress"
}
heading := vars.Heading
if heading == "" {
heading = "WordPress"
}
notice := ""
if vars.Notice != "" {
notice = `<div id="login_error">` + html.EscapeString(vars.Notice) + `</div>`
}
redir := html.EscapeString(vars.Redir)
var b strings.Builder
b.Grow(4096)
b.WriteString(`<!DOCTYPE html>` + "\n")
b.WriteString(`<!-- canary: ` + CanaryToken + ` -->` + "\n")
b.WriteString(`<html lang="en-US">` + "\n")
b.WriteString(`<head>` + "\n")
b.WriteString(`<meta charset="UTF-8" />` + "\n")
b.WriteString(`<meta name="viewport" content="width=device-width, initial-scale=1" />` + "\n")
b.WriteString(`<meta name="generator" content="TamaBox honeypot ` + CanaryToken + `" />` + "\n")
b.WriteString(`<title>` + html.EscapeString(title) + `</title>` + "\n")
// 静态内联样式,避免蜜罐依赖站点静态资源(也避免污染真实缓存)
b.WriteString(`<style>` + honeypotCSS + `</style>` + "\n")
b.WriteString(`</head>` + "\n")
b.WriteString(`<body class="login">` + "\n")
b.WriteString(`<div id="login">` + "\n")
b.WriteString(`<h1><a href="` + action + `" title="` + html.EscapeString(heading) + `">` + html.EscapeString(heading) + `</a></h1>` + "\n")
b.WriteString(notice + "\n")
b.WriteString(`<form name="loginform" id="loginform" action="` + action + `" method="post" enctype="application/x-www-form-urlencoded">` + "\n")
b.WriteString(`<p><label for="user_login">Username<br /><input type="text" name="log" id="user_login" class="input" value="" size="20" autocapitalize="off" autocomplete="username" /></label></p>` + "\n")
b.WriteString(`<p><label for="user_pass">Password<br /><input type="password" name="pwd" id="user_pass" class="input" value="" size="20" autocomplete="current-password" /></label></p>` + "\n")
b.WriteString(`<p class="forgetmenot"><input name="rememberme" type="checkbox" id="rememberme" value="forever" /> Remember Me</p>` + "\n")
b.WriteString(`<p class="submit"><input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" value="Log In" /></p>` + "\n")
b.WriteString(`<input type="hidden" name="redirect_to" value="` + redir + `" />` + "\n")
b.WriteString(`<input type="hidden" name="testcookie" value="1" />` + "\n")
b.WriteString(`</form>` + "\n")
b.WriteString(`<p id="nav"><a href="` + action + `?action=lostpassword">Lost your password?</a> | <a href="` + action + `?action=register">Register</a></p>` + "\n")
b.WriteString(`</div>` + "\n")
b.WriteString(`<div id="footer"><p>` + html.EscapeString(NotFoundFooter) + `</p></div>` + "\n")
b.WriteString(`</body>` + "\n")
b.WriteString(`</html>` + "\n")
return []byte(b.String())
}
const honeypotCSS = `html{background:#f0f0f1}body{margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:14px;color:#3c434a}
#login{border:1px solid #dcdcde;background:#fff;width:320px;margin:5em auto;padding:26px 24px 46px;box-shadow:0 1px 3px rgba(0,0,0,.04)}
#login h1{text-align:center;margin:0 0 1em;font-size:24px;font-weight:400}
#login h1 a{color:#444;text-decoration:none}
#login form{margin-top:16px}
#login label{display:block;font-size:14px}
#login input[type=text],#login input[type=password]{width:100%;padding:5px 8px;border:1px solid #ddd;border-radius:3px;box-sizing:border-box;margin-top:4px}
#login .button{width:100%;padding:7px 12px;background:#2271b1;border:1px solid #2271b1;border-radius:3px;color:#fff;cursor:pointer;font-size:13px}
#login .submit{margin:16px 0 0}
#login_error{border-left:4px solid #d63638;background:#fcf0f1;padding:10px 12px;margin:12px 0}
#nav,.forgetmenot{font-size:13px;color:#50575e}
#footer{text-align:center;color:#646970;font-size:12px;margin:24px auto;max-width:320px}
`
// NotFoundFooter 是 404 假页脚,用于给扫描器「后台存在但资源不存在」的错觉。
const NotFoundFooter = "The requested resource was not found on this server."
// LoginFailureNotice 是提交凭据后的统一提示(不区分「用户不存在」与「密码错误」,
// 避免因响应差异让爆破脚本调整策略;同时这也是虚假后台「存在」的证据)。
const LoginFailureNotice = "ERROR: Invalid username or password."
// WriteLoginPage 将假登录页写入响应。
func WriteLoginPage(w http.ResponseWriter, vars LoginPageVars) {
body := RenderLoginPage(vars)
h := w.Header()
h.Set("Content-Type", "text/html; charset=utf-8")
h.Set("Cache-Control", "no-store, no-cache, must-revalidate")
// 诱饵页面绝不进入任何共享/边缘缓存,也不被中间层改写。
h.Set("X-Robots-Tag", "noindex, nofollow")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(body)
}