├── .gitignore ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── domains.txt ├── providers-data.csv └── tko-subs.go /.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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 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. 10 | 11 | 12 | ### Disclaimer: DONT BE A JERK! 13 | 14 | Needless to mention, please use this tool very very carefully. The authors won't be responsible for any consequences. 15 | 16 | 17 | ### Pre-requisites 18 | 19 | We need GO installed. Once you have GO, just type `go get github.com/anshumanbh/tko-subs` to download the tool. 20 | 21 | Once the tool is downloaded, type `tko-subs -h`. 22 | 23 | 24 | ### How to run? 25 | 26 | Once you have everything installed, `cd` into the directory and type: 27 | `tko-subs -domains=domains.txt -data=providers-data.csv -output=output.csv` 28 | 29 | If you just want to check for a single domain, type: 30 | `tko-subs -domain ` 31 | 32 | If you just want to check for multiple domains, type: 33 | `tko-subs -domain ,` 34 | 35 | By default: 36 | * the `domains` flag is set to `domains.txt` 37 | * the `data` flag is set to `providers-data.csv` 38 | * the `output` flag is set to `output.csv` 39 | * 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 40 | * the `threads` flag is set to `5` 41 | 42 | So, simply running `tko-subs` would run with the default values mentioned above. 43 | 44 | 45 | ### How is providers-data.csv formatted? 46 | 47 | name,cname,string,http 48 | 49 | * name: The name of the provider (e.g. github) 50 | * cname: The CNAME used to map a website to the provider's content (e.g. github.io) 51 | * string: The error message returned for an unclaimed subdomain (e.g. "There isn't a GitHub Pages site here") 52 | * http: Whether to use http (not https, which is the default) to connect to the site (true/false) 53 | 54 | 55 | ### How is the output formatted? 56 | 57 | Domain,CNAME,Provider,IsVulnerable,Response 58 | 59 | * Domain: The domain checked 60 | * CNAME: The CNAME of the domain 61 | * Provider: The provider the domain was found to be using 62 | * IsVulnerable: Whether the domain was found to be vulnerable or not (true/false) 63 | * Response: The message that the subdomain was checked against 64 | 65 | If a dead DNS record is found, `Provider` is left empty. 66 | If a misbehaving nameserver is found, `Provider` and `CNAME` are left empty 67 | 68 | ### What is going on under the hood? 69 | 70 | This will iterate over all the domains (concurrently using GoRoutines) in the `subdomains.txt` file and: 71 | * See if they have a misbehaving authoritative nameserver; if they do, we mark that domain as vulnerable. 72 | * See if they have dangling CNAME records aka dead DNS records; if they do we mark that domain as vulnerable. 73 | * 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. 74 | * If the response matches, we mark that domain as vulnerable. 75 | * And, that's it! 76 | 77 | 78 | ### Future Work 79 | 80 | * ~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 81 | * Add more CMS providers 82 | 83 | 84 | ### Credits 85 | 86 | * Thanks to Luke Young (@TheBoredEng) for helping me out with the go-github library. 87 | * Thanks to Frans Rosen (@fransrosen) for helping me understand the technical details that are required for some of the takeovers. 88 | * Thanks to Mohammed Diaa (@mhmdiaa) for taking time to implement the provider data functionality and getting the code going. 89 | * Thanks to high-stakes for a much needed code refresh. 90 | 91 | 92 | ### Changelog 93 | `8/4/18` 94 | * Added more services 95 | * Added CNAME recursion 96 | * Removed takeover functionality 97 | 98 | `5/27` 99 | * Added new Dockerfile reducing the size of the image 100 | * Added sample domains.txt file to test against 101 | * mhmdiaa added the logic for dead DNS takeovers. Updated documentation. Thanks a lot! 102 | 103 | `11/6` 104 | * high-stakes issues a PR with a bunch of new code that fixes a few bugs and makes the code cleaner 105 | 106 | `9/22` 107 | * Added an optional flag to check for single domain 108 | * Made it easier to install and run 109 | 110 | `6/25` 111 | * Made the code much more faster by implementing goroutines 112 | * 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!! 113 | 114 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 73 | zendesk,zendesk.com,Help Center Closed 74 | -------------------------------------------------------------------------------- /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 *config.deadRecordCheck { 143 | if exists, err := resolves(cname); !exists { 144 | scanResult := DomainScan{Domain: domain, Cname: cname, IsVulnerable: true, Response: "Dead DNS record"} 145 | return []DomainScan{scanResult}, nil 146 | } else if err != nil { 147 | return nil, err 148 | } 149 | } 150 | 151 | scanResults := checkCnameAgainstProviders(domain, cname, cmsRecords, config) 152 | if len(scanResults) == 0 { 153 | err = errors.New(fmt.Sprintf("Cname [%s] found but could not determine provider", cname)) 154 | } 155 | return scanResults, err 156 | } 157 | 158 | // resolves function returns false if NXDOMAIN, and true otherwise 159 | func resolves(domain string) (bool, error) { 160 | client := dns.Client{} 161 | message := dns.Msg{} 162 | 163 | message.SetQuestion(dns.Fqdn(domain), dns.TypeA) 164 | r, _, err := client.Exchange(&message, "1.1.1.1:53") 165 | if err != nil { 166 | return false, err 167 | } 168 | if r.Rcode == dns.RcodeNameError { 169 | return false, nil 170 | } 171 | return true, nil 172 | } 173 | 174 | // getCnameForDomain function to lookup CNAME records of a domain 175 | // 176 | // Doing CNAME lookups using GOLANG's net package or for that matter just doing a host on a domain 177 | // does not necessarily let us know about any dead DNS records. So, we need to read the raw DNS response 178 | // to properly figure out if there are any dead DNS records 179 | func getCnameForDomain(domain string) (string, error) { 180 | c := dns.Client{} 181 | m := dns.Msg{} 182 | 183 | m.SetQuestion(dns.Fqdn(domain), dns.TypeCNAME) 184 | m.RecursionDesired = true 185 | 186 | r, _, err := c.Exchange(&m, "1.1.1.1:53") 187 | if err != nil { 188 | return "", err 189 | } 190 | 191 | if len(r.Answer) > 0 { 192 | record := r.Answer[0].(*dns.CNAME) 193 | cname := record.Target 194 | return cname, errors.New("Recursion detected") 195 | } else { 196 | return domain, nil 197 | } 198 | return "", errors.New("Cname not found") 199 | } 200 | 201 | // function parseNS to parse NS records (found in answer to NS query or in the authority section) into a list of record values 202 | func parseNS(records []dns.RR) []string { 203 | var recordData []string 204 | for _, ans := range records { 205 | if ans.Header().Rrtype == dns.TypeNS { 206 | record := ans.(*dns.NS) 207 | recordData = append(recordData, record.Ns) 208 | } else if ans.Header().Rrtype == dns.TypeSOA { 209 | record := ans.(*dns.SOA) 210 | recordData = append(recordData, record.Ns) 211 | } 212 | } 213 | return recordData 214 | } 215 | 216 | // getAuthorityForDomain function to lookup the authoritative nameservers of a domain 217 | func getAuthorityForDomain(domain string, nameserver string) ([]string, error) { 218 | c := dns.Client{} 219 | m := dns.Msg{} 220 | 221 | domain = dns.Fqdn(domain) 222 | 223 | m.SetQuestion(domain, dns.TypeNS) 224 | r, _, err := c.Exchange(&m, nameserver+":53") 225 | if err != nil { 226 | return nil, err 227 | } 228 | 229 | var recordData []string 230 | if r.Rcode == dns.RcodeSuccess { 231 | if len(r.Answer) > 0 { 232 | recordData = parseNS(r.Answer) 233 | } else { 234 | // if no NS records are found, fallback to using the authority section 235 | recordData = parseNS(r.Ns) 236 | } 237 | } else { 238 | return nil, fmt.Errorf("failed to get authoritative servers; Rcode: %d", r.Rcode) 239 | } 240 | 241 | return recordData, nil 242 | } 243 | 244 | // authorityReturnRefusedOrServfail returns true if at least one of the domain's authoritative nameservers 245 | // returns a REFUSED/SERVFAIL response when queried for the domain 246 | func authorityReturnRefusedOrServfail(domain string) (bool, error) { 247 | // EffectiveTLDPlusOne considers the root domain "." an additional TLD 248 | // so for "example.com.", it returns "com." 249 | // but for "example.com" (without trailing "."), it returns "example.com" 250 | // so we use unFqdn() to remove the trailing dot 251 | apex, err := publicsuffix.EffectiveTLDPlusOne(unFqdn(domain)) 252 | if err != nil { 253 | return false, err 254 | } 255 | 256 | apexAuthority, err := getAuthorityForDomain(apex, "1.1.1.1") 257 | if err != nil { 258 | return false, err 259 | } 260 | if len(apexAuthority) == 0 { 261 | return false, fmt.Errorf("couldn't find the apex's nameservers") 262 | } 263 | 264 | domainAuthority, err := getAuthorityForDomain(domain, apexAuthority[0]) 265 | if err != nil { 266 | return false, err 267 | } 268 | 269 | for _, nameserver := range domainAuthority { 270 | vulnerable, err := nameserverReturnsRefusedOrServfail(domain, nameserver) 271 | if err != nil { 272 | // TODO: report this kind of error to the caller? 273 | continue 274 | } 275 | if vulnerable { 276 | return true, nil 277 | } 278 | } 279 | return false, nil 280 | } 281 | 282 | // nameserverReturnsRefusedOrServfail returns true if the given nameserver 283 | // returns a REFUSED/SERVFAIL response when queried for the domain 284 | func nameserverReturnsRefusedOrServfail(domain string, nameserver string) (bool, error) { 285 | client := dns.Client{} 286 | message := dns.Msg{} 287 | 288 | message.SetQuestion(dns.Fqdn(domain), dns.TypeA) 289 | r, _, err := client.Exchange(&message, nameserver+":53") 290 | if err != nil { 291 | return false, err 292 | } 293 | if r.Rcode == dns.RcodeServerFailure || r.Rcode == dns.RcodeRefused { 294 | return true, nil 295 | } 296 | return false, nil 297 | } 298 | 299 | //Now, for each entry in the data providers file, we will check to see if the output 300 | //from the dig command against the current domain matches the CNAME for that data provider 301 | //if it matches the CNAME, we need to now check if it matches the string for that data provider 302 | //So, we curl it and see if it matches. At this point, we know its vulnerable 303 | func checkCnameAgainstProviders(domain string, cname string, cmsRecords []*CMS, config Configuration) []DomainScan { 304 | transport := &http.Transport{ 305 | Dial: (&net.Dialer{Timeout: 10 * time.Second}).Dial, 306 | TLSHandshakeTimeout: 10 * time.Second, 307 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} 308 | 309 | client := &http.Client{Transport: transport, Timeout: time.Duration(10 * time.Second)} 310 | var scanResults []DomainScan 311 | 312 | for _, cmsRecord := range cmsRecords { 313 | usesprovider, _ := regexp.MatchString(cmsRecord.CName, cname) 314 | if usesprovider { 315 | res, str := evaluateDomainProvider(domain, cname, cmsRecord, client, config) 316 | if res { 317 | scanResult := DomainScan{Domain: domain, Cname: cname, IsVulnerable: res, Provider: cmsRecord.CName, Response: str} 318 | scanResults = append(scanResults, scanResult) 319 | } 320 | } 321 | } 322 | return scanResults 323 | } 324 | 325 | //If there is a CNAME and can't curl it, we will assume its vulnerable 326 | //If we can curl it, we will regex match the string obtained in the response with 327 | //the string specified in the data providers file to see if its vulnerable or not 328 | func evaluateDomainProvider(domain string, cname string, cmsRecord *CMS, client *http.Client, config Configuration) (bool, string) { 329 | httpResponse, err := client.Get(fmt.Sprintf("http://%s", domain)) 330 | httpsResponse, err1 := client.Get(fmt.Sprintf("https://%s", domain)) 331 | 332 | if err != nil && err1 != nil { 333 | if *config.deadRecordCheck { 334 | return true, "Can't CURL it but dig shows a dead DNS record" 335 | } 336 | } else if err == nil && err1 == nil { 337 | text, err := ioutil.ReadAll(httpResponse.Body) 338 | text2, err2 := ioutil.ReadAll(httpsResponse.Body) 339 | if err != nil && err2 != nil { 340 | return false, err.Error() 341 | } else { 342 | x, err := regexp.MatchString(cmsRecord.String, string(text)) 343 | y, err2 := regexp.MatchString(cmsRecord.String, string(text2)) 344 | if err != nil && err2 != nil { 345 | return false, err.Error() 346 | } 347 | if x && y { 348 | return true, cmsRecord.String 349 | } 350 | } 351 | } 352 | return false, "nope" 353 | } 354 | 355 | func loadProviders(recordsFilePath string) []*CMS { 356 | clientsFile, err := os.OpenFile(recordsFilePath, os.O_RDWR|os.O_CREATE, os.ModePerm) 357 | panicOnError(err) 358 | defer clientsFile.Close() 359 | 360 | cmsRecords := []*CMS{} 361 | err = gocsv.UnmarshalFile(clientsFile, &cmsRecords) 362 | panicOnError(err) 363 | return cmsRecords 364 | } 365 | 366 | func writeResultsToCsv(scanResults []DomainScan, outputFilePath string) { 367 | outputFile, err := os.Create(outputFilePath) 368 | panicOnError(err) 369 | defer outputFile.Close() 370 | 371 | err = gocsv.MarshalFile(&scanResults, outputFile) 372 | panicOnError(err) 373 | } 374 | 375 | func printResults(scanResults []DomainScan) { 376 | table := tablewriter.NewWriter(os.Stdout) 377 | table.SetHeader([]string{"Domain", "Cname", "Provider", "Vulnerable", "Response"}) 378 | 379 | for _, scanResult := range scanResults { 380 | if (len(scanResult.Cname) > 0 && len(scanResult.Provider) > 0) || len(scanResult.Response) > 0 { 381 | table.Append([]string{scanResult.Domain, scanResult.Cname, scanResult.Provider, 382 | strconv.FormatBool(scanResult.IsVulnerable), 383 | scanResult.Response}) 384 | } 385 | } 386 | table.Render() 387 | } 388 | --------------------------------------------------------------------------------