mirror of
https://github.com/taygun08/spoof_checker.git
synced 2026-08-27 03:41:53 +00:00
add subtakeover parameter https://redsift.com/guides/subdomailing-guide
This commit is contained in:
parent
3e1ec45980
commit
95f7ea05ec
2 changed files with 245 additions and 18 deletions
25
README.md
25
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
|
||||
|
|
|
|||
238
spoof_check.go
238
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue