// xss-scanner.go - 高性能 XSS 批量扫描工具 // // 授权边界: // - 仅用于自有资产、测试环境或已明确授权的目标 // - 允许公网验证,但必须确认资产归属或授权关系 // - 不面向无授权第三方网站或泛互联网枚举 package main import ( "encoding/json" "flag" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" "sync" "time" ) type XSSResult struct { URL string Payload string Type string Category string } type XSSScanner struct { Client *http.Client Threads int Timeout time.Duration Payloads map[string][]string Headers map[string]string Cookie string Quiet bool } var ( colorRed = "\033[91m" colorGreen = "\033[92m" colorYellow = "\033[93m" colorBlue = "\033[94m" colorCyan = "\033[96m" colorBold = "\033[1m" colorEnd = "\033[0m" ) func NewXSSScanner(threads int, timeout time.Duration) *XSSScanner { return &XSSScanner{ Client: &http.Client{ Timeout: timeout, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }, Threads: threads, Timeout: timeout, Headers: map[string]string{}, Payloads: map[string][]string{ "basic": { "", "", "", "", "", }, "event_handlers": { "\"onfocus=alert(1) autofocus=", "\"onmouseover=alert(1)//", "\"onclick=alert(1)//", "\"onerror=alert(1)//", "\"onload=alert(1)//", }, "tag_injection": { "", "
", "", }, "filter_bypass": { "", "", "", "alert(1)", }, "encoding": { "%3Cscript%3Ealert(1)%3C/script%3E", "<script>alert(1)</script>", }, }, } } func (s *XSSScanner) SendRequest(targetURL, method, param, payload string) (string, error) { var req *http.Request var err error if method == "GET" { u, _ := url.Parse(targetURL) q := u.Query() q.Set(param, payload) u.RawQuery = q.Encode() req, err = http.NewRequest("GET", u.String(), nil) } else { data := url.Values{} data.Set(param, payload) req, err = http.NewRequest("POST", targetURL, strings.NewReader(data.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } if err != nil { return "", err } req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") for k, v := range s.Headers { req.Header.Set(k, v) } if s.Cookie != "" { req.Header.Set("Cookie", s.Cookie) } resp, err := s.Client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) return string(body), nil } func (s *XSSScanner) ScanURL(targetURL, method, param string) []XSSResult { var results []XSSResult var mu sync.Mutex var wg sync.WaitGroup sem := make(chan struct{}, s.Threads) for category, payloads := range s.Payloads { for _, payload := range payloads { wg.Add(1) go func(cat, p string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() body, err := s.SendRequest(targetURL, method, param, p) if err != nil { return } if strings.Contains(body, p) || strings.Contains(body, url.QueryEscape(p)) { mu.Lock() results = append(results, XSSResult{ URL: targetURL, Payload: p, Type: "Reflected XSS", Category: cat, }) mu.Unlock() if !s.Quiet { fmt.Printf("%s[VULN]%s [%s] %s - %s\n", colorRed+colorBold, colorEnd, cat, param, p[:min(50, len(p))]) } } }(category, payload) } } wg.Wait() return results } func (s *XSSScanner) CheckCSP(targetURL string) map[string]interface{} { result := map[string]interface{}{ "has_csp": false, "csp": "", "weaknesses": []string{}, } resp, err := s.Client.Get(targetURL) if err != nil { return result } defer resp.Body.Close() csp := resp.Header.Get("Content-Security-Policy") if csp != "" { result["has_csp"] = true result["csp"] = csp weaknesses := []string{} if strings.Contains(csp, "unsafe-inline") { weaknesses = append(weaknesses, "允许内联脚本 (unsafe-inline)") } if strings.Contains(csp, "unsafe-eval") { weaknesses = append(weaknesses, "允许 eval (unsafe-eval)") } if strings.Contains(csp, "*") { weaknesses = append(weaknesses, "使用通配符 (*)") } result["weaknesses"] = weaknesses } return result } func (s *XSSScanner) ScanDOMXSS(targetURL string) []map[string]string { results := []map[string]string{} resp, err := s.Client.Get(targetURL) if err != nil { return results } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) html := string(body) patterns := map[string]string{ `document\.write\s*\([^)]*location`: "document.write with location", `element\.innerHTML\s*=\s*[^;]*location`: "innerHTML with location", `eval\s*\([^)]*location`: "eval with location", `window\.location\.hash`: "location.hash usage", } for pattern, desc := range patterns { matched, _ := regexp.MatchString(pattern, html) if matched { results = append(results, map[string]string{ "pattern": pattern, "desc": desc, }) } } return results } func min(a, b int) int { if a < b { return a } return b } func parseHeaders(raw string) map[string]string { headers := map[string]string{} if raw == "" { return headers } for _, part := range strings.Split(raw, ",") { pair := strings.SplitN(part, ":", 2) if len(pair) != 2 { continue } headers[strings.TrimSpace(pair[0])] = strings.TrimSpace(pair[1]) } return headers } func main() { target := flag.String("u", "", "Target URL") method := flag.String("m", "GET", "HTTP Method (GET/POST)") param := flag.String("p", "q", "Parameter to test") threads := flag.Int("t", 10, "Number of threads") timeout := flag.Duration("timeout", 10*time.Second, "Request timeout") checkCSP := flag.Bool("check-csp", false, "Check CSP headers") domScan := flag.Bool("dom-scan", false, "Scan for DOM XSS") header := flag.String("header", "", "Extra headers in Name:Value,Name2:Value2 format") cookie := flag.String("cookie", "", "Cookie header value") format := flag.String("format", "text", "Output format: text or json") output := flag.String("output", "", "Write output to file") evidenceDir := flag.String("evidence-dir", "", "Optional evidence directory") runID := flag.String("run-id", "", "Associated run ID") caseID := flag.String("case-id", "", "Associated case ID") ackAuthorized := flag.Bool("ack-authorized", false, "Confirm the target is owned or authorized") flag.Parse() if *target == "" || !*ackAuthorized { fmt.Printf("%s[ERROR]%s Target URL is required. Use -u flag.\n", colorRed, colorEnd) flag.Usage() return } scanner := NewXSSScanner(*threads, *timeout) scanner.Headers = parseHeaders(*header) scanner.Cookie = *cookie scanner.Quiet = *format != "text" cspResult := map[string]interface{}{"has_csp": false, "weaknesses": []string{}} if *checkCSP { cspResult = scanner.CheckCSP(*target) if *format == "text" && cspResult["has_csp"].(bool) { fmt.Printf("%s[+]%s CSP configured: %s\n", colorGreen, colorEnd, cspResult["csp"].(string)[:min(100, len(cspResult["csp"].(string)))]) for _, w := range cspResult["weaknesses"].([]string) { fmt.Printf("%s[-]%s Weakness: %s\n", colorYellow, colorEnd, w) } } else if *format == "text" { fmt.Printf("%s[-]%s No CSP configured!\n", colorYellow, colorEnd) } } domResults := []map[string]string{} if *domScan { domResults = scanner.ScanDOMXSS(*target) if *format == "text" { fmt.Printf("\n%s[*]%s Scanning for DOM XSS...\n", colorCyan, colorEnd) } for _, r := range domResults { if *format != "text" { continue } fmt.Printf("%s[-]%s Potential DOM XSS: %s\n", colorYellow, colorEnd, r["desc"]) } } results := scanner.ScanURL(*target, *method, *param) report := map[string]interface{}{ "tool": "xss-scanner-go", "mode": "bulk-reflected-xss", "target": *target, "status": "needs-review", "severity": "info", "timestamp": time.Now().UTC().Format(time.RFC3339), "request_summary": map[string]interface{}{"method": *method, "param": *param, "threads": *threads}, "payload_or_probe": map[string]interface{}{"reflected_hits": results, "dom_hits": domResults, "csp": cspResult}, "evidence_refs": []string{}, "minimal_validation": "只读探测、最小化注入、可审计回显、可回滚验证。", "authorization_scope": "lab-local, lab-public, authorized-third-party", "destructive_risk": "low", "run_id": *runID, "case_id": *caseID, } if len(results) > 0 { report["status"] = "verified" report["severity"] = "high" } else if len(domResults) > 0 { report["status"] = "suspected" report["severity"] = "medium" } if *evidenceDir != "" { _ = os.MkdirAll(*evidenceDir, 0o755) evidencePath := *evidenceDir + "/xss-scanner-go.json" if raw, err := json.MarshalIndent(report, "", " "); err == nil { _ = os.WriteFile(evidencePath, append(raw, '\n'), 0o644) report["evidence_refs"] = append(report["evidence_refs"].([]string), evidencePath) } } var content []byte if *format == "json" { content, _ = json.MarshalIndent(report, "", " ") } else { text := []string{ strings.Repeat("=", 60), "XSS Scanner (Go)", strings.Repeat("=", 60), "Target: " + *target, "Method: " + *method, fmt.Sprintf("Reflected Hits: %d", len(results)), fmt.Sprintf("DOM Findings: %d", len(domResults)), "Status: " + report["status"].(string), } content = []byte(strings.Join(text, "\n")) } if *output != "" { _ = os.WriteFile(*output, append(content, '\n'), 0o644) } fmt.Println(string(content)) }