├── domains.txt ├── .gitignore ├── Dockerfile ├── Gopkg.toml ├── LICENSE ├── Gopkg.lock ├── providers-data.csv ├── README.md └── tko-subs.go /domains.txt: -------------------------------------------------------------------------------- 1 | test.com 2 | anshumanbhartiya.com 3 | www.anshumanbhartiya.com 4 | github.anshumanbhartiya.com 5 | helpscout.anshumanbhartiya.com 6 | bitbucket.anshumanbhartiya.com 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .env 27 | output.csv 28 | 29 | vendor/ 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Container 2 | FROM golang:1.9.4-alpine3.7 AS build-env 3 | RUN apk add --no-cache --upgrade git 4 | RUN go get -u github.com/golang/dep/cmd/dep 5 | WORKDIR /go/src/app 6 | 7 | # Cache the dependencies early 8 | COPY Gopkg.toml Gopkg.lock ./ 9 | RUN dep ensure -vendor-only -v 10 | 11 | # Build 12 | COPY *.go ./ 13 | RUN go build -o ./tkosubs *.go 14 | 15 | # Final Container 16 | FROM alpine:3.7 17 | COPY --from=build-env /go/src/app/tkosubs /usr/bin/tkosubs 18 | RUN mkdir /app 19 | WORKDIR /app 20 | COPY providers-data.csv . 21 | COPY domains.txt . 22 | ENTRYPOINT ["/usr/bin/tkosubs"] 23 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/gocarina/gocsv" 31 | 32 | [[constraint]] 33 | name = "github.com/miekg/dns" 34 | version = "1.0.8" 35 | 36 | [[constraint]] 37 | branch = "master" 38 | name = "github.com/olekukonko/tablewriter" 39 | 40 | [[constraint]] 41 | branch = "master" 42 | name = "golang.org/x/net" 43 | 44 | [prune] 45 | go-tests = true 46 | unused-packages = true 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Anshuman Bhartiya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/gocarina/gocsv" 7 | packages = ["."] 8 | revision = "a5c9099e2484f1551abb9433885e158610a25f4b" 9 | 10 | [[projects]] 11 | name = "github.com/mattn/go-runewidth" 12 | packages = ["."] 13 | revision = "9e777a8366cce605130a531d2cd6363d07ad7317" 14 | version = "v0.0.2" 15 | 16 | [[projects]] 17 | name = "github.com/miekg/dns" 18 | packages = ["."] 19 | revision = "5a2b9fab83ff0f8bfc99684bd5f43a37abe560f1" 20 | version = "v1.0.8" 21 | 22 | [[projects]] 23 | branch = "master" 24 | name = "github.com/olekukonko/tablewriter" 25 | packages = ["."] 26 | revision = "d4647c9c7a84d847478d890b816b7d8b62b0b279" 27 | 28 | [[projects]] 29 | branch = "master" 30 | name = "golang.org/x/crypto" 31 | packages = [ 32 | "ed25519", 33 | "ed25519/internal/edwards25519" 34 | ] 35 | revision = "56440b844dfe139a8ac053f4ecac0b20b79058f4" 36 | 37 | [[projects]] 38 | branch = "master" 39 | name = "golang.org/x/net" 40 | packages = [ 41 | "bpf", 42 | "idna", 43 | "internal/iana", 44 | "internal/socket", 45 | "ipv4", 46 | "ipv6", 47 | "publicsuffix" 48 | ] 49 | revision = "f4c29de78a2a91c00474a2e689954305c350adf9" 50 | 51 | [[projects]] 52 | name = "golang.org/x/text" 53 | packages = [ 54 | "collate", 55 | "collate/build", 56 | "internal/colltab", 57 | "internal/gen", 58 | "internal/tag", 59 | "internal/triegen", 60 | "internal/ucd", 61 | "language", 62 | "secure/bidirule", 63 | "transform", 64 | "unicode/bidi", 65 | "unicode/cldr", 66 | "unicode/norm", 67 | "unicode/rangetable" 68 | ] 69 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 70 | version = "v0.3.0" 71 | 72 | [solve-meta] 73 | analyzer-name = "dep" 74 | analyzer-version = 1 75 | inputs-digest = "bc0cd1204a8bfab44261e62b8fa66ab43fbcf4fd63cf055124b3799135d310e2" 76 | solver-name = "gps-cdcl" 77 | solver-version = 1 78 | -------------------------------------------------------------------------------- /providers-data.csv: -------------------------------------------------------------------------------- 1 | name,cname,string 2 | acquia,acquia-test.co,The site you are looking for could not be found 3 | acquia,acquia.com,If you are an Acquia Cloud customer and expect to see your site at this address 4 | activecampaign,activehosted.com,LIGHTTPD - fly light 5 | aftership,aftership.com,The page you're looking for 6 | aha,ideas.aha.io,There is no portal here 7 | amazonaws,amazonaws.com,NoSuchBucket 8 | amazonaws,amazonaws.com,The specified bucket does not exist 9 | azure,azurewebsites.net,404 Web Site not found 10 | azure,cloudapp.net,404 Web Site not found 11 | azure,trafficmanager.net,404 Web Site not found 12 | bigcartel,bigcartel.com,

Oops! We couldn 13 | bitbucket,bitbucket.io,Repository not found 14 | bitbucket,bitbucket.org,The page you have requested does not exist 15 | brightcove,bcvp0rtal.com,Error Code: 404 16 | brightcove,brigtcovegallery.com,Error Code: 404 17 | brightcove,gallery.video,Error Code: 404 18 | campaignmonitor,createsend.com,Double check the URL 19 | cargocollective,cargocollective.com,If you're moving your domain away from Cargo 20 | cloudfront,cloudfront.net,The request could not be satisfied 21 | desk,desk.com,Please try again or try Desk.com free for 14 days. 22 | desk,desk.com,We Couldn't Find That Page 23 | fastly,fastly.net,Fastly error: unknown domain 24 | fastly,fastly.net,Please check that this domain has been added to a service 25 | feedpress,redirect.feedpress.me,The feed has not been found 26 | freshdesk,freshdesk.com,May be this is still fresh! 27 | getresponse,gr8.com,With GetResponse Landing Pages 28 | ghost,ghost.io,The thing you were looking for is no longer here 29 | github,github,For root URLs (like http://example.com/) you must provide an index.html file 30 | github,github,There isn't a GitHub Pages site here. 31 | helpjuice,helpjuice.com,We could not find what you're looking for. 32 | helpscout,helpscoutdocs.com,No settings were found for this company: 33 | heroku,heroku,No such app 34 | heroku,heroku,There's nothing here 35 | instapage,instapage.com,You've Discovered A Missing Link. Our Apologies! 36 | instapage,pageserve.co,You've Discovered A Missing Link. Our Apologies! 37 | intercom,custom.intercom.help,This page is reserved for 38 | jetbrains,myjetbrains.com,is not a registered InCloud YouTrack 39 | kajabi,endpoint.mykajabi.com,The page you were looking for 40 | mashery,mashery.com,Unrecognized domain 41 | pantheon,pantheonsite.io,The gods are wise 42 | pingdom,stats.pingdom.com,pingdom 43 | proposify,proposify.biz,If you need immediate assistance 44 | shopify,myshopify.com,Only one step left! 45 | shopify,myshopify.com,this shop is currently unavailable 46 | simplebooklet,simplebooklet.com,We can't find this 47 | smartling,smartling.com,Domain is not configured 48 | squarespace,squarespace.com,Website Expired 49 | squarespace,squarespace.com,You're Almost There... 50 | squarespace,squarespace.com,is not available 51 | statuspage,statuspage.io,Better Status Communication 52 | statuspage,statuspage.io,You are being 53 | surge,surge.sh,project not found 54 | surveygizmo,privatedomain.sgizmo.com,data-html-name 55 | surveygizmo,privatedomain.sgizmoca.com,data-html-name 56 | surveygizmo,privatedomain.surveygizmo.eu,data-html-name 57 | tave,clientaccess.tave.com,Error 404: Page Not Found 58 | teamwork,teamwork.com,We didn't find your site. 59 | thinkific,thinkific.com,You may have mistyped the address 60 | tictail,tictail.com,Building a brand of your own? 61 | tilda,tilda.ws,Domain has been assigned 62 | tumblr,tumblr.com,There's nothing here. 63 | tumblr,tumblr.com,Whatever you were looking for doesn't currently exist at this address 64 | unbounce,unbouncepages.com,The requested URL / was not found on this server. 65 | uservoice,uservoice.com,This UserVoice subdomain is currently available! 66 | vend,vendecommerce.com,Looks like you've traveled 67 | vimeo,vimeopro.com,not found 68 | webflow,proxy.webflow.io,The page you're looking for 69 | wishpond,wishpond.com,404?campaign=true 70 | wordpress,wordpress,Domain mapping upgrade for this domain not found 71 | wordpress,wordpress.com,Do you want to register 72 | wpengine,wpengine.com,The site you were looking for couldn't be found 73 | wpengine,wpengine.com,The site you were looking for is no longer available at this IP address 74 | zendesk,zendesk.com,Help Center Closed | Zendesk 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tko-subs 2 | 3 | This tool allows: 4 | * To check whether a subdomain can be taken over because it has: 5 | * a dangling CNAME pointing to a CMS provider (Heroku, Github, Shopify, Amazon S3, Amazon CloudFront, etc.) that can be taken over. 6 | * a dangling CNAME pointing to a non-existent domain name 7 | * one or more wrong/typoed NS records pointing to a nameserver that can be taken over by an attacker to gain control of the subdomain's DNS records 8 | 9 | * To actually take over those subdomain by providing a flag `-takeover`. Currently, take over is only supported for Github Pages and Heroku Apps and by default the take over functionality is off. 10 | 11 | * To specify your own CMS providers and check for them via the [providers-data.csv](providers-data.csv) file. In that file, you would mention the CMS name, their CNAME value, their string that you want to look for and whether it only works over HTTP or not. Check it out for some examples. 12 | 13 | 14 | ### Disclaimer: DONT BE A JERK! 15 | 16 | Needless to mention, please use this tool very very carefully. The authors won't be responsible for any consequences. 17 | By default, this tool does not allow taking over of subdomains. If you want to do it, just specify the `-takeover` flag. 18 | 19 | 20 | ### Pre-requisites 21 | 22 | We need GO installed. Once you have GO, just type `go get github.com/anshumanbh/tko-subs` to download the tool. 23 | 24 | Once the tool is downloaded, type `tko-subs -h`. 25 | 26 | The next thing we need to do is to get the following information: 27 | * Github's Personal Access Token - Make sure this token has the rights to create repositories, references, contents, etc. You can create this token here - https://github.com/settings/tokens 28 | * Heroku Username and API key 29 | * Heroku app name - You can create a static app on Heroku with whatever you want to be displayed on its homepage by following the instructions here - https://gist.github.com/wh1tney/2ad13aa5fbdd83f6a489. Once you create that app, use that app name in the flag (see below). We will use that app to takeover the domain (with the dangling CNAME to another Heroku app). 30 | 31 | NOTE - You only need these values if you want to take over subdomains. By default, that's not required. 32 | 33 | 34 | ### How to run? 35 | 36 | Once you have everything installed, `cd` into the directory and type: 37 | `tko-subs -domains=domains.txt -data=providers-data.csv -output=output.csv` 38 | 39 | If you want to take over as well, the command would be: 40 | `tko-subs -domains=domains.txt -data=providers-data.csv -output=output.csv -takeover -githubtoken= -herokuusername= -herokuapikey= -herokuappname=` 41 | 42 | If you just want to check for a single domain, type: 43 | `tko-subs -domain ` 44 | 45 | If you just want to check for multiple domains, type: 46 | `tko-subs -domain ,` 47 | 48 | By default: 49 | * the `domains` flag is set to `domains.txt` 50 | * the `data` flag is set to `providers-data.csv` 51 | * the `output` flag is set to `output.csv` 52 | * the `takeover` flag is not set so no take over by default 53 | * the `domain` flag is NOT set so it will always check for all the domains mentioned in the `domains.txt` file. If the `domain` flag is mentioned, it will only check that domain and ignore the `domains.txt` file, even if present 54 | * the `threads` flag is set to `5` 55 | 56 | So, simply running `tko-subs` would run with the default values mentioned above. 57 | 58 | 59 | ### How is providers-data.csv formatted? 60 | 61 | name,cname,string,http 62 | 63 | * name: The name of the provider (e.g. github) 64 | * cname: The CNAME used to map a website to the provider's content (e.g. github.io) 65 | * string: The error message returned for an unclaimed subdomain (e.g. "There isn't a GitHub Pages site here") 66 | * http: Whether to use http (not https, which is the default) to connect to the site (true/false) 67 | 68 | 69 | ### How is the output formatted? 70 | 71 | Domain,CNAME,Provider,IsVulnerable,IsTakenOver,Response 72 | 73 | * Domain: The domain checked 74 | * CNAME: The CNAME of the domain 75 | * Provider: The provider the domain was found to be using 76 | * IsVulnerable: Whether the domain was found to be vulnerable or not (true/false) 77 | * IsTakenOver: Whether the domain was taken over or not (true/false) 78 | * Response: The message that the subdomain was checked against 79 | 80 | If a dead DNS record is found, `Provider` is left empty. 81 | If a misbehaving nameserver is found, `Provider` and `CNAME` are left empty 82 | 83 | ### What is going on under the hood? 84 | 85 | This will iterate over all the domains (concurrently using GoRoutines) in the `subdomains.txt` file and: 86 | * See if they have a misbehaving authoritative nameserver; if they do, we mark that domain as vulnerable. 87 | * See if they have dangling CNAME records aka dead DNS records; if they do we mark that domain as vulnerable. 88 | * If a subdomain passes these two tests, it tries to curl them and get back a response and then try to see if that response matches any of the data provider strings mentioned in the [providers-data.csv](providers-data.csv) file. 89 | * If the response matches, we mark that domain as vulnerable. 90 | * Next, depending upon whether the `takeover` flag is mentioned or not, it will try to take over that vulnerable subdomain. 91 | * For example, to takeover a Github Page, the code will: 92 | * Create a repo 93 | * Create a branch `gh-pages` in that repo 94 | * Upload `CNAME` and `index.html` to the `gh-pages` branch in that repo. Here, `CNAME` contains the domain that needs to be taken over. `index.html` contains the text `This domain is temporarily suspended` that is to be displayed once the domain is taken over. 95 | * Similarly, for Heroku apps, the code will: 96 | * Add the dangling domain to your Heroku app (whose name you will be providing in the .env file) 97 | * And, that's it! 98 | 99 | 100 | ### Future Work 101 | 102 | * ~Take CMS name and regex from user or .env file and then automatically hook them into the tool to be able to find it.~ DONE 103 | * Add takeovers for more CMS 104 | * Add more CMS providers 105 | 106 | 107 | ### Credits 108 | 109 | * Thanks to Luke Young (@TheBoredEng) for helping me out with the go-github library. 110 | * Thanks to Frans Rosen (@fransrosen) for helping me understand the technical details that are required for some of the takeovers. 111 | * Thanks to Mohammed Diaa (@mhmdiaa) for taking time to implement the provider data functionality and getting the code going. 112 | * Thanks to high-stakes for a much needed code refresh. 113 | 114 | 115 | ### Changelog 116 | 117 | `5/27` 118 | * Added new Dockerfile reducing the size of the image 119 | * Added sample domains.txt file to test against 120 | * mhmdiaa added the logic for dead DNS takeovers. Updated documentation. Thanks a lot! 121 | 122 | `11/6` 123 | * high-stakes issues a PR with a bunch of new code that fixes a few bugs and makes the code cleaner 124 | 125 | `9/22` 126 | * Added an optional flag to check for single domain 127 | * Made it easier to install and run 128 | 129 | `6/25` 130 | * Made the code much more faster by implementing goroutines 131 | * Instead of checking using Golang's net packages' LookupCNAME function, made it to just use dig since that gives you dead DNS records as well. More attack surface!! 132 | 133 | -------------------------------------------------------------------------------- /tko-subs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "os" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "sync" 17 | "time" 18 | 19 | "github.com/gocarina/gocsv" 20 | "github.com/miekg/dns" 21 | "github.com/olekukonko/tablewriter" 22 | "golang.org/x/net/publicsuffix" 23 | ) 24 | 25 | type CMS struct { 26 | Name string `csv:"name"` 27 | CName string `csv:"cname"` 28 | String string `csv:"string"` 29 | } 30 | 31 | type DomainScan struct { 32 | Domain string 33 | Cname string 34 | Provider string 35 | IsVulnerable bool 36 | Response string 37 | } 38 | 39 | type Configuration struct { 40 | domainsFilePath *string 41 | recordsFilePath *string 42 | outputFilePath *string 43 | domain *string 44 | threadCount *int 45 | deadRecordCheck *bool 46 | } 47 | 48 | func main() { 49 | config := Configuration{ 50 | domainsFilePath: flag.String("domains", "domains.txt", "List of domains to check"), 51 | recordsFilePath: flag.String("data", "providers-data.csv", "CSV file containing CMS providers' string for identification"), 52 | outputFilePath: flag.String("output", "output.csv", "Output file to save the results"), 53 | domain: flag.String("domain", "", "Domains separated by ,"), 54 | threadCount: flag.Int("threads", 5, "Number of threads to run parallel"), 55 | deadRecordCheck: flag.Bool("dead-records", false, "Check for Dead DNS records too")} 56 | flag.Parse() 57 | 58 | cmsRecords := loadProviders(*config.recordsFilePath) 59 | var allResults []DomainScan 60 | 61 | if *config.domain != "" { 62 | for _, domain := range strings.Split(*config.domain, ",") { 63 | scanResults, err := scanDomain(domain, cmsRecords, config) 64 | if err == nil { 65 | allResults = append(allResults, scanResults...) 66 | } 67 | } 68 | } else { 69 | domainsFile, err := os.Open(*config.domainsFilePath) 70 | panicOnError(err) 71 | defer domainsFile.Close() 72 | domainsScanner := bufio.NewScanner(domainsFile) 73 | 74 | //Create an exec-queue with fixed size for parallel threads, it will block until new element can be added 75 | //Use this with a waitgroup to wait for threads which will be still executing after we have no elements to add to the queue 76 | semaphore := make(chan bool, *config.threadCount) 77 | var wg sync.WaitGroup 78 | 79 | for domainsScanner.Scan() { 80 | wg.Add(1) 81 | semaphore <- true 82 | go func(domain string) { 83 | scanResults, err := scanDomain(domain, cmsRecords, config) 84 | if err == nil { 85 | allResults = append(allResults, scanResults...) 86 | } /* else { 87 | fmt.Printf("[%s] Domain problem : %s\n", domain, err) 88 | }*/ 89 | <-semaphore 90 | wg.Done() 91 | }(domainsScanner.Text()) 92 | } 93 | wg.Wait() 94 | } 95 | 96 | printResults(allResults) 97 | 98 | if *config.outputFilePath != "" { 99 | writeResultsToCsv(allResults, *config.outputFilePath) 100 | Info("Results saved to: " + *config.outputFilePath) 101 | } 102 | } 103 | 104 | //panicOnError function as a generic check for error function 105 | func panicOnError(e error) { 106 | if e != nil { 107 | panic(e) 108 | } 109 | } 110 | 111 | //Info function to print pretty output 112 | func Info(format string, args ...interface{}) { 113 | fmt.Printf("\x1b[34;1m%s\x1b[0m\n", fmt.Sprintf(format, args...)) 114 | } 115 | 116 | // unFqdn removes the trailing from a FQDN 117 | func unFqdn(domain string) string { 118 | return strings.TrimSuffix(domain, ".") 119 | } 120 | 121 | //scanDomain function to scan for each domain being read from the domains file 122 | func scanDomain(domain string, cmsRecords []*CMS, config Configuration) ([]DomainScan, error) { 123 | // Check if the domain has a nameserver that returns servfail/refused 124 | if misbehavingNs, err := authorityReturnRefusedOrServfail(domain); misbehavingNs { 125 | scanResult := DomainScan{Domain: domain, IsVulnerable: true, Response: "REFUSED/SERVFAIL DNS status"} 126 | return []DomainScan{scanResult}, nil 127 | } else if err != nil { 128 | return nil, err 129 | } 130 | 131 | cname, err := getCnameForDomain(domain) 132 | for err != nil && err.Error() == "Recursion detected" { 133 | cname_old := cname 134 | cname, err = getCnameForDomain(cname_old) 135 | } 136 | 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | // Check if the domain has a dead DNS record, as in it's pointing to a CNAME that doesn't exist 142 | if exists, err := resolves(cname); !exists { 143 | scanResult := DomainScan{Domain: domain, Cname: cname, IsVulnerable: true, Response: "Dead DNS record"} 144 | return []DomainScan{scanResult}, nil 145 | } else if err != nil { 146 | return nil, err 147 | } 148 | 149 | scanResults := checkCnameAgainstProviders(domain, cname, cmsRecords, config) 150 | if len(scanResults) == 0 { 151 | err = errors.New(fmt.Sprintf("Cname [%s] found but could not determine provider", cname)) 152 | } 153 | return scanResults, err 154 | } 155 | 156 | // resolves function returns false if NXDOMAIN, and true otherwise 157 | func resolves(domain string) (bool, error) { 158 | client := dns.Client{} 159 | message := dns.Msg{} 160 | 161 | message.SetQuestion(dns.Fqdn(domain), dns.TypeA) 162 | r, _, err := client.Exchange(&message, "1.1.1.1:53") 163 | if err != nil { 164 | return false, err 165 | } 166 | if r.Rcode == dns.RcodeNameError { 167 | return false, nil 168 | } 169 | return true, nil 170 | } 171 | 172 | // getCnameForDomain function to lookup CNAME records of a domain 173 | // 174 | // Doing CNAME lookups using GOLANG's net package or for that matter just doing a host on a domain 175 | // does not necessarily let us know about any dead DNS records. So, we need to read the raw DNS response 176 | // to properly figure out if there are any dead DNS records 177 | func getCnameForDomain(domain string) (string, error) { 178 | c := dns.Client{} 179 | m := dns.Msg{} 180 | 181 | m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME) 182 | m.RecursionDesired = true 183 | 184 | r, _, err := c.Exchange(&m, "1.1.1.1:53") 185 | if err != nil { 186 | return "", err 187 | } 188 | 189 | if len(r.Answer) > 0 { 190 | record := r.Answer[0].(*dns.CNAME) 191 | cname := record.Target 192 | return cname, errors.New("Recursion detected") 193 | } else { 194 | return domain, nil 195 | } 196 | return "", errors.New("Cname not found") 197 | } 198 | 199 | // function parseNS to parse NS records (found in answer to NS query or in the authority section) into a list of record values 200 | func parseNS(records []dns.RR) []string { 201 | var recordData []string 202 | for _, ans := range records { 203 | if ans.Header().Rrtype == dns.TypeNS { 204 | record := ans.(*dns.NS) 205 | recordData = append(recordData, record.Ns) 206 | } else if ans.Header().Rrtype == dns.TypeSOA { 207 | record := ans.(*dns.SOA) 208 | recordData = append(recordData, record.Ns) 209 | } 210 | } 211 | return recordData 212 | } 213 | 214 | // getAuthorityForDomain function to lookup the authoritative nameservers of a domain 215 | func getAuthorityForDomain(domain string, nameserver string) ([]string, error) { 216 | c := dns.Client{} 217 | m := dns.Msg{} 218 | 219 | domain = dns.Fqdn(domain) 220 | 221 | m.SetQuestion(domain, dns.TypeNS) 222 | r, _, err := c.Exchange(&m, nameserver+":53") 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | var recordData []string 228 | if r.Rcode == dns.RcodeSuccess { 229 | if len(r.Answer) > 0 { 230 | recordData = parseNS(r.Answer) 231 | } else { 232 | // if no NS records are found, fallback to using the authority section 233 | recordData = parseNS(r.Ns) 234 | } 235 | } else { 236 | return nil, fmt.Errorf("failed to get authoritative servers; Rcode: %d", r.Rcode) 237 | } 238 | 239 | return recordData, nil 240 | } 241 | 242 | // authorityReturnRefusedOrServfail returns true if at least one of the domain's authoritative nameservers 243 | // returns a REFUSED/SERVFAIL response when queried for the domain 244 | func authorityReturnRefusedOrServfail(domain string) (bool, error) { 245 | // EffectiveTLDPlusOne considers the root domain "." an additional TLD 246 | // so for "example.com.", it returns "com." 247 | // but for "example.com" (without trailing "."), it returns "example.com" 248 | // so we use unFqdn() to remove the trailing dot 249 | apex, err := publicsuffix.EffectiveTLDPlusOne(unFqdn(domain)) 250 | if err != nil { 251 | return false, err 252 | } 253 | 254 | apexAuthority, err := getAuthorityForDomain(apex, "1.1.1.1") 255 | if err != nil { 256 | return false, err 257 | } 258 | if len(apexAuthority) == 0 { 259 | return false, fmt.Errorf("couldn't find the apex's nameservers") 260 | } 261 | 262 | domainAuthority, err := getAuthorityForDomain(domain, apexAuthority[0]) 263 | if err != nil { 264 | return false, err 265 | } 266 | 267 | for _, nameserver := range domainAuthority { 268 | vulnerable, err := nameserverReturnsRefusedOrServfail(domain, nameserver) 269 | if err != nil { 270 | // TODO: report this kind of error to the caller? 271 | continue 272 | } 273 | if vulnerable { 274 | return true, nil 275 | } 276 | } 277 | return false, nil 278 | } 279 | 280 | // nameserverReturnsRefusedOrServfail returns true if the given nameserver 281 | // returns a REFUSED/SERVFAIL response when queried for the domain 282 | func nameserverReturnsRefusedOrServfail(domain string, nameserver string) (bool, error) { 283 | client := dns.Client{} 284 | message := dns.Msg{} 285 | 286 | message.SetQuestion(dns.Fqdn(domain), dns.TypeA) 287 | r, _, err := client.Exchange(&message, nameserver+":53") 288 | if err != nil { 289 | return false, err 290 | } 291 | if r.Rcode == dns.RcodeServerFailure || r.Rcode == dns.RcodeRefused { 292 | return true, nil 293 | } 294 | return false, nil 295 | } 296 | 297 | //Now, for each entry in the data providers file, we will check to see if the output 298 | //from the dig command against the current domain matches the CNAME for that data provider 299 | //if it matches the CNAME, we need to now check if it matches the string for that data provider 300 | //So, we curl it and see if it matches. At this point, we know its vulnerable 301 | func checkCnameAgainstProviders(domain string, cname string, cmsRecords []*CMS, config Configuration) []DomainScan { 302 | transport := &http.Transport{ 303 | Dial: (&net.Dialer{Timeout: 10 * time.Second}).Dial, 304 | TLSHandshakeTimeout: 10 * time.Second, 305 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} 306 | 307 | client := &http.Client{Transport: transport, Timeout: time.Duration(10 * time.Second)} 308 | var scanResults []DomainScan 309 | 310 | for _, cmsRecord := range cmsRecords { 311 | usesprovider, _ := regexp.MatchString(cmsRecord.CName, cname) 312 | if usesprovider { 313 | scanResult := evaluateDomainProvider(domain, cname, cmsRecord, client, config) 314 | scanResults = append(scanResults, scanResult) 315 | } 316 | } 317 | return scanResults 318 | } 319 | 320 | //If there is a CNAME and can't curl it, we will assume its vulnerable 321 | //If we can curl it, we will regex match the string obtained in the response with 322 | //the string specified in the data providers file to see if its vulnerable or not 323 | func evaluateDomainProvider(domain string, cname string, cmsRecord *CMS, client *http.Client, config Configuration) DomainScan { 324 | scanResult := DomainScan{Domain: domain, Cname: cname, 325 | IsVulnerable: false, Provider: cmsRecord.Name} 326 | 327 | httpResponse, err := client.Get(fmt.Sprintf("http://%s", scanResult.Domain)) 328 | httpsResponse, err1 := client.Get(fmt.Sprintf("https://%s", scanResult.Domain)) 329 | 330 | if err != nil && err1 != nil { 331 | if *config.deadRecordCheck { 332 | scanResult.IsVulnerable = true 333 | scanResult.Response = "Can't CURL it but dig shows a dead DNS record" 334 | } 335 | } else if err == nil { 336 | text, err := ioutil.ReadAll(httpResponse.Body) 337 | text2, err2 := ioutil.ReadAll(httpsResponse.Body) 338 | if err != nil && err2 != nil { 339 | scanResult.Response = err.Error() 340 | } else { 341 | _, err := regexp.MatchString(cmsRecord.String, string(text)) 342 | _, err2 := regexp.MatchString(cmsRecord.String, string(text2)) 343 | if err != nil && err2 != nil { 344 | scanResult.Response = err.Error() 345 | } else { 346 | scanResult.IsVulnerable = true 347 | scanResult.Response = cmsRecord.String 348 | } 349 | } 350 | } 351 | return scanResult 352 | } 353 | 354 | func loadProviders(recordsFilePath string) []*CMS { 355 | clientsFile, err := os.OpenFile(recordsFilePath, os.O_RDWR|os.O_CREATE, os.ModePerm) 356 | panicOnError(err) 357 | defer clientsFile.Close() 358 | 359 | cmsRecords := []*CMS{} 360 | err = gocsv.UnmarshalFile(clientsFile, &cmsRecords) 361 | panicOnError(err) 362 | return cmsRecords 363 | } 364 | 365 | func writeResultsToCsv(scanResults []DomainScan, outputFilePath string) { 366 | outputFile, err := os.Create(outputFilePath) 367 | panicOnError(err) 368 | defer outputFile.Close() 369 | 370 | err = gocsv.MarshalFile(&scanResults, outputFile) 371 | panicOnError(err) 372 | } 373 | 374 | func printResults(scanResults []DomainScan) { 375 | table := tablewriter.NewWriter(os.Stdout) 376 | table.SetHeader([]string{"Domain", "Cname", "Provider", "Vulnerable", "Response"}) 377 | 378 | for _, scanResult := range scanResults { 379 | if (len(scanResult.Cname) > 0 && len(scanResult.Provider) > 0) || len(scanResult.Response) > 0 { 380 | table.Append([]string{scanResult.Domain, scanResult.Cname, scanResult.Provider, 381 | strconv.FormatBool(scanResult.IsVulnerable), 382 | scanResult.Response}) 383 | } 384 | } 385 | table.Render() 386 | } 387 | --------------------------------------------------------------------------------