├── .gitignore ├── Dockerfile ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── altdns ├── altdns.go └── altdns_test.go ├── main.go ├── util └── util.go ├── words.txt └── words2.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Binary 15 | goaltdns -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Container 2 | FROM golang:1.11.4-alpine3.7 AS build-env 3 | RUN apk add --no-cache --upgrade git openssh-client ca-certificates 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 ./goaltdns *.go 14 | 15 | # Final Container 16 | FROM alpine:3.7 17 | COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 18 | COPY --from=build-env /go/src/app/goaltdns /usr/bin/goaltdns 19 | ADD words.txt /altdns/words.txt 20 | ADD words2.txt /altdns/words2.txt 21 | ENTRYPOINT ["/usr/bin/goaltdns"] 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 | digest = "1:0e29b30c96f33bc5668f44af752f255a80bdd073e7f0b9b69eaeab3bc1a8c76c" 7 | name = "github.com/bobesa/go-domain-util" 8 | packages = ["domainutil"] 9 | pruneopts = "UT" 10 | revision = "1d708c097a6a59a3e6432affc53cf41808b35c8b" 11 | 12 | [[projects]] 13 | branch = "master" 14 | digest = "1:82491a0790e0945762096c875e2e14932a5d7b37a7ce3e83ef3b50437a71e09a" 15 | name = "golang.org/x/net" 16 | packages = ["idna"] 17 | pruneopts = "UT" 18 | revision = "fe579d43d83210096a79b46dcca0e3721058393a" 19 | 20 | [[projects]] 21 | digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" 22 | name = "golang.org/x/text" 23 | packages = [ 24 | "collate", 25 | "collate/build", 26 | "internal/colltab", 27 | "internal/gen", 28 | "internal/tag", 29 | "internal/triegen", 30 | "internal/ucd", 31 | "language", 32 | "secure/bidirule", 33 | "transform", 34 | "unicode/bidi", 35 | "unicode/cldr", 36 | "unicode/norm", 37 | "unicode/rangetable", 38 | ] 39 | pruneopts = "UT" 40 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 41 | version = "v0.3.0" 42 | 43 | [solve-meta] 44 | analyzer-name = "dep" 45 | analyzer-version = 1 46 | input-imports = ["github.com/bobesa/go-domain-util/domainutil"] 47 | solver-name = "gps-cdcl" 48 | solver-version = 1 49 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | [[constraint]] 2 | branch = "master" 3 | name = "github.com/bobesa/go-domain-util" 4 | 5 | [prune] 6 | go-tests = true 7 | unused-packages = true 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SubFinder Team 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 | # GoAltdns 2 | [![License](https://img.shields.io/badge/license-MIT-_red.svg)](https://opensource.org/licenses/MIT) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/subfinder/goaltdns)](https://goreportcard.com/report/github.com/subfinder/goaltdns) 4 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/subfinder/goaltdns/issues) 5 | 6 | GoAltdns is a permutation generation tool that can take a list of subdomains, permute them using a wordlist, insert indexes, numbers, dashes and increase your chance of finding that estoeric subdomain that no-one found during bug-bounty or pentest. It uses a number of techniques to accomplish this. It can allow for discovery of subdomains that conform to patterns. GoAltdns takes in words that could be present in subdomains under a domain (such as test, dev, staging) as well as takes in a list of subdomains that you know of. 7 | 8 | The tool itself is very simple and is built with golang concurrency providing it very quick execution times. 9 | 10 | # Installation Instructions 11 | 12 | The installation is easy. Just `go get` the repo. 13 | 14 | ```bash 15 | go get github.com/subfinder/goaltdns 16 | ``` 17 | 18 | Note - You need to copy the words.txt file into the same directory as the tool or specify it's location via the -w flag. 19 | 20 | ## Upgrading 21 | If you wish to upgrade the package you can use: 22 | 23 | ```bash 24 | go get -u github.com/subfinder/goaltdns 25 | ``` 26 | 27 | # Usage 28 | 29 | GoAltdns can read hosts directly from standard input, or either take a single host as argument, or a list of hosts. To provide a single host, you can use the `-h` option. In order to provide a list of hosts, you can use the `-l` option. 30 | 31 | Sample run: 32 | 33 | ```bash 34 | ice3man@TheDaemon:~/tmp/goaltdns$ ./altdns -host phabricator.freelancer.com 35 | 1phabricator.freelancer.com 36 | phabricator1.freelancer.com 37 | 10phabricator.freelancer.com 38 | 1-phabricator.freelancer.com 39 | phabricator10.freelancer.com 40 | phabricator-0.freelancer.com 41 | 1.phabricator.freelancer.com 42 | ... 43 | ``` 44 | 45 | Sample run reading from stdin: 46 | 47 | ```bash 48 | ice3man@TheDaemon:~/tmp/goaltdns$ echo phabricator.freelancer.com | ./altdns 49 | 1phabricator.freelancer.com 50 | phabricator1.freelancer.com 51 | 10phabricator.freelancer.com 52 | 1-phabricator.freelancer.com 53 | phabricator10.freelancer.com 54 | phabricator-0.freelancer.com 55 | 1.phabricator.freelancer.com 56 | ... 57 | ``` 58 | 59 | You can pass custom wordlists using the -w option. Currently, it uses words.txt taken from [here](https://github.com/haccer/altdns/blob/master/words.txt). 60 | 61 | By default, goaltdns writes to the standard output. If you want to save the results to a file, you can use `-o` flag with the name of then file to write to it. 62 | 63 | ```bash 64 | ice3man@TheDaemon:~/tmp/goaltdns$ ./altdns -l ~/uberinternal -o output.txt 65 | ``` 66 | 67 | This will render a blank console but the tool will still write to the output file. 68 | 69 | # License 70 | 71 | GoAltdns is made with 🖤 by [Subfinder](https://github.com/subfinder) team. 72 | 73 | See the **License** file for more details. 74 | 75 | # Thanks 76 | 77 | GoAltdns is heavily inspired from original [altdns](https://github.com/infosec-au/altdns) by @infosec_au and @nnwakelam. Thanks to them and their awesome research. Also, the wordlist is taken from [haccer](https://github.com/haccer/) 78 | -------------------------------------------------------------------------------- /altdns/altdns.go: -------------------------------------------------------------------------------- 1 | package altdns 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | "strconv" 9 | "sync" 10 | ) 11 | 12 | var ( 13 | nbrRe = regexp.MustCompile("[0-9]+") 14 | ) 15 | 16 | // AltDNS holds words, etc 17 | type AltDNS struct { 18 | PermutationWords []string 19 | } 20 | 21 | func (a *AltDNS) insertDashes(domain string, results chan string) { 22 | for _, w := range a.PermutationWords { 23 | if w == "" || domain == "" { 24 | continue 25 | } 26 | // prefixes 27 | results <- fmt.Sprint(w + "-" + domain) 28 | // suffixes 29 | results <- fmt.Sprint(domain + "-" + w) 30 | } 31 | 32 | for i, rune := range domain { 33 | if rune == '.' { 34 | for _, w := range a.PermutationWords { 35 | results <- fmt.Sprint(domain[:i] + "." + w + "-" + domain[i+1:]) 36 | results <- fmt.Sprintf(domain[:i] + "-" + w + domain[i:]) 37 | } 38 | } 39 | } 40 | } 41 | 42 | func (a *AltDNS) insertIndexes(domain string, results chan string) { 43 | for _, w := range a.PermutationWords { 44 | if w == "" || domain == "" { 45 | continue 46 | } 47 | // prefixes 48 | results <- fmt.Sprint(w + "." + domain) 49 | // suffixes 50 | results <- fmt.Sprint(domain + "." + w) 51 | } 52 | 53 | for i, rune := range domain { 54 | if rune == '.' { 55 | for _, w := range a.PermutationWords { 56 | results <- fmt.Sprint(domain[:i] + "." + w + domain[i:]) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func (a *AltDNS) insertNumberSuffixes(domain string, results chan string) { 63 | if domain != "" { 64 | for j := 0; j < 10; j++ { 65 | // suffixes 66 | results <- fmt.Sprintf("%s-%d", domain, j) 67 | } 68 | } 69 | 70 | for i, rune := range domain { 71 | if rune == '.' { 72 | for j := 0; j < 10; j++ { 73 | results <- fmt.Sprintf("%s-%d%s", domain[:i], j, domain[i:]) 74 | results <- fmt.Sprintf("%s%d%s", domain[:i], j, domain[i:]) 75 | } 76 | } 77 | } 78 | } 79 | 80 | func (a *AltDNS) insertWordsSubdomains(domain string, results chan string) { 81 | for _, w := range a.PermutationWords { 82 | // prefixes 83 | results <- fmt.Sprint(w + domain) 84 | // suffixes 85 | results <- fmt.Sprint(domain + w) 86 | } 87 | 88 | for i, rune := range domain { 89 | if rune == '.' { 90 | for _, w := range a.PermutationWords { 91 | results <- fmt.Sprint(domain[:i] + w + domain[i:]) 92 | results <- fmt.Sprint(domain[:i] + "." + w + domain[i+1:]) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func (a *AltDNS) expandNumbers(domain string, results chan string) { 99 | for _, ind := range nbrRe.FindAllStringIndex(domain, -1) { 100 | padSize := strconv.Itoa(ind[1] - ind[0]) 101 | for i := 1; i <= 10; i++ { 102 | results <- fmt.Sprintf("%s%0"+padSize+"d%s", domain[:ind[0]], i, domain[ind[1]:]) 103 | } 104 | } 105 | } 106 | 107 | // New Returns a new altdns object 108 | func New(wordList string) (*AltDNS, error) { 109 | altdns := AltDNS{} 110 | 111 | f, err := os.Open(wordList) 112 | if err != nil { 113 | return &altdns, err 114 | } 115 | 116 | scanner := bufio.NewScanner(f) 117 | 118 | for scanner.Scan() { 119 | altdns.PermutationWords = append(altdns.PermutationWords, scanner.Text()) 120 | } 121 | 122 | return &altdns, nil 123 | } 124 | 125 | // Permute permutes a given domain and sends output on a channel 126 | func (a *AltDNS) Permute(domain string) chan string { 127 | wg := sync.WaitGroup{} 128 | results := make(chan string) 129 | 130 | go func(domain string) { 131 | defer close(results) 132 | 133 | // Insert all indexes 134 | wg.Add(1) 135 | go func(domain string, results chan string) { 136 | defer wg.Done() 137 | a.insertIndexes(domain, results) 138 | }(domain, results) 139 | 140 | // Insert all dash 141 | wg.Add(1) 142 | go func(domain string, results chan string) { 143 | defer wg.Done() 144 | a.insertDashes(domain, results) 145 | }(domain, results) 146 | 147 | // Insert Number Suffix Subdomains 148 | wg.Add(1) 149 | go func(domain string, results chan string) { 150 | defer wg.Done() 151 | a.insertNumberSuffixes(domain, results) 152 | }(domain, results) 153 | 154 | // Join Words Subdomains 155 | wg.Add(1) 156 | go func(domain string, results chan string) { 157 | defer wg.Done() 158 | a.insertWordsSubdomains(domain, results) 159 | }(domain, results) 160 | 161 | // Permute numbers 0x -> 01, 02, 03, ... 162 | wg.Add(1) 163 | go func(domain string, results chan string) { 164 | defer wg.Done() 165 | a.expandNumbers(domain, results) 166 | }(domain, results) 167 | 168 | wg.Wait() 169 | }(domain) 170 | 171 | return results 172 | } 173 | 174 | // Permutations permutes a given domain and return a list of permutations 175 | func (a *AltDNS) Permutations(domain string) (permutations []string) { 176 | uniq := make(map[string]bool) 177 | for r := range a.Permute(domain) { 178 | // avoid duplicates 179 | if _, ok := uniq[r]; ok { 180 | continue 181 | } 182 | 183 | uniq[r] = true 184 | permutations = append(permutations, r) 185 | } 186 | return permutations 187 | } 188 | -------------------------------------------------------------------------------- /altdns/altdns_test.go: -------------------------------------------------------------------------------- 1 | package altdns 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/bobesa/go-domain-util/domainutil" 9 | ) 10 | 11 | func TestPermute(t *testing.T) { 12 | urls := []string{"abc.xyz.freelancer.com", "aa.bb.cc"} 13 | 14 | altdns, _ := New("words.txt") 15 | 16 | jobs := sync.WaitGroup{} 17 | 18 | for _, u := range urls { 19 | subdomain := domainutil.Subdomain(u) 20 | domainSuffix := domainutil.Domain(u) 21 | jobs.Add(1) 22 | go func(domain string) { 23 | defer jobs.Done() 24 | for r := range altdns.Permute(subdomain) { 25 | fmt.Printf("%s.%s\n", r, domainSuffix) 26 | } 27 | }(u) 28 | } 29 | 30 | jobs.Wait() 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "sync" 9 | 10 | "github.com/bobesa/go-domain-util/domainutil" 11 | "github.com/subfinder/goaltdns/altdns" 12 | "github.com/subfinder/goaltdns/util" 13 | ) 14 | 15 | func main() { 16 | var wordlist, host, list, output string 17 | hostList := []string{} 18 | flag.StringVar(&host, "h", "", "Host to generate permutations for") 19 | flag.StringVar(&list, "l", "", "List of hosts to generate permutations for") 20 | flag.StringVar(&wordlist, "w", "words.txt", "Wordlist to generate permutations with") 21 | flag.StringVar(&output, "o", "", "File to write permutation output to (optional)") 22 | 23 | flag.Parse() 24 | 25 | if host == "" && list == "" && !util.PipeGiven() { 26 | fmt.Printf("%s: no host/hosts specified!\n", os.Args[0]) 27 | os.Exit(1) 28 | } 29 | 30 | if host != "" { 31 | hostList = append(hostList, host) 32 | } 33 | 34 | if list != "" { 35 | hostList = append(hostList, util.LinesInFile(list)...) 36 | } 37 | 38 | if util.PipeGiven() { 39 | hostList = append(hostList, util.LinesInStdin()...) 40 | } 41 | 42 | var f *os.File 43 | var err error 44 | if output != "" { 45 | f, err = os.OpenFile(output, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600) 46 | if err != nil { 47 | fmt.Printf("output: %s\n", err) 48 | os.Exit(1) 49 | } 50 | 51 | defer f.Close() 52 | } 53 | 54 | altdns, err := altdns.New(wordlist) 55 | if err != nil { 56 | fmt.Printf("wordlist: %s\n", err) 57 | os.Exit(1) 58 | } 59 | 60 | writerJob := sync.WaitGroup{} 61 | 62 | writequeue := make(chan string) 63 | 64 | writerJob.Add(1) 65 | go func() { 66 | defer writerJob.Done() 67 | 68 | w := bufio.NewWriter(f) 69 | defer w.Flush() 70 | 71 | for permutation := range writequeue { 72 | w.WriteString(permutation) 73 | } 74 | }() 75 | 76 | jobs := sync.WaitGroup{} 77 | 78 | for _, u := range hostList { 79 | subdomain := domainutil.Subdomain(u) 80 | domainSuffix := domainutil.Domain(u) 81 | jobs.Add(1) 82 | go func(domain string) { 83 | defer jobs.Done() 84 | uniq := make(map[string]bool) 85 | for r := range altdns.Permute(subdomain) { 86 | permutation := fmt.Sprintf("%s.%s\n", r, domainSuffix) 87 | 88 | // avoid duplicates 89 | if _, ok := uniq[permutation]; ok { 90 | continue 91 | } 92 | 93 | uniq[permutation] = true 94 | 95 | if output == "" { 96 | fmt.Printf("%s", permutation) 97 | } else { 98 | writequeue <- permutation 99 | } 100 | } 101 | }(u) 102 | } 103 | 104 | jobs.Wait() 105 | 106 | close(writequeue) 107 | 108 | writerJob.Wait() 109 | } 110 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | ) 7 | 8 | // PipeGiven checks if command is piped 9 | func PipeGiven() bool { 10 | fi, _ := os.Stdin.Stat() 11 | return (fi.Mode() & os.ModeCharDevice) == 0 12 | } 13 | 14 | // LinesInFile return all lines from the given file 15 | func LinesInFile(fileName string) []string { 16 | f, _ := os.Open(fileName) 17 | scanner := bufio.NewScanner(f) 18 | return readLines(scanner) 19 | } 20 | 21 | // LinesInStdin return all lines from stdin 22 | func LinesInStdin() []string { 23 | scanner := bufio.NewScanner(os.Stdin) 24 | return readLines(scanner) 25 | } 26 | 27 | func readLines(scanner *bufio.Scanner) (result []string) { 28 | for scanner.Scan() { 29 | line := scanner.Text() 30 | result = append(result, line) 31 | } 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /words.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 10 3 | 11 4 | 12 5 | 13 6 | 14 7 | 15 8 | 16 9 | 17 10 | 18 11 | 19 12 | 2 13 | 20 14 | 2009 15 | 2010 16 | 2011 17 | 2012 18 | 2013 19 | 2014 20 | 2015 21 | 2016 22 | 3 23 | 4 24 | 5 25 | 6 26 | 7 27 | 8 28 | 9 29 | a 30 | acc 31 | accounts 32 | admin 33 | admin1 34 | administrator 35 | akali 36 | akamai 37 | alpha 38 | alt 39 | america 40 | analytics 41 | api 42 | api1 43 | api-docs 44 | apollo 45 | april 46 | aws 47 | b 48 | backend 49 | beta 50 | billing 51 | boards 52 | box 53 | brand 54 | brasil 55 | brazil 56 | bucket 57 | bucky 58 | c 59 | cdn 60 | cf 61 | chef 62 | ci 63 | client 64 | cloudfront 65 | cms 66 | cms1 67 | cn 68 | com 69 | confluence 70 | container 71 | control 72 | data 73 | dec 74 | demo 75 | dev 76 | dev1 77 | developer 78 | devops 79 | docker 80 | docs 81 | drop 82 | edge 83 | elasticbeanstalk 84 | elb 85 | email 86 | eng 87 | engima 88 | engine 89 | engineering 90 | eu 91 | europe 92 | europewest 93 | euw 94 | euwe 95 | evelynn 96 | events 97 | feb 98 | firewall 99 | forms 100 | forum 101 | frontpage 102 | fw 103 | games 104 | germany 105 | gh 106 | ghcpi 107 | git 108 | github 109 | global 110 | hkg 111 | hw 112 | hwcdn 113 | i 114 | ids 115 | int 116 | internal 117 | jenkins 118 | jinx 119 | july 120 | june 121 | kor 122 | korea 123 | kr 124 | lan 125 | las 126 | latin 127 | latinamerica 128 | lax 129 | lax1 130 | lb 131 | loadbalancer 132 | login 133 | machine 134 | mail 135 | march 136 | merch 137 | mirror 138 | na 139 | nautilus 140 | net 141 | netherlands 142 | nginx 143 | nl 144 | node 145 | northamerica 146 | nov 147 | oceania 148 | oct 149 | ops 150 | org 151 | origin 152 | page 153 | pantheon 154 | pass 155 | pay 156 | payment 157 | pc 158 | php 159 | pl 160 | poland 161 | preferences 162 | priv 163 | private 164 | prod 165 | production 166 | profile 167 | profiles 168 | promo 169 | promotion 170 | proxy 171 | redirector 172 | region 173 | repo 174 | repository 175 | reset 176 | restrict 177 | restricted 178 | reviews 179 | s 180 | s3 181 | sandbox 182 | search 183 | secure 184 | security 185 | sept 186 | server 187 | service 188 | singed 189 | skins 190 | spring 191 | ssl 192 | staff 193 | stage 194 | stage1 195 | staging 196 | static 197 | support 198 | swagger 199 | system 200 | t 201 | team 202 | test 203 | test1 204 | testbed 205 | testing 206 | testing1 207 | tomcat 208 | tpe 209 | tr 210 | trial 211 | tur 212 | turk 213 | turkey 214 | twitch 215 | uat 216 | v1 217 | v2 218 | vi 219 | vpn 220 | w3 221 | web 222 | web1 223 | webapp 224 | westeurope 225 | z 226 | us-west-1 227 | us-east-1 228 | us-east-2 229 | us-east-3 230 | us-east-4 231 | us-east-5 232 | us-east-6 233 | us-east-7 234 | us-east-8 235 | us-east-9 236 | us-east-10 237 | us-east-11 238 | us-east-12 239 | us-east-13 240 | us-east-14 241 | us-east-15 242 | us-east-16 -------------------------------------------------------------------------------- /words2.txt: -------------------------------------------------------------------------------- 1 | stage 2 | test 3 | --------------------------------------------------------------------------------