├── go.mod ├── LICENSE ├── README.md └── xsschecker.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rix4uni/xsschecker 2 | 3 | go 1.23.0 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bhagirath Saxena 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## xsschecker 2 | 3 | xsschecker tests endpoints for reflected XSS by injecting payloads and checking responses. It prints vulnerable if the payload is reflected, otherwise not vulnerable. 4 | 5 | ## Installation 6 | ``` 7 | go install github.com/rix4uni/xsschecker@latest 8 | ``` 9 | 10 | ## Download prebuilt binaries 11 | ``` 12 | wget https://github.com/rix4uni/xsschecker/releases/download/v0.0.5/xsschecker-linux-amd64-0.0.5.tgz 13 | tar -xvzf xsschecker-linux-amd64-0.0.5.tgz 14 | rm -rf xsschecker-linux-amd64-0.0.5.tgz 15 | mv xsschecker ~/go/bin/xsschecker 16 | ``` 17 | Or download [binary release](https://github.com/rix4uni/xsschecker/releases) for your platform. 18 | 19 | ## Compile from source 20 | ``` 21 | git clone --depth 1 github.com/rix4uni/xsschecker.git 22 | cd xsschecker; go install 23 | ``` 24 | 25 | ## Usage 26 | ```yaml 27 | Usage: xsschecker [OPTIONS] 28 | 29 | Options: 30 | -H string 31 | Custom User-Agent header for HTTP requests. (default "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36") 32 | -ao string 33 | File to append the output instead of overwriting. 34 | -filter 35 | Print only URLs Exclude this from output, (e.g. Vulnerable/Not Vulnerable: [status] [server]). 36 | -i string 37 | Input file containing list of URLs. 38 | -match string 39 | The string(s) to match against the domain response. Separate multiple strings with commas. (required) (default "alert(1), confirm(1), prompt(1)") 40 | -maxssc int 41 | Maximum number of status code responses required before skipping all URLs from that domain, This flag only can be use with -ssc flag. (default 20) 42 | -nc 43 | Do not use colored output. 44 | -o string 45 | File to save the output. 46 | -proxy string 47 | Proxy server for HTTP requests. (e.g., http://127.0.0.1:8080) 48 | -retries int 49 | Number of retry attempts for failed HTTP requests. (default 1) 50 | -scdn string 51 | Comma-separated server names to skip all URLs for (e.g., "cloudflare,AkamaiGHost,CloudFront,Imperva"). 52 | -ssc string 53 | Comma-separated status codes to skip all URLs from a domain if encountered (e.g., 403,400). 54 | -t int 55 | Number of concurrent threads. (default 20) 56 | -timeout int 57 | Timeout for HTTP requests in seconds. (default 15) 58 | -u string 59 | Single URL to test. 60 | -v Enable verbose output for debugging purposes. 61 | -version 62 | Print the version of the tool and exit. 63 | -vuln 64 | If set, only vulnerable URLs will be printed. 65 | ``` 66 | 67 | ## Usage Examples 68 | ### Reflected XSS Mass Automation 69 | ```yaml 70 | ▶ Step 1: 71 | wget https://raw.githubusercontent.com/rix4uni/WordList/refs/heads/main/payloads/xss/favourite.txt 72 | if grep -qv "^rix4uni" "favourite.txt";then sed -i 's/^/rix4uni/' "favourite.txt";fi 73 | 74 | ▶ Step 2: 75 | echo "dell.com" | subfinder -duc -silent -nc | waybackurls | urldedupe -s | grep -aE '=|%3D' | \ 76 | egrep -aiv '.(jpg|jpeg|gif|css|tif|tiff|png|ttf|woff|woff2|icon|pdf|svg|txt|js)' | \ 77 | pvreplace -silent -payload favourite.txt -fuzzing-mode single | xsschecker -nc -match 'rix4uni' -vuln 78 | 79 | ▶ Step 3: 80 | You can run pyxss to check false positive or check manually one by one url in chrome 81 | ``` 82 | 83 | ### Reflected XSS Oneliner for 1 payload 84 | ```yaml 85 | ▶ Step 1: 86 | echo "testphp.vulnweb.com" | waybackurls | urldedupe -s | grep -aE '=|%3D' | \ 87 | egrep -aiv '.(jpg|jpeg|gif|css|tif|tiff|png|ttf|woff|woff2|icon|pdf|svg|txt|js)' | \ 88 | pvreplace -silent -payload 'rix4uni">' -fuzzing-mode single | xsschecker -nc -match 'rix4uni' -vuln 89 | 90 | ▶ Step 2: 91 | You can run pyxss to check false positive or check manually one by one url in chrome 92 | ``` 93 | 94 | ### Reflected XSS Oneliner Command1 and Reflected XSS Oneliner Command2 Results Comparison 95 | ![image](https://github.com/rix4uni/xsschecker/assets/72344025/8034668c-42c3-47b1-9fee-5a58c2c96d63) 96 | -------------------------------------------------------------------------------- /xsschecker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | const version = "0.0.5" 18 | 19 | func printUsage() { 20 | fmt.Println("Usage: xsschecker [OPTIONS]") 21 | fmt.Println("\nOptions:") 22 | flag.PrintDefaults() 23 | } 24 | 25 | func printVersion() { 26 | fmt.Printf("xsschecker version %s\n", version) 27 | } 28 | 29 | func main() { 30 | // Suppress the default error output of the flag package 31 | flag.CommandLine.Usage = func() {} 32 | 33 | // Define the flags with clearer descriptions 34 | versionFlag := flag.Bool("version", false, "Print the version of the tool and exit.") 35 | matchString := flag.String("match", "alert(1), confirm(1), prompt(1)", "The string(s) to match against the domain response. Separate multiple strings with commas. (required)") 36 | onlyVulnerable := flag.Bool("vuln", false, "If set, only vulnerable URLs will be printed.") 37 | filter := flag.Bool("filter", false, "Print only URLs Exclude this from output, (e.g. Vulnerable/Not Vulnerable: [status] [server]).") 38 | timeout := flag.Int("timeout", 15, "Timeout for HTTP requests in seconds.") 39 | outputFile := flag.String("o", "", "File to save the output.") 40 | appendOutput := flag.String("ao", "", "File to append the output instead of overwriting.") 41 | noColor := flag.Bool("nc", false, "Do not use colored output.") 42 | threads := flag.Int("t", 20, "Number of concurrent threads.") 43 | userAgent := flag.String("H", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36", "Custom User-Agent header for HTTP requests.") 44 | verbose := flag.Bool("v", false, "Enable verbose output for debugging purposes.") 45 | retries := flag.Int("retries", 1, "Number of retry attempts for failed HTTP requests.") 46 | proxy := flag.String("proxy", "", "Proxy server for HTTP requests. (e.g., http://127.0.0.1:8080)") 47 | inputFile := flag.String("i", "", "Input file containing list of URLs.") 48 | singleURL := flag.String("u", "", "Single URL to test.") 49 | skipStatusCodes := flag.String("ssc", "", "Comma-separated status codes to skip all URLs from a domain if encountered (e.g., 403,400).") 50 | maxStatusCodeSkips := flag.Int("maxssc", 20, "Maximum number of status code responses required before skipping all URLs from that domain, This flag only can be use with -ssc flag.") 51 | skipServer := flag.String("scdn", "", "Comma-separated server names to skip all URLs for (e.g., \"cloudflare,AkamaiGHost,CloudFront,Imperva\").") 52 | 53 | // Custom flag parsing to handle unknown flags 54 | flag.CommandLine.Init(os.Args[0], flag.ContinueOnError) 55 | err := flag.CommandLine.Parse(os.Args[1:]) 56 | if err != nil { 57 | printUsage() 58 | return 59 | } 60 | 61 | // Print version and exit if --version flag is provided 62 | if *versionFlag { 63 | printVersion() 64 | return 65 | } 66 | 67 | // If no flags are provided or required flags are missing, print usage and exit. 68 | if len(os.Args) == 1 { 69 | printUsage() 70 | return 71 | } 72 | 73 | if *matchString == "" { 74 | fmt.Println("Please provide a match string using the -match flag.") 75 | return 76 | } 77 | 78 | matchStrings := strings.Split(*matchString, ", ") 79 | skipStatusCodeList := make(map[int]bool) 80 | if *skipStatusCodes != "" { 81 | for _, codeStr := range strings.Split(*skipStatusCodes, ",") { 82 | code, err := strconv.Atoi(codeStr) 83 | if err != nil { 84 | fmt.Printf("Invalid status code: %s\n", codeStr) 85 | return 86 | } 87 | skipStatusCodeList[code] = true 88 | } 89 | } 90 | 91 | // Parse the skip server names into a slice 92 | skipServers := strings.Split(*skipServer, ",") 93 | 94 | sc := bufio.NewScanner(os.Stdin) 95 | 96 | if *inputFile != "" { 97 | file, err := os.Open(*inputFile) 98 | if err != nil { 99 | fmt.Println("Error opening input file:", err) 100 | return 101 | } 102 | defer file.Close() 103 | sc = bufio.NewScanner(file) 104 | } 105 | 106 | jobs := make(chan string) 107 | var wg sync.WaitGroup 108 | 109 | client := &http.Client{ 110 | Timeout: time.Duration(*timeout) * time.Second, 111 | } 112 | 113 | // Set proxy if provided 114 | if *proxy != "" { 115 | proxyURL, err := url.Parse(*proxy) 116 | if err != nil { 117 | fmt.Println("Error parsing proxy URL:", err) 118 | return 119 | } 120 | client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)} 121 | } 122 | 123 | // Create or open output file if specified 124 | var output *os.File 125 | if *outputFile != "" { 126 | output, err = os.Create(*outputFile) 127 | if err != nil { 128 | fmt.Println("Error creating output file:", err) 129 | return 130 | } 131 | defer output.Close() 132 | } else if *appendOutput != "" { 133 | output, err = os.OpenFile(*appendOutput, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) 134 | if err != nil { 135 | fmt.Println("Error opening output file for appending:", err) 136 | return 137 | } 138 | defer output.Close() 139 | } 140 | 141 | skippedDomains := make(map[string]int) 142 | skippedDomainsLimitReached := make(map[string]bool) 143 | var skippedDomainsLock sync.Mutex 144 | 145 | for i := 0; i < *threads; i++ { 146 | wg.Add(1) 147 | go func() { 148 | defer wg.Done() 149 | for domain := range jobs { 150 | urlParsed, err := url.Parse(domain) 151 | if err != nil { 152 | if *verbose { 153 | fmt.Println("Error parsing URL:", err) 154 | } 155 | continue 156 | } 157 | host := urlParsed.Host 158 | 159 | // Check if the domain should be skipped 160 | skippedDomainsLock.Lock() 161 | if skippedDomainsLimitReached[host] { 162 | skippedDomainsLock.Unlock() 163 | continue 164 | } 165 | skippedDomainsLock.Unlock() 166 | 167 | for attempt := 0; attempt < *retries; attempt++ { 168 | req, err := http.NewRequest("GET", domain, nil) 169 | if err != nil { 170 | if *verbose { 171 | fmt.Println("Error creating request:", err) 172 | } 173 | continue 174 | } 175 | req.Header.Set("User-Agent", *userAgent) 176 | 177 | resp, err := client.Do(req) 178 | if err != nil { 179 | if *verbose { 180 | fmt.Println("Error making request:", err) 181 | } 182 | continue 183 | } 184 | defer resp.Body.Close() 185 | 186 | body, err := ioutil.ReadAll(resp.Body) 187 | if err != nil { 188 | if *verbose { 189 | fmt.Println("Error reading response body:", err) 190 | } 191 | continue 192 | } 193 | sb := string(body) 194 | 195 | isVulnerable := false 196 | for _, str := range matchStrings { 197 | if strings.Contains(sb, str) { 198 | isVulnerable = true 199 | break 200 | } 201 | } 202 | 203 | status := fmt.Sprintf("[%d] ", resp.StatusCode) 204 | server := resp.Header.Get("Server") 205 | outputStr := "" 206 | if isVulnerable { 207 | if *noColor { 208 | if *filter { 209 | outputStr = fmt.Sprintf("%s\n", domain) 210 | } else { 211 | outputStr = fmt.Sprintf("Vulnerable: %s[%s] %s\n", status, server, domain) 212 | } 213 | } else { 214 | if *filter { 215 | outputStr = fmt.Sprintf("\033[1;31m%s\033[0;0m\n", domain) 216 | } else { 217 | outputStr = fmt.Sprintf("\033[1;31mVulnerable: %s[%s] %s\033[0;0m\n", status, server, domain) 218 | } 219 | } 220 | } else if !*onlyVulnerable { // If onlyVulnerable is false, print non-vulnerable URLs 221 | if *noColor { 222 | if *filter { 223 | outputStr = fmt.Sprintf("%s\n", domain) 224 | } else { 225 | outputStr = fmt.Sprintf("Not Vulnerable: %s[%s] %s\n", status, server, domain) 226 | } 227 | } else { 228 | if *filter { 229 | outputStr = fmt.Sprintf("\033[1;35m%s\033[0;0m\n", domain) 230 | } else { 231 | outputStr = fmt.Sprintf("\033[1;35mNot Vulnerable: %s[%s] %s\033[0;0m\n", status, server, domain) 232 | } 233 | } 234 | } 235 | 236 | fmt.Print(outputStr) 237 | if output != nil { 238 | output.WriteString(outputStr) 239 | } 240 | 241 | // Check if the response meets the skip criteria 242 | if *skipStatusCodes != "" { 243 | for _, skipServerName := range skipServers { 244 | if skipStatusCodeList[resp.StatusCode] && strings.Contains(strings.ToLower(server), strings.ToLower(skipServerName)) { 245 | skippedDomainsLock.Lock() 246 | skippedDomains[host]++ 247 | if skippedDomains[host] >= *maxStatusCodeSkips { 248 | skippedDomainsLimitReached[host] = true 249 | if *verbose { 250 | fmt.Printf("Skipped all URLs of this domain %s [ERR: Blocked by %s]\n", host, *skipServer) 251 | } 252 | } 253 | skippedDomainsLock.Unlock() 254 | break // Exit retry loop on skip condition met 255 | } 256 | } 257 | } 258 | break // Exit retry loop on successful request 259 | } 260 | } 261 | }() 262 | } 263 | 264 | // Handle single URL input 265 | if *singleURL != "" { 266 | jobs <- *singleURL 267 | } else { 268 | // Handle multiple URLs from input 269 | for sc.Scan() { 270 | domain := sc.Text() 271 | jobs <- domain 272 | } 273 | } 274 | 275 | close(jobs) 276 | wg.Wait() 277 | } 278 | --------------------------------------------------------------------------------