package honeypot
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestIsXMLRPCPath(t *testing.T) {
cases := map[string]bool{
"/xmlrpc.php": true,
"/xmlrpc": true,
"/XMLRPC.PHP": true,
"/xmlrpc.php/": true,
"/xmlrpcx.php": false,
"/wp-login.php": false,
"": false,
}
for p, want := range cases {
if got := IsXMLRPCPath(p); got != want {
t.Errorf("IsXMLRPCPath(%q) = %v, want %v", p, got, want)
}
}
}
// TestExtractXMLRPCCredentialsUsersBlogs 覆盖最常见的爆破形态:
// wp.getUsersBlogs 的参数是 [站点地址, 用户名, 密码],站点地址必须被剔除。
func TestExtractXMLRPCCredentialsUsersBlogs(t *testing.T) {
body := `
wp.getUsersBlogs
https://target.test/xmlrpc.php
admin
s3cr3t
`
req := httptest.NewRequest(http.MethodPost, "/xmlrpc.php", strings.NewReader(body))
req.Header.Set("Content-Type", "text/xml")
creds := ExtractXMLRPCCredentials(req)
if !creds.HadFields {
t.Fatal("HadFields = false, want true(这是标准的凭据提交)")
}
if creds.Username != "admin" || creds.Password != "s3cr3t" {
t.Errorf("凭据 = %q/%q, want admin/s3cr3t(站点 URL 必须被剔除)", creds.Username, creds.Password)
}
if got := creds.Fields["xmlrpc.method"]; got != "wp.getUsersBlogs" {
t.Errorf("xmlrpc.method = %q", got)
}
}
// TestExtractXMLRPCCredentialsMulticall 覆盖 system.multicall:
// 一包多组凭据时,第一组进标准字段,其余进 cred2/cred3,不能只剩第一组。
func TestExtractXMLRPCCredentialsMulticall(t *testing.T) {
pacel := func(user, pass string) string {
return `
methodNamewp.getUsersBlogs
params
https://target.test/xmlrpc.php
` + user + `
` + pass + `
`
}
body := `system.multicall` +
pacel("root", "toor") + pacel("root", "hunter2") +
``
req := httptest.NewRequest(http.MethodPost, "/xmlrpc.php", strings.NewReader(body))
creds := ExtractXMLRPCCredentials(req)
if creds.Username != "root" || creds.Password != "toor" {
t.Errorf("第一组 = %q/%q, want root/toor", creds.Username, creds.Password)
}
if got := creds.Fields["cred2.password"]; got != "hunter2" {
t.Errorf("cred2.password = %q, want hunter2(multicall 的字典不能只留第一组)", got)
}
}
// TestExtractXMLRPCCredentialsNumericBlogID 覆盖 blog_id=1 的错位场景:
// 参数是 [1, user, pass],前面的 URL 过滤没有剔除 "1",配对会整体错一位。
func TestExtractXMLRPCCredentialsNumericBlogID(t *testing.T) {
body := `wp.getUsersBlogs
1
admin
123456
`
req := httptest.NewRequest(http.MethodPost, "/xmlrpc.php", strings.NewReader(body))
creds := ExtractXMLRPCCredentials(req)
if creds.Username != "admin" || creds.Password != "123456" {
t.Errorf("凭据 = %q/%q, want admin/123456(短数字首位应视为 blog_id 丢弃)",
creds.Username, creds.Password)
}
}
// TestExtractXMLRPCCredentialsIgnored 覆盖「不是 XML-RPC 调用」的请求:
// 不能把普通探测误判成凭据提交(否则会触发一封假告警并立即封禁)。
func TestExtractXMLRPCCredentialsIgnored(t *testing.T) {
for _, body := range []string{
"log=admin&pwd=x",
`{"log":"admin","pwd":"x"}`,
`system.listMethods`,
} {
req := httptest.NewRequest(http.MethodPost, "/xmlrpc.php", strings.NewReader(body))
creds := ExtractXMLRPCCredentials(req)
if creds.HadFields {
t.Errorf("body %q: HadFields = true, want false", body)
}
}
}
func TestWriteXMLRPCFault(t *testing.T) {
for _, status := range []int{http.StatusOK, http.StatusMethodNotAllowed} {
rec := httptest.NewRecorder()
WriteXMLRPCFault(rec, status, 403, XMLRPCFaultBadLogin)
if rec.Code != status {
t.Errorf("status = %d, want %d", rec.Code, status)
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/xml") {
t.Errorf("Content-Type = %q, want text/xml(真实 WordPress 不会回 HTML)", ct)
}
body := rec.Body.String()
if !strings.Contains(body, "403") || !strings.Contains(body, XMLRPCFaultBadLogin) {
t.Errorf("body = %q, want 含 faultCode 与文案", body)
}
if strings.Contains(body, "system.multicall") {
t.Errorf("body 缺 system.multicall:%q", body)
}
if !strings.Contains(body, "") {
t.Errorf("body 不是 methodResponse:%q", body)
}
}