This commit is contained in:
taygun08 2025-04-06 19:50:48 +03:00
parent 3e1ec45980
commit 95f7ea05ec
2 changed files with 245 additions and 18 deletions

View file

@ -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

View file

@ -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