Add files via upload

This commit is contained in:
taygun08 2025-04-05 20:08:19 +03:00 • committed by GitHub
parent b4f50c34fd
commit cfe997f221
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 997 additions and 0 deletions

164
README.md Normal file
View file

@ -0,0 +1,164 @@
# SPF/DMARC Spoofing Vulnerability Checker
A command-line tool written in Go that checks domains for email spoofing vulnerabilities by analyzing their SPF and DMARC DNS records.
## Features
- Analyzes SPF (Sender Policy Framework) records for common misconfigurations
- Evaluates DMARC (Domain-based Message Authentication, Reporting & Conformance) policies
- Detects potential spoofing vulnerabilities like:
- Missing or improperly configured SPF/DMARC records
- Overly permissive SPF policies (e.g., "+all" or "?all")
- Insecure DMARC policy qualifiers
- SPF record loops and excessive DNS lookups
- Multiple output formats (text and JSON)
- Process individual domains or bulk check from a file
- Interactive mode for checking domains one by one
- Save results to an output file
- Concurrent processing for faster bulk scanning
## Installation
Assuming you have Go installed, you can build the program with:
```bash
go build spoof_check.go
```
## Usage
### Check a single domain
```bash
./spoof_check -domain example.com
```
### Process multiple domains from a file
Create a text file with one domain per line. Lines starting with `#` will be treated as comments and skipped.
```bash
./spoof_check -input domains.txt
```
### Interactive mode
```bash
./spoof_check -interactive
```
### Output options
#### JSON output
```bash
./spoof_check -domain example.com -json
```
#### Save results to a file
```bash
./spoof_check -domain example.com -output results.txt
```
#### JSON output saved to a file
```bash
./spoof_check -domain example.com -json -output results.json
```
#### Bulk processing with JSON output
```bash
./spoof_check -input domains.txt -json -output results.json
```
## Command-Line Options
| Option | Description |
| ------ | ----------- |
| `-domain` | Specify a single domain to check |
| `-input` | Specify a file containing a list of domains to check |
| `-output` | Save results to the specified file |
| `-json` | Output results in JSON format |
| `-interactive` | Run in interactive mode |
## Issue Codes
The tool identifies several types of issues with the following codes:
| Code | Title | Severity |
| ---- | ----- | -------- |
| 0 | Non-existent domain | Low |
| 1 | No SPF record exists | High |
| 2 | SPF "all" mechanism is missing or set to "?all" | High |
| 3 | SPF "+all" mechanism set | Very High |
| 4 | SPF "~all" (SoftFail) mechanism set | Medium |
| 5 | SPF has too many lookups for receiver validation | Medium |
| 6 | No SPF sub-domain record exists | Medium |
| 7 | No DMARC record exists | High |
| 8 | Insecure DMARC policy 'p' qualifier | High |
| 9 | Insecure DMARC sub-domain 'p' qualifier | High |
| 10 | Partial DMARC coverage | Medium |
| 11 | DNS Timeout during Scan | Low |
| 12 | Trivial SPF recurse loop | Medium |
## Example Output
### Text format:
```
--- Checking domain: example.com ---
SPF Record: v=spf1 include:_spf.example.com ~all
DMARC Record: v=DMARC1; p=none; rua=mailto:dmarc@example.com
Found the following issues:
Code 4: SPF "~all" (SoftFail) mechanism set
Severity: Medium
Detail: The "all" mechanism at the end of the end of an SPF record tells receivers how to treat unauthorised (i.e. spoofed) emails - the "~all" setting tells receivers to 'SoftFail' (i.e. quarantine) emails that fail SPF authentication. In practice however, many email filters only slightly raise the total spam score and accept 'SoftFailed' (i.e. spoofed) emails.
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.
--- End of report ---
```
### JSON format:
```json
{
"domain": "example.com",
"spf_record": "v=spf1 include:_spf.example.com ~all",
"dmarc_record": "v=DMARC1; p=none; rua=mailto:dmarc@example.com",
"issues": [
{
"code": 4,
"title": "SPF \"~all\" (SoftFail) mechanism set",
"detail": "The \"all\" mechanism at the end of the end of an SPF record tells receivers how to treat unauthorised (i.e. spoofed) emails - the \"~all\" setting tells receivers to 'SoftFail' (i.e. quarantine) emails that fail SPF authentication. In practice however, many email filters only slightly raise the total spam score and accept 'SoftFailed' (i.e. spoofed) emails.",
"severity": "Medium"
},
{
"code": 8,
"title": "Insecure DMARC policy 'p' qualifier",
"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.",
"severity": "High"
}
],
"success": true
}
```
## Recommendations
Based on the issues detected, you should consider:
1. Setting SPF records to end with `-all` (hard fail) instead of `~all` or `?all`
2. Ensuring DMARC policy 'p' qualifier is set to 'quarantine' or 'reject'
3. Configuring wildcard SPF records for sub-domains
4. Setting DMARC percentage to 100% for full coverage
## License
This tool is provided as-is without any warranty. Use at your own risk.

833
spoof_check.go Normal file
View file

@ -0,0 +1,833 @@
package main
import (
"bufio"
"context" // New import
"encoding/json"
"flag"
"fmt"
"net"
"os"
"strings" // New import
"time" // New import
)
// SPFDMARCRecord contains both SPF and DMARC records for a domain
type SPFDMARCRecord struct {
domainName string
spfRecord string
dmarcRecord string
}
// ParsedSPFRecord contains the parsed elements of an SPF record
type ParsedSPFRecord struct {
aRecord []string
includeRecords []ParsedIncludeRecord
mxRecord []string
existsRecords []string
ptrRecords []string
}
// ParsedIncludeRecord represents an include statement in SPF
type ParsedIncludeRecord struct {
includeRecord string
subLookup *ParsedSPFRecord
}
// IssueScanResult represents a detected issue during scanning
type IssueScanResult struct {
code int
title string
detail string
severity string
}
// IssueEngine handles the scanning logic for SPF and DMARC records
type IssueEngine struct {
rolledIncludeRecords []string
}
// 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"`
}
// JSONIssue represents a single issue in JSON format
type JSONIssue struct {
Code int `json:"code"`
Title string `json:"title"`
Detail string `json:"detail"`
Severity string `json:"severity"`
}
// Function to check if a string is empty
func isNullOrWhiteSpace(s string) bool {
return len(strings.TrimSpace(s)) == 0
}
// Get SPF and DMARC records for a domain
func getSPFDMARCRecord(domain string) SPFDMARCRecord {
var record SPFDMARCRecord
record.domainName = domain
// Setup timeout context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Use DNS resolver with custom timeout
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 3 * time.Second,
}
return d.DialContext(ctx, network, address)
},
}
// Get SPF record with timeout
spfChan := make(chan string, 1)
go func() {
txtRecords, err := resolver.LookupTXT(ctx, domain)
if err == nil {
for _, txt := range txtRecords {
if strings.HasPrefix(txt, "v=spf1") {
spfChan <- txt
return
}
}
}
spfChan <- ""
}()
// Get DMARC record with timeout
dmarcChan := make(chan string, 1)
go func() {
txtRecords, err := resolver.LookupTXT(ctx, "_dmarc."+domain)
if err == nil {
for _, txt := range txtRecords {
if strings.HasPrefix(txt, "v=DMARC1") {
dmarcChan <- txt
return
}
}
}
dmarcChan <- ""
}()
// Wait for results with timeout
select {
case record.spfRecord = <-spfChan:
case <-ctx.Done():
// Timeout occurred
record.spfRecord = ""
}
select {
case record.dmarcRecord = <-dmarcChan:
case <-ctx.Done():
// Timeout occurred
record.dmarcRecord = ""
}
return record
}
// Parse SPF record to extract all components
func parseSPFRecord(spfRecord string, domainName string) *ParsedSPFRecord {
parsedRecord := &ParsedSPFRecord{
aRecord: make([]string, 0),
includeRecords: make([]ParsedIncludeRecord, 0),
mxRecord: make([]string, 0),
existsRecords: make([]string, 0),
ptrRecords: make([]string, 0),
}
if isNullOrWhiteSpace(spfRecord) {
return parsedRecord
}
// Track domains to prevent recursion loops
visitedDomains := make(map[string]bool)
visitedDomains[domainName] = true
return parseSPFRecordWithDepth(spfRecord, domainName, visitedDomains, 0)
}
// Helper function with depth tracking to prevent stack overflow
func parseSPFRecordWithDepth(spfRecord string, domainName 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),
}
}
parsedRecord := &ParsedSPFRecord{
aRecord: make([]string, 0),
includeRecords: make([]ParsedIncludeRecord, 0),
mxRecord: make([]string, 0),
existsRecords: make([]string, 0),
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,
}
// Skip already visited domains to prevent loops
if !visitedDomains[includeDomain] {
visitedDomains[includeDomain] = true
// Get the SPF record for the included domain
includedSPF := getSPFDMARCRecord(includeDomain)
if !isNullOrWhiteSpace(includedSPF.spfRecord) {
includeRecord.subLookup = parseSPFRecordWithDepth(includedSPF.spfRecord, includeDomain, visitedDomains, depth+1)
}
}
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...)
}
}
}
}
return parsedRecord
}
// Check if subdomain SPF exists
func subDomainSPFNotExists(domain string) bool {
// Try to lookup a fake subdomain to see if wildcard SPF exists
randomSub := "random123456" + "." + domain
txtRecords, err := net.LookupTXT(randomSub)
if err != nil {
return true // Error means no wildcard record
}
for _, txt := range txtRecords {
if strings.HasPrefix(txt, "v=spf1") {
return false // Found SPF for subdomain
}
}
return true
}
// Check for issues in SPF and DMARC records
func (engine *IssueEngine) IssueScan(spfDMARCRecord SPFDMARCRecord, extendedSPF *ParsedSPFRecord) []IssueScanResult {
issueResults := make([]IssueScanResult, 0)
spfIssues := engine.SPFIssueScan(spfDMARCRecord.spfRecord, spfDMARCRecord.domainName, extendedSPF)
issueResults = append(issueResults, spfIssues...)
if len(issueResults) >= 1 {
if issueResults[0].code != 0 && issueResults[0].code != 11 {
issueResults = engine.DMARCIssueScan(spfDMARCRecord.dmarcRecord, spfDMARCRecord.domainName, issueResults)
}
} else {
issueResults = engine.DMARCIssueScan(spfDMARCRecord.dmarcRecord, spfDMARCRecord.domainName, issueResults)
}
return issueResults
}
// Check for SPF-related issues
func (engine *IssueEngine) SPFIssueScan(spfRecord string, domainName string, parsedSPF *ParsedSPFRecord) []IssueScanResult {
spfIssues := make([]IssueScanResult, 0)
if subDomainSPFNotExists(domainName) {
spfIssues = append(spfIssues, engine.issueDescriptors(6, domainName))
}
if isNullOrWhiteSpace(spfRecord) {
_, err := net.LookupIP(domainName)
if err != nil {
// Check if it's a CNAME
_, err = net.LookupCNAME(domainName)
if err != nil {
spfIssues = append(spfIssues, engine.issueDescriptors(0, domainName))
} else {
spfIssues = append(spfIssues, engine.issueDescriptors(1, domainName))
}
} else {
spfIssues = append(spfIssues, engine.issueDescriptors(1, domainName))
}
} else {
// Check for redirect and update SPF record if needed
if strings.Contains(spfRecord, "redirect=") {
splitSPF := strings.Split(spfRecord, " ")
for _, part := range splitSPF {
if strings.HasPrefix(part, "redirect=") {
redirectDomain := strings.TrimPrefix(part, "redirect=")
redirectRecord := getSPFDMARCRecord(redirectDomain)
spfRecord = redirectRecord.spfRecord
break
}
}
}
if strings.Contains(spfRecord, "+all") {
spfIssues = append(spfIssues, engine.issueDescriptors(3, domainName))
} else if strings.Contains(spfRecord, "~all") {
spfIssues = append(spfIssues, engine.issueDescriptors(4, domainName))
} else if !strings.Contains(spfRecord, "-all") || strings.Contains(spfRecord, "?all") {
spfIssues = append(spfIssues, engine.issueDescriptors(2, domainName))
}
if engine.includeRollUp(parsedSPF) > 10 {
spfIssues = append(spfIssues, engine.issueDescriptors(5, domainName))
}
// Check for loops
seenIncludes := make(map[string]bool)
for _, include := range engine.rolledIncludeRecords {
if seenIncludes[include] {
spfIssues = append(spfIssues, engine.issueDescriptors(12, domainName))
break
}
seenIncludes[include] = true
}
}
return spfIssues
}
// Check for DMARC-related issues
func (engine *IssueEngine) DMARCIssueScan(dmarcRecord string, domainName string, spfIssues []IssueScanResult) []IssueScanResult {
dmarcIssues := make([]IssueScanResult, 0)
updatedIssues := make([]IssueScanResult, 0)
if isNullOrWhiteSpace(dmarcRecord) {
dmarcIssues = append(dmarcIssues, engine.issueDescriptors(7, domainName))
} else {
if strings.Contains(dmarcRecord, "; p=none") || strings.Contains(dmarcRecord, ";p=none") {
dmarcIssues = append(dmarcIssues, engine.issueDescriptors(8, domainName))
}
if strings.Contains(dmarcRecord, "sp=none") {
dmarcIssues = append(dmarcIssues, engine.issueDescriptors(9, domainName))
}
if strings.Contains(dmarcRecord, "pct=") && !strings.Contains(dmarcRecord, "pct=100") {
dmarcIssues = append(dmarcIssues, engine.issueDescriptors(10, domainName))
}
}
hasDMARCIssue7 := false
hasDMARCIssue8 := false
hasDMARCIssue10 := false
for _, issue := range dmarcIssues {
if issue.code == 7 {
hasDMARCIssue7 = true
} else if issue.code == 8 {
hasDMARCIssue8 = true
} else if issue.code == 10 {
hasDMARCIssue10 = true
}
}
if !hasDMARCIssue7 && !hasDMARCIssue8 && !hasDMARCIssue10 {
for _, spfIssue := range spfIssues {
if spfIssue.code != 3 {
updatedIssues = append(updatedIssues, engine.issueDescriptorUpdate(spfIssue))
} else {
updatedIssues = append(updatedIssues, spfIssue)
}
}
} else {
updatedIssues = append(updatedIssues, spfIssues...)
}
updatedIssues = append(updatedIssues, dmarcIssues...)
return updatedIssues
}
// Count total DNS lookups needed for SPF validation
func (engine *IssueEngine) includeRollUp(parsedSPF *ParsedSPFRecord) int {
if parsedSPF == nil {
return 0
}
// Reset the include record tracking
engine.rolledIncludeRecords = make([]string, 0)
// Track visited domains to prevent infinite loops
visited := make(map[string]bool)
// Count lookups with safety limits
lookupCount := 0
// Count all mechanisms in the base record
lookupCount += len(parsedSPF.aRecord)
lookupCount += len(parsedSPF.mxRecord)
lookupCount += len(parsedSPF.existsRecords)
lookupCount += len(parsedSPF.ptrRecords)
// Process all includes with a safety limit
for _, includeRecord := range parsedSPF.includeRecords {
engine.rolledIncludeRecords = append(engine.rolledIncludeRecords, includeRecord.includeRecord)
lookupCount++ // Count the include itself as a lookup
// Process the include safely
if includeRecord.subLookup != nil {
subCount := engine.countLookupsNonRecursive(includeRecord.subLookup, visited)
lookupCount += subCount
// Early exit if we're already over the limit
if lookupCount > 10 {
return lookupCount
}
}
}
return lookupCount
}
// Non-recursive version of countLookupsRecursive to prevent stack overflow
func (engine *IssueEngine) countLookupsNonRecursive(spf *ParsedSPFRecord, visited map[string]bool) int {
if spf == nil {
return 0
}
// Count basic lookups
lookupCount := len(spf.aRecord) + len(spf.mxRecord) + len(spf.existsRecords) + len(spf.ptrRecords)
// Use a stack to simulate recursion
type StackItem struct {
spf *ParsedSPFRecord
}
stack := []StackItem{{spf: spf}}
processedIncludes := make(map[string]bool)
for len(stack) > 0 {
// Check if we're already over limit for early exit
if lookupCount > 10 {
return lookupCount
}
// Pop from stack
current := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// Process all includes in this record
for _, include := range current.spf.includeRecords {
// Skip if already processed this include to prevent loops
if processedIncludes[include.includeRecord] {
continue
}
// Mark as processed and count it
processedIncludes[include.includeRecord] = true
engine.rolledIncludeRecords = append(engine.rolledIncludeRecords, include.includeRecord)
lookupCount++ // Count the include itself
// Add nested includes to stack if they exist
if include.subLookup != nil {
stack = append(stack, StackItem{spf: include.subLookup})
// Count direct mechanisms in this include
lookupCount += len(include.subLookup.aRecord)
lookupCount += len(include.subLookup.mxRecord)
lookupCount += len(include.subLookup.existsRecords)
lookupCount += len(include.subLookup.ptrRecords)
}
}
}
return lookupCount
}
// Update issue status to mitigated when appropriate DMARC policies are in place
func (engine *IssueEngine) issueDescriptorUpdate(issueResult IssueScanResult) IssueScanResult {
issueResult.detail = "This issue has been mitigated through the DMARC policy 'p' qualifier being set to 'Quarantine' or 'Reject'."
issueResult.severity = "Mitigated"
return issueResult
}
// Create issue descriptors based on the issue code
func (engine *IssueEngine) issueDescriptors(code int, domain string) IssueScanResult {
issueResult := IssueScanResult{}
switch code {
case 0:
issueResult.code = 0
issueResult.title = "Non-existent domain"
issueResult.detail = "The DNS resolver raised an NXDomain error for " + domain + ". Mail receivers will be unable to resolve a DNS response for your domain and will almost certainly flag any mail as spam."
issueResult.severity = "Low"
case 1:
issueResult.code = 1
issueResult.title = "No SPF record exists"
issueResult.detail = "There is no SPF DNS record for " + domain + ". Mail receivers have no mechanism to determine what your authorised mail servers are. Mail receivers will pass authentication checks with a \"None\" result, indicating no check could be performed. Spoofed emails are likely to be accepted."
issueResult.severity = "High"
case 2:
issueResult.code = 2
issueResult.title = "SPF \"all\" mechanism is missing or set to \"?all\""
issueResult.detail = "The \"all\" mechanism at the end of the end of an SPF record tells receivers how to treat unauthorised (i.e. spoofed) emails - if the mechanism is missing or set to \"?all\", authentication checks will always return a \"Neutral\" result which many receivers interpret to accept all mail from " + domain + " (including spoofed emails)."
issueResult.severity = "High"
case 3:
issueResult.code = 3
issueResult.title = "SPF \"+all\" mechanism set"
issueResult.detail = "The \"all\" mechanism at the end of the end of an SPF record tells receivers how to treat unauthorised (i.e. spoofed) emails - the \"+all\" setting tells receivers to pass/accept all mail from " + domain + " (including spoofed emails)."
issueResult.severity = "Very High"
case 4:
issueResult.code = 4
issueResult.title = "SPF \"~all\" (SoftFail) mechanism set"
issueResult.detail = "The \"all\" mechanism at the end of the end of an SPF record tells receivers how to treat unauthorised (i.e. spoofed) emails - the \"~all\" setting tells receivers to 'SoftFail' (i.e. quarantine) emails that fail SPF authentication. In practice however, many email filters only slightly raise the total spam score and accept 'SoftFailed' (i.e. spoofed) emails."
issueResult.severity = "Medium"
case 5:
issueResult.code = 5
issueResult.title = "SPF has too many lookups for receiver validation"
issueResult.detail = "The SPF record requires more than 10 DNS lookups for the validation process. The RFC states that maximum 10 lookups are allowed. As a result, recipients may throw a PermError instead of proceeding with SPF validation. Recipients treat these errors differently than a hard or soft SPF fail, but some will continue processing the mail (i.e. accept spoofed emails)."
issueResult.severity = "Medium"
case 6:
issueResult.code = 6
issueResult.title = "No SPF sub-domain record exists"
issueResult.detail = "The SPF sub-domain policy is a catch-all mechanism used to prevent threat actors from maliciously spoofing sub-domains from which an explicit SPF record hasn't been set. This is typically represented through a DNS entry similar to \"* IN TXT v=spf1 -all\", effectively telling recipients to block mail if an explicit SPF entry for the sub-domain hasn't been set."
issueResult.severity = "Medium"
case 7:
issueResult.code = 7
issueResult.title = "No DMARC record exists"
issueResult.detail = "There is no DMARC DNS Record set for the domain. Spoofed emails utilising an attack technique known as SPF-bypass are likely to be accepted."
issueResult.severity = "High"
case 8:
issueResult.code = 8
issueResult.title = "Insecure DMARC policy 'p' qualifier"
issueResult.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."
issueResult.severity = "High"
case 9:
issueResult.code = 9
issueResult.title = "Insecure DMARC sub-domain 'p' qualifier"
issueResult.detail = "The DMARC policy 'p' qualifier for sub-domains is set to \"none\". If the DMARC policy is neither \"reject\" nor \"quarantine\", spoofed emails from any " + domain + " sub-domain utilising an attack technique known as SPF-bypass are likely to be accepted."
issueResult.severity = "High"
case 10:
issueResult.code = 10
issueResult.title = "Partial DMARC coverage"
issueResult.detail = "The DMARC \"pct\" value is set to less than '100' (i.e. 100%), meaning the DMARC policy will only be applied to a percentage of incoming mail. A threat actor can continously deliver spoofed emails (via SPF-bypass) until the DMARC policy isn't applied and mail is accepted."
issueResult.severity = "Medium"
case 11:
issueResult.code = 11
issueResult.title = "DNS Timeout during Scan"
issueResult.detail = "There was a DNS timeout when querying " + domain + ". This will result in an SPF temperror and any mail will almost certainly be flagged as spam by mail receivers"
issueResult.severity = "Low"
case 12:
issueResult.code = 12
issueResult.title = "Trivial SPF recurse loop"
issueResult.detail = "The SPF record is configured whereby an infinite lookup loop exists in the validation chain for " + domain + ". This will likely result in an SPF PermError. Recipients will treat these errors differently than a hard or soft SPF fail, but many will continue processing the mail (i.e. accept spoofed emails)."
issueResult.severity = "Medium"
}
return issueResult
}
func main() {
// Parse command line arguments
var domain string
var interactive bool
var jsonOutput bool
var outputFile string
var inputFile string
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.Parse()
// Process domains from input file if specified
if inputFile != "" {
processDomainFile(inputFile, jsonOutput, outputFile)
} else if interactive {
for {
fmt.Print("Enter domain to check (or 'exit' to quit): ")
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
domain = scanner.Text()
if strings.ToLower(domain) == "exit" {
break
}
checkDomain(domain, jsonOutput, outputFile)
}
} else if domain != "" {
checkDomain(domain, jsonOutput, outputFile)
} else {
fmt.Println("Please specify a domain with -domain, use -input to read from a file, or use -interactive")
flag.Usage()
}
}
// Process a file containing a list of domains
func processDomainFile(inputFile string, jsonOutput bool, outputFile string) {
// Read the file
file, err := os.Open(inputFile)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", inputFile, err)
return
}
defer file.Close()
// Create a scanner to read the file line by line
scanner := bufio.NewScanner(file)
// Open output file once if specified
var outFile *os.File
if outputFile != "" {
outFile, err = os.Create(outputFile)
if err != nil {
fmt.Printf("Error creating output file %s: %v\n", outputFile, err)
return
}
defer outFile.Close()
// If JSON output, start the JSON array
if jsonOutput {
outFile.WriteString("[\n")
}
}
// Process each domain
domainCount := 0
for scanner.Scan() {
domain := strings.TrimSpace(scanner.Text())
if domain == "" || strings.HasPrefix(domain, "#") {
continue // Skip empty lines and comments
}
// 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)
// Format the output
var result string
if jsonOutput {
// Convert to JSON
jsonResult := issuestoJSON(domain, spfDmarcRecord, issues)
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))
}
}
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)
}
}
domainCount++
}
// Check for scanner errors
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading file: %v\n", err)
}
// If JSON output and file output, close the JSON array
if outputFile != "" && jsonOutput {
outFile.WriteString("\n]")
}
fmt.Printf("Processed %d domains from %s\n", domainCount, inputFile)
if outputFile != "" {
fmt.Printf("Results saved to %s\n", outputFile)
}
}
// Convert scan results to JSON format
func issuestoJSON(domain string, spfDmarcRecord SPFDMARCRecord, issues []IssueScanResult) JSONScanResult {
result := JSONScanResult{
Domain: domain,
SPF: spfDmarcRecord.spfRecord,
DMARC: spfDmarcRecord.dmarcRecord,
Issues: make([]JSONIssue, 0),
Success: true,
}
// If no issues found
if len(issues) == 0 {
result.Message = "No issues found. The domain is well protected against spoofing."
} else {
for _, issue := range issues {
jsonIssue := JSONIssue{
Code: issue.code,
Title: issue.title,
Detail: issue.detail,
Severity: issue.severity,
}
result.Issues = append(result.Issues, jsonIssue)
}
}
return result
}
func checkDomain(domain string, jsonOutput bool, outputFile string) {
// 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)
var output string
var fileOutput string
if jsonOutput {
// Format as JSON
jsonResult := issuestoJSON(domain, spfDmarcRecord, issues)
jsonData, err := json.MarshalIndent(jsonResult, "", " ")
if err != nil {
output = fmt.Sprintf("{\"success\": false, \"message\": \"Error generating JSON: %s\"}\n", err)
fileOutput = output
} else {
output = string(jsonData)
// For file output, wrap in square brackets for proper JSON array format
if outputFile != "" {
fileOutput = "[\n" + output + "\n]"
} else {
fileOutput = output
}
}
} 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))
}
}
builder.WriteString("\n--- End of report ---\n")
output = builder.String()
fileOutput = output
}
// Display the output
fmt.Print(output)
// Save to file if specified
if outputFile != "" {
err := writeToFile(outputFile, fileOutput)
if err != nil {
fmt.Printf("Error writing to file %s: %v\n", outputFile, err)
} else {
fmt.Printf("Results saved to %s\n", outputFile)
}
}
}
// Write string data to a file
func writeToFile(filename string, data string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(data)
if err != nil {
return err
}
return nil
}