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) } }