debug mode added + spf macros added

This commit is contained in:
taygun08 2025-04-07 10:50:27 +03:00
parent 6c8323ff02
commit 2f7b1818d0

View file

@ -10,10 +10,21 @@ import (
"net"
"net/http" // New import
"os"
"regexp"
"strings"
"time"
)
// Global debug flag
var debugMode bool
// Debug logs a message if debug mode is enabled
func Debug(format string, args ...interface{}) {
if debugMode {
fmt.Printf("[DEBUG] "+format+"\n", args...)
}
}
// SPFDMARCRecord contains both SPF and DMARC records for a domain
type SPFDMARCRecord struct {
domainName string
@ -161,8 +172,115 @@ func getSPFDMARCRecord(domain string) SPFDMARCRecord {
return record
}
// Parse SPF record to extract all components
func parseSPFRecord(spfRecord string, domainName string) *ParsedSPFRecord {
// Reverse an IP address (e.g., "192.168.1.1" -> "1.1.168.192")
func reverseIP(ip string) string {
parts := strings.Split(ip, ".")
for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
parts[i], parts[j] = parts[j], parts[i]
}
return strings.Join(parts, ".")
}
// Expand SPF macros as per RFC 7208
func expandSPFMacro(input string, domain string, sender string, ip string) string {
// Compute reverse IP if available
reverseIPValue := ""
if ip != "" {
reverseIPValue = reverseIP(ip)
}
// Split sender into local and domain parts
localPart := ""
domainPart := ""
if strings.Contains(sender, "@") {
parts := strings.SplitN(sender, "@", 2)
localPart = parts[0]
domainPart = parts[1]
}
// Define macro replacements
replacements := map[string]string{
"s": sender,
"l": localPart,
"o": domainPart,
"d": domain,
"i": ip,
"ir": reverseIPValue, // Reverse IP
"v": "in-addr", // IPv4
"p": "unknown", // Placeholder for policy domain
"c": ip, // Client IP
"r": "unknown", // Placeholder for receiver
"t": fmt.Sprintf("%d", time.Now().Unix()),
}
// Regex to match macros
macroRegex := regexp.MustCompile(`%{([slodipvcrt]|ir)(\d+)?r?}`)
// Replace macros
return macroRegex.ReplaceAllStringFunc(input, func(macro string) string {
matches := macroRegex.FindStringSubmatch(macro)
if len(matches) < 2 {
return macro // Return as-is if no match
}
key := matches[1]
if value, exists := replacements[key]; exists {
return value
}
return macro // Return as-is if no replacement found
})
}
// Improved resolveIPFromSPF to try additional methods for IP resolution
func resolveIPFromSPF(spfRecord string, domain string) string {
// Try the existing mechanisms first
mechanisms := strings.Split(spfRecord, " ")
for _, mechanism := range mechanisms {
if strings.HasPrefix(mechanism, "a") {
ips, err := net.LookupIP(domain)
if err == nil && len(ips) > 0 {
for _, ip := range ips {
// Prefer IPv4 addresses
if ip.To4() != nil {
Debug("Resolved IP from A record: %s", ip.String())
return ip.String()
}
}
// If no IPv4, use whatever we have
Debug("Resolved IP from A record: %s", ips[0].String())
return ips[0].String()
}
} else if strings.HasPrefix(mechanism, "mx") {
// ... existing mx handler code ...
} else if strings.HasPrefix(mechanism, "exists:") {
// ... existing exists handler code ...
}
}
// If no IP was found from mechanisms, try a direct lookup as fallback
ips, err := net.LookupIP(domain)
if err == nil && len(ips) > 0 {
for _, ip := range ips {
if ip.To4() != nil {
Debug("Resolved IP via direct lookup: %s", ip.String())
return ip.String()
}
}
Debug("Resolved IP via direct lookup: %s", ips[0].String())
return ips[0].String()
}
// If all else fails, return a dummy IP for debugging
Debug("Could not resolve any IP for domain %s", domain)
return "127.0.0.1" // Return a dummy IP to prevent empty macros
}
// Parse SPF record to extract all components, including macro expansion
func parseSPFRecord(spfRecord string, domainName string, sender string, ip string) *ParsedSPFRecord {
// Dynamically resolve IP if not provided
if ip == "" {
ip = resolveIPFromSPF(spfRecord, domainName)
}
parsedRecord := &ParsedSPFRecord{
aRecord: make([]string, 0),
includeRecords: make([]ParsedIncludeRecord, 0),
@ -179,23 +297,20 @@ func parseSPFRecord(spfRecord string, domainName string) *ParsedSPFRecord {
visitedDomains := make(map[string]bool)
visitedDomains[domainName] = true
return parseSPFRecordWithDepth(spfRecord, domainName, visitedDomains, 0)
Debug("Starting SPF parsing for domain: %s", domainName)
return parseSPFRecordWithDepth(spfRecord, domainName, sender, ip, visitedDomains, 0)
}
// Helper function with depth tracking to prevent stack overflow
func parseSPFRecordWithDepth(spfRecord string, domainName string, visitedDomains map[string]bool, depth int) *ParsedSPFRecord {
func parseSPFRecordWithDepth(spfRecord string, domainName string, sender string, ip string, visitedDomains map[string]bool, depth int) *ParsedSPFRecord {
// Limit recursion depth
maxDepth := 10
if depth > maxDepth {
return &ParsedSPFRecord{
aRecord: make([]string, 0),
includeRecords: make([]ParsedIncludeRecord, 0),
mxRecord: make([]string, 0),
existsRecords: make([]string, 0),
ptrRecords: make([]string, 0),
}
Debug("Max recursion depth reached for domain: %s", domainName)
return &ParsedSPFRecord{}
}
Debug("Parsing SPF record for domain: %s, depth: %d", domainName, depth)
parsedRecord := &ParsedSPFRecord{
aRecord: make([]string, 0),
includeRecords: make([]ParsedIncludeRecord, 0),
@ -204,62 +319,60 @@ func parseSPFRecordWithDepth(spfRecord string, domainName string, visitedDomains
ptrRecords: make([]string, 0),
}
if isNullOrWhiteSpace(spfRecord) {
return parsedRecord
}
mechanisms := strings.Split(spfRecord, " ")
for _, mechanism := range mechanisms {
if strings.HasPrefix(mechanism, "a:") || mechanism == "a" {
parsedRecord.aRecord = append(parsedRecord.aRecord, mechanism)
} else if strings.HasPrefix(mechanism, "include:") {
includeDomain := strings.TrimPrefix(mechanism, "include:")
includeRecord := ParsedIncludeRecord{
includeRecord: includeDomain,
expandedMechanism := expandSPFMacro(mechanism, domainName, sender, ip)
Debug("Expanded mechanism: %s", expandedMechanism)
if strings.HasPrefix(expandedMechanism, "a:") || expandedMechanism == "a" {
parsedRecord.aRecord = append(parsedRecord.aRecord, expandedMechanism)
} else if strings.HasPrefix(expandedMechanism, "include:") {
includeDomain := strings.TrimPrefix(expandedMechanism, "include:")
Debug("Processing include: %s", includeDomain)
if visitedDomains[includeDomain] {
Debug("Loop detected for include: %s", includeDomain)
continue
}
// Skip already visited domains to prevent loops
if !visitedDomains[includeDomain] {
visitedDomains[includeDomain] = true
visitedDomains[includeDomain] = true
includeSPF := getSPFDMARCRecord(includeDomain)
if !isNullOrWhiteSpace(includeSPF.spfRecord) {
subParsed := parseSPFRecordWithDepth(includeSPF.spfRecord, includeDomain, sender, ip, visitedDomains, depth+1)
parsedRecord.includeRecords = append(parsedRecord.includeRecords, ParsedIncludeRecord{
includeRecord: includeDomain,
subLookup: subParsed,
})
}
} else if strings.HasPrefix(expandedMechanism, "mx:") || expandedMechanism == "mx" {
parsedRecord.mxRecord = append(parsedRecord.mxRecord, expandedMechanism)
} else if strings.HasPrefix(expandedMechanism, "exists:") {
parsedRecord.existsRecords = append(parsedRecord.existsRecords, expandedMechanism)
} else if strings.HasPrefix(expandedMechanism, "ptr:") || expandedMechanism == "ptr" {
parsedRecord.ptrRecords = append(parsedRecord.ptrRecords, expandedMechanism)
} else if strings.HasPrefix(expandedMechanism, "redirect=") {
redirectDomain := strings.TrimPrefix(expandedMechanism, "redirect=")
Debug("Processing redirect to: %s", redirectDomain)
// Get the SPF record for the included domain
includedSPF := getSPFDMARCRecord(includeDomain)
if !isNullOrWhiteSpace(includedSPF.spfRecord) {
includeRecord.subLookup = parseSPFRecordWithDepth(includedSPF.spfRecord, includeDomain, visitedDomains, depth+1)
}
if visitedDomains[redirectDomain] {
Debug("Loop detected for redirect: %s", redirectDomain)
continue
}
parsedRecord.includeRecords = append(parsedRecord.includeRecords, includeRecord)
} else if strings.HasPrefix(mechanism, "mx:") || mechanism == "mx" {
parsedRecord.mxRecord = append(parsedRecord.mxRecord, mechanism)
} else if strings.HasPrefix(mechanism, "exists:") {
parsedRecord.existsRecords = append(parsedRecord.existsRecords, mechanism)
} else if strings.HasPrefix(mechanism, "ptr:") || mechanism == "ptr" {
parsedRecord.ptrRecords = append(parsedRecord.ptrRecords, mechanism)
}
// Check for redirect
if strings.HasPrefix(mechanism, "redirect=") {
redirectDomain := strings.TrimPrefix(mechanism, "redirect=")
// Skip already visited domains to prevent loops
if !visitedDomains[redirectDomain] {
visitedDomains[redirectDomain] = true
redirectSPF := getSPFDMARCRecord(redirectDomain)
if !isNullOrWhiteSpace(redirectSPF.spfRecord) {
// Parse the redirected SPF record
redirectParsed := parseSPFRecordWithDepth(redirectSPF.spfRecord, redirectDomain, visitedDomains, depth+1)
// Merge the parsed records
parsedRecord.aRecord = append(parsedRecord.aRecord, redirectParsed.aRecord...)
parsedRecord.includeRecords = append(parsedRecord.includeRecords, redirectParsed.includeRecords...)
parsedRecord.mxRecord = append(parsedRecord.mxRecord, redirectParsed.mxRecord...)
parsedRecord.existsRecords = append(parsedRecord.existsRecords, redirectParsed.existsRecords...)
parsedRecord.ptrRecords = append(parsedRecord.ptrRecords, redirectParsed.ptrRecords...)
}
visitedDomains[redirectDomain] = true
redirectSPF := getSPFDMARCRecord(redirectDomain)
if !isNullOrWhiteSpace(redirectSPF.spfRecord) {
redirectParsed := parseSPFRecordWithDepth(redirectSPF.spfRecord, redirectDomain, sender, ip, visitedDomains, depth+1)
parsedRecord.aRecord = append(parsedRecord.aRecord, redirectParsed.aRecord...)
parsedRecord.includeRecords = append(parsedRecord.includeRecords, redirectParsed.includeRecords...)
parsedRecord.mxRecord = append(parsedRecord.mxRecord, redirectParsed.mxRecord...)
parsedRecord.existsRecords = append(parsedRecord.existsRecords, redirectParsed.existsRecords...)
parsedRecord.ptrRecords = append(parsedRecord.ptrRecords, redirectParsed.ptrRecords...)
}
}
}
Debug("Finished parsing SPF record for domain: %s, depth: %d", domainName, depth)
return parsedRecord
}
@ -697,8 +810,8 @@ func main() {
var jsonOutput bool
var outputFile string
var inputFile string
var checkSubTakeover bool // New flag
var checkSubTakeover bool // New flag
flag.BoolVar(&debugMode, "debug", false, "Enable debug mode") // New debug 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")
@ -707,6 +820,10 @@ func main() {
flag.BoolVar(&checkSubTakeover, "subtakeover", false, "Check for subdomain takeover vulnerabilities")
flag.Parse()
Debug("Debug mode enabled")
Debug("Parsed flags: domain=%s, interactive=%v, jsonOutput=%v, outputFile=%s, inputFile=%s, checkSubTakeover=%v",
domain, interactive, jsonOutput, outputFile, inputFile, checkSubTakeover)
// Process domains from input file if specified
if inputFile != "" {
processDomainFile(inputFile, jsonOutput, outputFile, checkSubTakeover)
@ -733,6 +850,8 @@ func main() {
// Process a file containing a list of domains
func processDomainFile(inputFile string, jsonOutput bool, outputFile string, checkSubTakeover bool) {
Debug("Processing input file: %s", inputFile)
// Read the file
file, err := os.Open(inputFile)
if err != nil {
@ -768,109 +887,10 @@ func processDomainFile(inputFile string, jsonOutput bool, outputFile string, che
continue // Skip empty lines and comments
}
Debug("Processing domain: %s", domain)
// Process the domain
fmt.Printf("Processing domain: %s\n", domain)
// Get SPF and DMARC records
spfDmarcRecord := getSPFDMARCRecord(domain)
// Parse SPF record
parsedSPF := parseSPFRecord(spfDmarcRecord.spfRecord, domain)
// Scan for issues
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)
} else {
result = string(jsonData)
}
// For file output, add to all results
if outputFile != "" {
// Write to the file with appropriate comma
if domainCount > 0 {
outFile.WriteString(",\n")
}
outFile.WriteString(result)
} else {
// Print to console
fmt.Println(result)
}
} else {
// Format as text
var builder strings.Builder
builder.WriteString(fmt.Sprintf("\n--- Checking domain: %s ---\n", domain))
builder.WriteString(fmt.Sprintf("SPF Record: %s\n", spfDmarcRecord.spfRecord))
builder.WriteString(fmt.Sprintf("DMARC Record: %s\n\n", spfDmarcRecord.dmarcRecord))
if len(issues) == 0 {
builder.WriteString("No issues found. The domain is well protected against spoofing.\n")
} else {
builder.WriteString("Found the following issues:\n")
for _, issue := range issues {
builder.WriteString(fmt.Sprintf("\nCode %d: %s\n", issue.code, issue.title))
builder.WriteString(fmt.Sprintf(" Severity: %s\n", issue.severity))
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()
// If output file specified, write to it; otherwise print to console
if outputFile != "" {
outFile.WriteString(result)
} else {
fmt.Print(result)
}
}
checkDomain(domain, jsonOutput, outputFile, checkSubTakeover)
domainCount++
}
@ -885,6 +905,7 @@ func processDomainFile(inputFile string, jsonOutput bool, outputFile string, che
outFile.WriteString("\n]")
}
Debug("Processed %d domains from %s", domainCount, inputFile)
fmt.Printf("Processed %d domains from %s\n", domainCount, inputFile)
if outputFile != "" {
fmt.Printf("Results saved to %s\n", outputFile)
@ -919,22 +940,70 @@ func issuestoJSON(domain string, spfDmarcRecord SPFDMARCRecord, issues []IssueSc
return result
}
// Modified function to also display SPF record content for each domain
func DebugSPFRecord(prefix string, record *ParsedSPFRecord, domainName string, spfText string) {
if record == nil {
Debug("%sNil SPF Record", prefix)
return
}
// Display the domain name and its SPF record at the top level
if domainName != "" && spfText != "" {
Debug("%sDomain: %s", prefix, domainName)
Debug("%sSPF Record: %s", prefix, spfText)
}
Debug("%sA Records: %v", prefix, record.aRecord)
Debug("%sMX Records: %v", prefix, record.mxRecord)
Debug("%sExists Records: %v", prefix, record.existsRecords)
Debug("%sPTR Records: %v", prefix, record.ptrRecords)
if len(record.includeRecords) > 0 {
Debug("%sIncludes (%d):", prefix, len(record.includeRecords))
for i, include := range record.includeRecords {
Debug("%s [%d] Domain: %s", prefix, i, include.includeRecord)
if include.subLookup != nil {
Debug("%s [%d] SubLookup:", prefix, i)
// Get the SPF record for this included domain
includeSPF := getSPFDMARCRecord(include.includeRecord)
DebugSPFRecord(prefix+" ", include.subLookup, include.includeRecord, includeSPF.spfRecord)
} else {
Debug("%s [%d] SubLookup: nil", prefix, i)
}
}
} else {
Debug("%sNo includes", prefix)
}
}
// Update the checkDomain function to use the enhanced debug function
func checkDomain(domain string, jsonOutput bool, outputFile string, checkSubTakeover bool) {
Debug("Checking domain: %s", domain)
// Get SPF and DMARC records
spfDmarcRecord := getSPFDMARCRecord(domain)
Debug("Retrieved SPF record: %s", spfDmarcRecord.spfRecord)
Debug("Retrieved DMARC record: %s", spfDmarcRecord.dmarcRecord)
// Parse SPF record
parsedSPF := parseSPFRecord(spfDmarcRecord.spfRecord, domain)
parsedSPF := parseSPFRecord(spfDmarcRecord.spfRecord, domain, "", "")
// Use our enhanced debug function to print the parsed SPF record
Debug("Parsed SPF record structure:")
DebugSPFRecord(" ", parsedSPF, domain, spfDmarcRecord.spfRecord)
// Scan for issues
engine := &IssueEngine{}
issues := engine.IssueScan(spfDmarcRecord, parsedSPF)
Debug("Identified issues: %+v", issues)
// Check for subdomain takeover if flag is enabled
var takeoverResult *SubdomainTakeoverResult
if checkSubTakeover {
result := checkSubdomainTakeover(domain)
takeoverResult = &result
Debug("Subdomain takeover result: %+v", takeoverResult)
}
var output string