├── 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 |
--------------------------------------------------------------------------------