From a2f2a51701196abc9d5bbd46857820e26e4ce908 Mon Sep 17 00:00:00 2001 From: vmfunc <59031302+vmfunc@users.noreply.github.com> Date: Sat, 12 Oct 2024 22:52:27 +0200 Subject: [PATCH] feat: subdomain takeover checks --- README.md | 1 + pkg/config/config.go | 2 + pkg/scan/subdomaintakeover.go | 154 ++++++++++++++++++++++++++++++++++ sif.go | 27 +++++- 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 pkg/scan/subdomaintakeover.go diff --git a/README.md b/README.md index 1203c0f..458eb27 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ _(__ )_ / _ __/ - 📦 CMS detection - 🔍 HTTP Header Analysis - ☁️ C3 Misconfiguration Scanner +- 🔍 Subdomain Takeover Checks ## Contributing and support diff --git a/pkg/config/config.go b/pkg/config/config.go index 73ffe0a..dbbd8b4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -28,6 +28,7 @@ type Settings struct { CMS bool Headers bool CloudStorage bool + SubdomainTakeover bool } const ( @@ -69,6 +70,7 @@ func Parse() *Settings { flagSet.BoolVar(&settings.CMS, "cms", false, "Enable CMS detection"), flagSet.BoolVar(&settings.Headers, "headers", false, "Enable HTTP Header Analysis"), flagSet.BoolVar(&settings.CloudStorage, "c3", false, "Enable C3 Misconfiguration Scan"), + flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"), ) flagSet.CreateGroup("runtime", "Runtime", diff --git a/pkg/scan/subdomaintakeover.go b/pkg/scan/subdomaintakeover.go new file mode 100644 index 0000000..76f9d99 --- /dev/null +++ b/pkg/scan/subdomaintakeover.go @@ -0,0 +1,154 @@ +package scan + +import ( + "fmt" + "io" + "net" + "net/http" + "strings" + "time" + "os" + "sync" + "github.com/charmbracelet/log" + "github.com/dropalldatabases/sif/internal/styles" + "github.com/dropalldatabases/sif/pkg/logger" +) + +type SubdomainTakeoverResult struct { + Subdomain string `json:"subdomain"` + Vulnerable bool `json:"vulnerable"` + Service string `json:"service,omitempty"` +} + +func SubdomainTakeover(url string, dnsResults []string, timeout time.Duration, threads int, logdir string) ([]SubdomainTakeoverResult, error) { + fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Subdomain Takeover Vulnerability Check") + "...")) + + sanitizedURL := strings.Split(url, "://")[1] + + if logdir != "" { + if err := logger.WriteHeader(sanitizedURL, logdir, "Subdomain Takeover Vulnerability Check"); err != nil { + log.Errorf("Error creating log file: %v", err) + return nil, err + } + } + + subdomainlog := log.NewWithOptions(os.Stderr, log.Options{ + Prefix: "Subdomain Takeover 🔍", + }) + + client := &http.Client{ + Timeout: timeout, + } + + var wg sync.WaitGroup + wg.Add(threads) + + resultsChan := make(chan SubdomainTakeoverResult, len(dnsResults)) + + for thread := 0; thread < threads; thread++ { + go func(thread int) { + defer wg.Done() + + for i, subdomain := range dnsResults { + if i%threads != thread { + continue + } + + vulnerable, service := checkSubdomainTakeover(subdomain, client) + result := SubdomainTakeoverResult{ + Subdomain: subdomain, + Vulnerable: vulnerable, + Service: service, + } + resultsChan <- result + + if vulnerable { + subdomainlog.Warnf("Potential subdomain takeover: %s (%s)", styles.Highlight.Render(subdomain), service) + if logdir != "" { + logger.Write(sanitizedURL, logdir, fmt.Sprintf("Potential subdomain takeover: %s (%s)\n", subdomain, service)) + } + } else { + subdomainlog.Infof("Subdomain not vulnerable: %s", subdomain) + } + } + }(thread) + } + + go func() { + wg.Wait() + close(resultsChan) + }() + + var results []SubdomainTakeoverResult + for result := range resultsChan { + results = append(results, result) + } + + return results, nil +} + +func checkSubdomainTakeover(subdomain string, client *http.Client) (bool, string) { + resp, err := client.Get("http://" + subdomain) + if err != nil { + if strings.Contains(err.Error(), "no such host") { + // Check if CNAME exists + cname, err := net.LookupCNAME(subdomain) + if err == nil && cname != "" { + return true, "Dangling CNAME" + } + } + return false, "" + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + bodyString := string(body) + + // Check for common takeover signatures in the response + signatures := map[string]string{ + "GitHub Pages": "There isn't a GitHub Pages site here.", + "Heroku": "No such app", + "Shopify": "Sorry, this shop is currently unavailable.", + "Tumblr": "There's nothing here.", + "WordPress": "Do you want to register *.wordpress.com?", + "Amazon S3": "The specified bucket does not exist", + "Bitbucket": "Repository not found", + "Ghost": "The thing you were looking for is no longer here, or never was", + "Pantheon": "The gods are wise, but do not know of the site which you seek.", + "Fastly": "Fastly error: unknown domain", + "Zendesk": "Help Center Closed", + "Teamwork": "Oops - We didn't find your site.", + "Helpjuice": "We could not find what you're looking for.", + "Helpscout": "No settings were found for this company:", + "Cargo": "If you're moving your domain away from Cargo you must make this configuration through your registrar's DNS control panel.", + "Uservoice": "This UserVoice subdomain is currently available!", + "Surge": "project not found", + "Intercom": "This page is reserved for artistic dogs.", + "Webflow": "The page you are looking for doesn't exist or has been moved.", + "Kajabi": "The page you were looking for doesn't exist.", + "Thinkific": "You may have mistyped the address or the page may have moved.", + "Tave": "Sorry, this page is no longer available.", + "Wishpond": "https://www.wishpond.com/404?campaign=true", + "Aftership": "Oops.

The page you're looking for doesn't exist.", + "Aha": "There is no portal here ... sending you back to Aha!", + "Brightcove": "

", + "Bigcartel": "

Oops! We couldn’t find that page.

", + "Activecompaign": "alt=\"LIGHTTPD - fly light.\"", + "Compaignmonitor": "Double check the URL or