diff --git a/README.md b/README.md index c9aca54..ac9a8c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# SPF/DMARC Spoofing Vulnerability Checker +# SPF/DMARC Spoofing & Subdomain Takeover Checker -A command-line tool written in Go that checks domains for email spoofing vulnerabilities by analyzing their SPF and DMARC DNS records. +A command-line tool written in Go that checks domains for email spoofing vulnerabilities and subdomain takeover risks. ## Features @@ -11,6 +11,7 @@ A command-line tool written in Go that checks domains for email spoofing vulnera - Overly permissive SPF policies (e.g., "+all" or "?all") - Insecure DMARC policy qualifiers - SPF record loops and excessive DNS lookups +- Checks for subdomain takeover vulnerabilities using known fingerprints - Multiple output formats (text and JSON) - Process individual domains or bulk check from a file - Interactive mode for checking domains one by one @@ -78,6 +79,12 @@ Create a text file with one domain per line. Lines starting with `#` will be tre ```bash ./spoof_check -input domains.txt -output results.txt ``` + +#### Bulk processing subdomain takeover check with json output + +```bash +./spoof_check -input domains.txt -subtakeover -json -output results.txt +``` ## Command-Line Options | Option | Description | @@ -87,6 +94,7 @@ Create a text file with one domain per line. Lines starting with `#` will be tre | `-output` | Save results to the specified file | | `-json` | Output results in JSON format | | `-interactive` | Run in interactive mode | +| `-subtakeover` | Check for subdomain takeover vulnerabilities | ## Issue Codes @@ -127,6 +135,10 @@ Code 8: Insecure DMARC policy 'p' qualifier Severity: High Detail: The DMARC policy 'p' qualifier is "none". If the DMARC policy is neither "reject" nor "quarantine", spoofed emails utilising an attack technique known as SPF-bypass are likely to be accepted. +--- Subdomain Takeover Check --- +Not vulnerable to subdomain takeover. +CNAME: example.com + --- End of report --- ``` @@ -151,8 +163,13 @@ Code 8: Insecure DMARC policy 'p' qualifier "severity": "High" } ], - "success": true -} + "success": true, + "subdomain_takeover": { + "domain": "example.com", + "vulnerable": false, + "cname_record": "example.com" + } +}, ``` ## Recommendations diff --git a/spoof_check.go b/spoof_check.go index a7b4bf2..fbcfefa 100644 --- a/spoof_check.go +++ b/spoof_check.go @@ -2,14 +2,16 @@ package main import ( "bufio" - "context" // New import + "context" "encoding/json" "flag" "fmt" + "io" "net" + "net/http" // New import "os" - "strings" // New import - "time" // New import + "strings" + "time" ) // SPFDMARCRecord contains both SPF and DMARC records for a domain @@ -49,12 +51,13 @@ type IssueEngine struct { // JSONScanResult represents the JSON structure for scan results type JSONScanResult struct { - Domain string `json:"domain"` - SPF string `json:"spf_record"` - DMARC string `json:"dmarc_record"` - Issues []JSONIssue `json:"issues,omitempty"` - Success bool `json:"success"` - Message string `json:"message,omitempty"` + Domain string `json:"domain"` + SPF string `json:"spf_record"` + DMARC string `json:"dmarc_record"` + Issues []JSONIssue `json:"issues,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + TakeoverResults *SubdomainTakeoverResult `json:"subdomain_takeover,omitempty"` // New field } // JSONIssue represents a single issue in JSON format @@ -65,6 +68,26 @@ type JSONIssue struct { Severity string `json:"severity"` } +// TakeoverFingerprint represents a fingerprint for subdomain takeover +type TakeoverFingerprint struct { + Service string `json:"service"` + Cname []string `json:"cname"` // Changed from string to []string + Fingerprint string `json:"fingerprint"` + Status string `json:"status"` + Vulnerable bool `json:"vulnerable"` +} + +// SubdomainTakeoverResult represents the result of a subdomain takeover check +type SubdomainTakeoverResult struct { + Domain string `json:"domain"` + Vulnerable bool `json:"vulnerable"` + Service string `json:"service,omitempty"` + Fingerprint string `json:"fingerprint,omitempty"` + ResponseBody string `json:"response_body,omitempty"` + CnameRecord string `json:"cname_record,omitempty"` + ErrorMessage string `json:"error,omitempty"` +} + // Function to check if a string is empty func isNullOrWhiteSpace(s string) bool { return len(strings.TrimSpace(s)) == 0 @@ -562,6 +585,111 @@ func (engine *IssueEngine) issueDescriptors(code int, domain string) IssueScanRe return issueResult } +// Download and parse the fingerprints JSON file +func getFingerprints() ([]TakeoverFingerprint, error) { + fingerprintsURL := "https://raw.githubusercontent.com/EdOverflow/can-i-take-over-xyz/master/fingerprints.json" + + // Make HTTP request + resp, err := http.Get(fingerprintsURL) + if err != nil { + return nil, fmt.Errorf("failed to download fingerprints: %v", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read fingerprints data: %v", err) + } + + // Parse JSON + var fingerprints []TakeoverFingerprint + if err := json.Unmarshal(body, &fingerprints); err != nil { + return nil, fmt.Errorf("failed to parse fingerprints JSON: %v", err) + } + + return fingerprints, nil +} + +// Check if a domain is vulnerable to subdomain takeover +func checkSubdomainTakeover(domain string) SubdomainTakeoverResult { + result := SubdomainTakeoverResult{ + Domain: domain, + Vulnerable: false, + } + + // Get CNAME record + cname, err := net.LookupCNAME(domain) + if err != nil { + result.ErrorMessage = fmt.Sprintf("Failed to lookup CNAME: %v", err) + return result + } + + // Normalize CNAME (remove trailing dot) + cname = strings.TrimSuffix(cname, ".") + result.CnameRecord = cname + + // Get fingerprints + fingerprints, err := getFingerprints() + if err != nil { + result.ErrorMessage = fmt.Sprintf("Failed to get fingerprints: %v", err) + return result + } + + // Check if CNAME matches any known fingerprint + for _, fp := range fingerprints { + // Check against each CNAME pattern in the array + for _, cnamePattern := range fp.Cname { + if strings.Contains(cname, cnamePattern) { + // We found a matching CNAME pattern, now check HTTP response + if fp.Fingerprint != "" { + // Make HTTP request + resp, err := http.Get("http://" + domain) + if err != nil { + // Try HTTPS if HTTP fails + resp, err = http.Get("https://" + domain) + if err != nil { + continue // Skip this fingerprint if we can't connect + } + } + + // Read response body + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + continue // Skip if we can't read the body + } + + // Check if response contains the fingerprint + responseText := string(body) + if strings.Contains(responseText, fp.Fingerprint) && fp.Vulnerable { + result.Vulnerable = true + result.Service = fp.Service + result.Fingerprint = fp.Fingerprint + // Store a snippet of the response body (first 200 chars) + if len(responseText) > 200 { + result.ResponseBody = responseText[:200] + "..." + } else { + result.ResponseBody = responseText + } + return result + } + } else if fp.Vulnerable { + // If there's no fingerprint but the service is vulnerable based on CNAME alone + result.Vulnerable = true + result.Service = fp.Service + return result + } + + // If we've checked this CNAME pattern, we can break out of the inner loop + break + } + } + } + + return result +} + func main() { // Parse command line arguments var domain string @@ -569,17 +697,19 @@ func main() { var jsonOutput bool var outputFile string var inputFile string + var checkSubTakeover bool // New flag flag.StringVar(&domain, "domain", "", "Domain to check for spoofing vulnerabilities") flag.BoolVar(&interactive, "interactive", false, "Run in interactive mode") flag.BoolVar(&jsonOutput, "json", false, "Output results in JSON format") flag.StringVar(&outputFile, "output", "", "Save results to specified file") flag.StringVar(&inputFile, "input", "", "Read domains from specified file (one domain per line)") + flag.BoolVar(&checkSubTakeover, "subtakeover", false, "Check for subdomain takeover vulnerabilities") flag.Parse() // Process domains from input file if specified if inputFile != "" { - processDomainFile(inputFile, jsonOutput, outputFile) + processDomainFile(inputFile, jsonOutput, outputFile, checkSubTakeover) } else if interactive { for { fmt.Print("Enter domain to check (or 'exit' to quit): ") @@ -591,10 +721,10 @@ func main() { break } - checkDomain(domain, jsonOutput, outputFile) + checkDomain(domain, jsonOutput, outputFile, checkSubTakeover) } } else if domain != "" { - checkDomain(domain, jsonOutput, outputFile) + checkDomain(domain, jsonOutput, outputFile, checkSubTakeover) } else { fmt.Println("Please specify a domain with -domain, use -input to read from a file, or use -interactive") flag.Usage() @@ -602,7 +732,7 @@ func main() { } // Process a file containing a list of domains -func processDomainFile(inputFile string, jsonOutput bool, outputFile string) { +func processDomainFile(inputFile string, jsonOutput bool, outputFile string, checkSubTakeover bool) { // Read the file file, err := os.Open(inputFile) if err != nil { @@ -651,11 +781,24 @@ func processDomainFile(inputFile string, jsonOutput bool, outputFile string) { engine := &IssueEngine{} issues := engine.IssueScan(spfDmarcRecord, parsedSPF) + // Check for subdomain takeover if flag is enabled + var takeoverResult *SubdomainTakeoverResult + if checkSubTakeover { + result := checkSubdomainTakeover(domain) + takeoverResult = &result + } + // Format the output var result string if jsonOutput { // Convert to JSON jsonResult := issuestoJSON(domain, spfDmarcRecord, issues) + + // Add takeover result if available + if takeoverResult != nil { + jsonResult.TakeoverResults = takeoverResult + } + jsonData, err := json.MarshalIndent(jsonResult, "", " ") if err != nil { result = fmt.Sprintf("{\"success\": false, \"message\": \"Error generating JSON: %s\"}\n", err) @@ -691,6 +834,33 @@ func processDomainFile(inputFile string, jsonOutput bool, outputFile string) { builder.WriteString(fmt.Sprintf(" Detail: %s\n", issue.detail)) } } + + // Add subdomain takeover results if enabled + if takeoverResult != nil { + builder.WriteString("\n--- Subdomain Takeover Check ---\n") + if takeoverResult.Vulnerable { + builder.WriteString(fmt.Sprintf("VULNERABLE to subdomain takeover!\n")) + builder.WriteString(fmt.Sprintf("Service: %s\n", takeoverResult.Service)) + if takeoverResult.CnameRecord != "" { + builder.WriteString(fmt.Sprintf("CNAME: %s\n", takeoverResult.CnameRecord)) + } + if takeoverResult.Fingerprint != "" { + builder.WriteString(fmt.Sprintf("Matching Fingerprint: %s\n", takeoverResult.Fingerprint)) + } + if takeoverResult.ResponseBody != "" { + builder.WriteString(fmt.Sprintf("Response snippet: %s\n", takeoverResult.ResponseBody)) + } + } else { + builder.WriteString("Not vulnerable to subdomain takeover.\n") + if takeoverResult.ErrorMessage != "" { + builder.WriteString(fmt.Sprintf("Note: %s\n", takeoverResult.ErrorMessage)) + } + if takeoverResult.CnameRecord != "" { + builder.WriteString(fmt.Sprintf("CNAME: %s\n", takeoverResult.CnameRecord)) + } + } + } + builder.WriteString("\n--- End of report ---\n") result = builder.String() @@ -749,7 +919,7 @@ func issuestoJSON(domain string, spfDmarcRecord SPFDMARCRecord, issues []IssueSc return result } -func checkDomain(domain string, jsonOutput bool, outputFile string) { +func checkDomain(domain string, jsonOutput bool, outputFile string, checkSubTakeover bool) { // Get SPF and DMARC records spfDmarcRecord := getSPFDMARCRecord(domain) @@ -760,12 +930,25 @@ func checkDomain(domain string, jsonOutput bool, outputFile string) { engine := &IssueEngine{} issues := engine.IssueScan(spfDmarcRecord, parsedSPF) + // Check for subdomain takeover if flag is enabled + var takeoverResult *SubdomainTakeoverResult + if checkSubTakeover { + result := checkSubdomainTakeover(domain) + takeoverResult = &result + } + var output string var fileOutput string if jsonOutput { // Format as JSON jsonResult := issuestoJSON(domain, spfDmarcRecord, issues) + + // Add takeover result if available + if takeoverResult != nil { + jsonResult.TakeoverResults = takeoverResult + } + jsonData, err := json.MarshalIndent(jsonResult, "", " ") if err != nil { output = fmt.Sprintf("{\"success\": false, \"message\": \"Error generating JSON: %s\"}\n", err) @@ -797,6 +980,33 @@ func checkDomain(domain string, jsonOutput bool, outputFile string) { builder.WriteString(fmt.Sprintf(" Detail: %s\n", issue.detail)) } } + + // Add subdomain takeover results if enabled + if takeoverResult != nil { + builder.WriteString("\n--- Subdomain Takeover Check ---\n") + if takeoverResult.Vulnerable { + builder.WriteString(fmt.Sprintf("VULNERABLE to subdomain takeover!\n")) + builder.WriteString(fmt.Sprintf("Service: %s\n", takeoverResult.Service)) + if takeoverResult.CnameRecord != "" { + builder.WriteString(fmt.Sprintf("CNAME: %s\n", takeoverResult.CnameRecord)) + } + if takeoverResult.Fingerprint != "" { + builder.WriteString(fmt.Sprintf("Matching Fingerprint: %s\n", takeoverResult.Fingerprint)) + } + if takeoverResult.ResponseBody != "" { + builder.WriteString(fmt.Sprintf("Response snippet: %s\n", takeoverResult.ResponseBody)) + } + } else { + builder.WriteString("Not vulnerable to subdomain takeover.\n") + if takeoverResult.ErrorMessage != "" { + builder.WriteString(fmt.Sprintf("Note: %s\n", takeoverResult.ErrorMessage)) + } + if takeoverResult.CnameRecord != "" { + builder.WriteString(fmt.Sprintf("CNAME: %s\n", takeoverResult.CnameRecord)) + } + } + } + builder.WriteString("\n--- End of report ---\n") output = builder.String() fileOutput = output