├── .github └── workflows │ └── codeql-analysis.yml ├── README.md ├── Syborg Creative Usage Guidelines.md ├── carbon.png ├── requirements.txt ├── syborg.py └── wordlist.txt /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '38 21 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Syborg 2 | Syborg is a Recursive DNS Domain Enumerator which is neither active nor completely passive. This tool simply constructs a domain name and queries it with a specified DNS Server. 3 | 4 | Syborg has a Dead-end Avoidance system inspired from [@Tomnomnom](https://github.com/tomnomnom/hacks)'s [ettu](https://github.com/tomnomnom/hacks). 5 | 6 | When you run subdomain enumeration with some of the tools, most of them passively query public records like `virustotal`, `crtsh` or `censys`. This enumeration technique is really fast and helps to find out a lot of domains in much less time. 7 | 8 | However, there are some domains that may not be mentioned in these public records. In order to find those domains, Syborg interacts with the nameservers and recursively brute-forces subdomain from the DNS until it's queue is empty. 9 | 10 | ![carbon.png](carbon.png) 11 | Image Credits: [Carbon](https://carbon.now.sh) 12 | 13 | As mentioned on [ettu](https://github.com/tomnomnom/hacks)'s page, I quote: 14 | 15 | > Ordinarily if there are no records to return for a DNS name you might expect an `NXDOMAIN` error: 16 | > ```bash 17 | > ▶ host four.tomnomnom.uk 18 | > Host four.tomnomnom.uk not found: 3(NXDOMAIN) 19 | > ``` 20 | > You may have noticed that sometimes you get an empty response instead though: 21 | > ```bash 22 | > ▶ host three.tomnomnom.uk 23 | > ``` 24 | > The difference in the latter case is often that another name - one that has your queried name as a suffix - exists and has records to return 25 | > ```bash 26 | > ▶ host one.two.three.tomnomnom.uk 27 | > one.two.three.tomnomnom.uk has address 46.101.59.42 28 | > ``` 29 | > This difference in response can be used to help avoid dead-ends in recursive DNS brute-forcing by not recursing in the former situation: 30 | > ```bash 31 | > ▶ echo -e "www\none\ntwo\nthree" | ettu tomnomnom.uk 32 | > one.two.three.tomnomnom.uk 33 | > ``` 34 | 35 | Syborg incorporates all of these functionalities with simple concurrency and recursion. 36 | 37 | ## Requirements: 38 | 39 | Python 3.x (Recommended) 40 | 41 | Python 2.x (Not tested) 42 | 43 | ## Installation: 44 | 45 | Clone the repo using the `git clone` command as follows: 46 | 47 | ```bash 48 | git clone https://github.com/MilindPurswani/Syborg.git 49 | ``` 50 | 51 | Resolve the Dependencies: 52 | 53 | ```bash 54 | pip3 install -r requirements.txt 55 | ``` 56 | 57 | ## Usage: 58 | 59 | ```bash 60 | python3 syborg.py yahoo.com 61 | ``` 62 | 63 | *More information regarding usage can be found in Syborg's [Creative Usage Guidelines](https://github.com/MilindPurswani/Syborg/blob/master/Syborg%20Creative%20Usage%20Guidelines.md). Do check it out!* 64 | 65 | **At times, it is also possible that Syborg will hit High CPU Usage and that can cost you a lot if you are trying to use this tool on your VPS. Therefore to limit that use another utility called Cpulimit** 66 | 67 | ```bash 68 | cpulimit -l 50 -p $(pgrep python3) 69 | ``` 70 | 71 | This tool can be downloaded as follows: 72 | 73 | ```bash 74 | sudo apt install cpulimit 75 | ``` 76 | 77 | 78 | 79 | ## Special Thanks <3: 80 | 81 | 1. [@nahamsec](https://twitter.com/nahamsec) for his invaluable contribution towards the community by live streams. Check out his twitch channel https://twitch.tv/nahamsec 82 | 2. [@tomnomnom](https://twitter.com/tomnomnom) for making such awesome tools and sharing with everyone. Be sure to check out his twitch https://www.twitch.tv/tomnomnomuk 83 | 3. [@GP89](https://github.com/GP89) for the `FileQueue` lib that resolved high memory consumption problem with Syborg. 84 | 4. [Patrik Hudak](https://0xpatrik.com/) for his awesome teachings and tools like [`dnsgen`](https://github.com/ProjectAnte/dnsgen). 85 | -------------------------------------------------------------------------------- /Syborg Creative Usage Guidelines.md: -------------------------------------------------------------------------------- 1 | # Syborg Creative Usage Guidelines 2 | 3 | Syborg is a great tool for Subdomain enumeration. It's really smart and fast. But it's simply dumb! That sounds quite contrary. Doesn't it? 4 | 5 | **How can it be a great tool, smart and fast, yet dumb?** 6 | 7 | The answer is really simple! Syborg relies brute-forcing DNS servers for proper response. It recursively searches for subdomains by brute-forcing. That's it! Nothing more! However the power of this tool depends on how one uses it! 8 | 9 | The wordlist provided with Syborg has a very limited, general set of words that are normally available. This wordlist does with in most of the cases but can't give you the results you are expecting. We have to be smart with how we query for subdomains using syborg. 10 | 11 | Here I am enlisting a few ways in which you can guarantee good results with Syborg. 12 | 13 | ## Try doing passive reconnaissance first. 14 | 15 | Syborg works wonderfully if you have a domain specific wordlist. All domains are different. For instance, 16 | 17 | ``` 18 | mail.google.com 19 | docs.google.com 20 | photos.google.com 21 | ``` 22 | 23 | are valid subdomains related to `google.com` but these subdomains may not be valid in case of `yahoo.com` simply meaning that the wordlist of `google.com` has to be tweaked a little before using it against `yahoo.com` for subdomain enumeration. This is what I mean by domain specific wordlist. 24 | 25 | This functionality is offered by a lot of tools and there is no point re-inventing the wheel, therefore I recommend using some of the tools mentioned below 26 | 27 | ### AssetFinder with tok 28 | 29 | If you are looking for Passive reconnaissance, assetfinder is a great tool! It's simply fast.. This tool is offered by [@tomnomnom](https://github.com/tomnomnom/) and can be easily used in conjunction with Syborg. In order to use this tool, you should have `go` installed and properly configured. So, generate a wordlist with assetfinder as follows: 30 | 31 | ``` 32 | assetfinder --subs-only media.yahoo.com | tok -delim-exceptions=- | sort -u | tee -a media.yahoo.com-wordlist.txt 33 | ``` 34 | 35 | Inorder to install `assetfinder` and `tok` 36 | 37 | ``` 38 | go get -u github.com/tomnomnom/assetfinder 39 | go get -u github.com/tomnomnom/hacks/tok 40 | ``` 41 | 42 | This will generate a great wordlist for you and look for hidden domains. 43 | 44 | ### [Sublist3r](https://github.com/aboul3la/Sublist3r) 45 | 46 | Let alone Sublist3r is also enough for generating a domain specific wordlist. If you have Sublist3r you can use the following commands to create one. 47 | 48 | ``` 49 | python sublist3r.py -d media.yahoo.com -o domains.txt 50 | cat domains.txt | sed 's/[.]/\n/g' | sort -u | tee -a media.yahoo.com-wordlist.txt 51 | ``` 52 | 53 | There is no limit to number of tools that can be used here. One can also use any other tool such as masscan. In the end all that matters is that we need a good wordlist, that can be used here. 54 | 55 | ## Taking it to Next Level 56 | 57 | There are certain cases where the current wordlist isn't simply enough or we may need some more permutations inorder to search for such domains. 58 | 59 | Consider this example: 60 | 61 | ``` 62 | python sublist3r.py -d milindpurswani.com 63 | ``` 64 | 65 | results in following domains: 66 | 67 | ``` 68 | www.milindpurswani.com 69 | log.milindpurswani.com 70 | mail.milindpurswani.com 71 | ``` 72 | 73 | Well, this is not looking so good, but our community is filled with creative people who have solutions to these problems. 74 | 75 | In this [article](https://0xpatrik.com/subdomain-enumeration-smarter/), [Patrik Hudak](https://0xpatrik.com/subdomain-enumeration-smarter/) explains a smarter way of doing reconnaissance. He introduces us with his new tool, [`dnsgen`](https://pypi.org/project/dnsgen/). This tool generates a combination of domain names from the provided input. Combinations are created based on wordlist. Custom words are extracted per execution (as he mentions). For more information, check out his repo. By simply running. 76 | 77 | ``` 78 | dnsgen domains.txt -w media.yahoo.com-wordlist.txt 79 | ``` 80 | 81 | You will have generated many possible combinations of subdomains that can be queried with resolvers (as he mentions). Let's take it to another step and pass this data to Syborg. 82 | 83 | ``` 84 | dnsgen domains.txt -w media.yahoo.com-wordlist.txt | sed 's/[.]/\n/g' | sort -u | tee -a media.yahoo.com-extreem-wordlist.txt 85 | ``` 86 | 87 | This resultant wordlist would contain almost all possible combinations of words that would be specific to any subdomain for which it has been made. Let's pass it to Syborg as follows: 88 | 89 | ``` 90 | python syborg.py media.yahoo.com -w media.yahoo.com-extreem-wordlist.txt -c 50 -v 91 | ``` 92 | 93 | I hope now you have some clear understanding of Syborg's usage. Simply dumb, yet Smart! -------------------------------------------------------------------------------- /carbon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MilindPurswani/Syborg/5cd010b941920965f48a886cdcf0c23d817ed5f7/carbon.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | filequeue 2 | argparse 3 | dnspython -------------------------------------------------------------------------------- /syborg.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import sys 3 | from filequeue import * 4 | import sys 5 | import socket 6 | import dns.resolver 7 | import argparse 8 | import time 9 | import random 10 | import string 11 | 12 | 13 | 14 | def randomString(stringLength=10): 15 | """Generate a random string of fixed length """ 16 | letters = string.ascii_lowercase 17 | return ''.join(random.choice(letters) for i in range(stringLength)) 18 | 19 | def getTime(): 20 | return time.strftime("%Y:%m:%d - %H:%M:%S") 21 | 22 | def printVerbose(message): 23 | if args.verbose: 24 | print(message) 25 | 26 | 27 | def doWork(): 28 | while True: 29 | word = q.get() 30 | getStatus(word) 31 | q.task_done() 32 | if q.empty(): 33 | #print("empty queue") 34 | pass 35 | 36 | def checkwildcard(domain): 37 | a = randomString() 38 | temp_domain = a + '.'+ domain 39 | result = checkrandom(temp_domain) 40 | if(result == "resolved" or result == "noanswer"): 41 | return False 42 | else: 43 | return True 44 | 45 | def warning(): 46 | return "\033[1;31;40m[*]["+getTime()+"] " 47 | 48 | def error(): 49 | return "\033[0m 0;37;40m[*]["+getTime()+"] " 50 | 51 | def info(): 52 | return "\033[0;37;40m[*]["+getTime()+"] " 53 | 54 | def success(): 55 | return"\033[1;32;40m[*]["+getTime()+"] " 56 | 57 | 58 | def checkrandom(domain): 59 | try: 60 | answers = dns.resolver.query(domain) 61 | return "resolved" 62 | except dns.resolver.Timeout: 63 | return "timeout" 64 | except dns.resolver.NXDOMAIN: 65 | return "nxdomain" 66 | except dns.resolver.NoAnswer: 67 | return "noanswer" 68 | except dns.exception.DNSException: 69 | return "unknown exception" 70 | 71 | def getStatus(domain): 72 | try: 73 | answers = dns.resolver.query(domain) 74 | #for rdata in answers: # for each response 75 | #print("Resolved " + domain + " : " +str(rdata)) # print the data 76 | appenddataset1(domain) 77 | printVerbose(success()+"Resolved domain %s" % domain) 78 | print(domain) 79 | log("Resolved domain %s" % domain) 80 | out_file.write(domain+"\n") 81 | except dns.resolver.Timeout: 82 | printVerbose(warning()+"Timeout for domain %s" % domain) 83 | addbacktoqueue(domain) 84 | log("Timeout for domain %s" % domain) 85 | except dns.resolver.NXDOMAIN: 86 | printVerbose(info()+"No such domain %s" % domain) 87 | log("No such domain %s" % domain) 88 | except dns.resolver.NoAnswer: 89 | printVerbose(success()+"Not resolved %s" % domain) 90 | appenddataset1(domain) 91 | log("Not resolved %s" % domain) 92 | except dns.exception.DNSException: 93 | #print("Unhandled exception") 94 | log("DNS Exception %s" % domain) 95 | printVerbose("DNS Exception %s" % domain) 96 | 97 | def checkdomain(): 98 | result = checkrandom(site) 99 | printVerbose("Checkrandom returned: "+result) 100 | if(result == "resolved" or result == "noanswer"): 101 | print("calling appenddataset1") 102 | appenddataset(site) 103 | elif(result == "timeout"): 104 | print("Internet Connection issues") 105 | else: 106 | pass 107 | 108 | def appenddataset(domain): 109 | try: 110 | if checkwildcard(domain): 111 | for words in open(wordlist ,'r'): 112 | q.put(words.strip() + "." + domain) 113 | pass 114 | q.join() 115 | except: 116 | print("unexpected error") 117 | log("unexpected error") 118 | 119 | 120 | def appenddataset1(domain): 121 | try: 122 | if checkwildcard(domain): 123 | print("addding to queue") 124 | for words in open(wordlist ,'r'): 125 | q.put(words.strip() + "." + domain) 126 | except exception as e: 127 | print(e) 128 | log(e) 129 | 130 | failed_dict = {} 131 | 132 | def addbacktoqueue(domain): 133 | if domain in failed_dict: 134 | if failed_dict[domain] >= threshold: 135 | if args.enablelogging: 136 | log("Not putting %s into queue [Failed attempts exceeded]" % domain) 137 | printVerbose("Not putting %s into queue [Failed attempts exceeded]" % domain) 138 | else: 139 | failed_dict[domain] += 1 140 | q.put(domain) 141 | else: 142 | failed_dict[domain] = 1 143 | q.put(domain) 144 | 145 | 146 | parser = argparse.ArgumentParser() 147 | parser.add_argument("domain",help="domain name of the target") 148 | parser.add_argument("-d","--dns",help="DNS Server to be used (default: 8.8.8.8)") 149 | parser.add_argument("-w","--wordlist",help="Specify a custom wordlist (default: wordlist.txt)") 150 | parser.add_argument("-o","--output",help="Specify the output file (default: results-domain.txt)") 151 | parser.add_argument("-c","--concurrency",help="Specify the level of concurrency (default: 10)") 152 | parser.add_argument("-l","--enablelogging",help="Enable logging to a file",action="store_true") 153 | parser.add_argument("-v", "--verbose", help="increase output verbosity",action="store_true") 154 | parser.add_argument("-t", "--threshold", help="Number of times to retry domain incase of timeout(default: 3)") 155 | args = parser.parse_args() 156 | site = args.domain 157 | 158 | if args.dns: 159 | dns_server = args.dns 160 | else: 161 | dns_server = "8.8.8.8" 162 | 163 | if args.threshold: 164 | threshold = int(args.threshold) 165 | else: 166 | threshold = 3 167 | 168 | if args.output: 169 | output_file = args.output 170 | else: 171 | output_file = "results-"+site+".txt" 172 | 173 | out_file = open(output_file,"a") 174 | 175 | if args.wordlist: 176 | wordlist = args.wordlist 177 | else: 178 | wordlist = "wordlist.txt" 179 | 180 | if args.concurrency: 181 | concurrent = int(args.concurrency,10) 182 | else: 183 | concurrent = 10 184 | 185 | 186 | 187 | printVerbose("[*]["+getTime()+"] Starting Scan against %s " % site) 188 | printVerbose("[*]["+getTime()+"] Verbose Mode On!") 189 | printVerbose("[*]["+getTime()+"] DNS Server Set to %s" % dns_server) 190 | printVerbose("[*]["+getTime()+"] Output to %s" % output_file) 191 | printVerbose("[*]["+getTime()+"] BEWARE: This may overwrite the file if it's already existing.") 192 | printVerbose("[*]["+getTime()+"] Using wordlist %s" % wordlist) 193 | printVerbose("[*]["+getTime()+"] Concurrency set to %s" % concurrent) 194 | 195 | 196 | def log(data): 197 | if args.enablelogging: 198 | logfile.write(data+"\n") 199 | 200 | if args.enablelogging: 201 | log_file = output_file+"-"+"log.log" 202 | logfile = open(log_file,"a+") 203 | printVerbose("[*]["+getTime()+"] Logging to file %s" % log_file) 204 | log("[*]["+getTime()+"] Logging to file %s" % log_file) 205 | 206 | 207 | 208 | log("[*]["+getTime()+"] Starting Scan against %s " % site) 209 | log("[*]["+getTime()+"] Verbose Mode On!") 210 | log("[*]["+getTime()+"] DNS Server Set to %s" % dns_server) 211 | log("[*]["+getTime()+"] Output to %s" % output_file) 212 | log("[*]["+getTime()+"] BEWARE: This may overwrite the file if it's already existing.") 213 | log("[*]["+getTime()+"] Using wordlist %s" % wordlist) 214 | log("[*]["+getTime()+"] Concurrency set to %s" % concurrent) 215 | 216 | #Delay the script from running to allow users to read options 217 | if args.verbose: 218 | time.sleep(2) 219 | 220 | resolver = dns.resolver.Resolver() 221 | resolver.nameservers = [socket.gethostbyname(dns_server)] 222 | resolver.timeout = 1 223 | resolver.lifetime = 1 224 | 225 | q = filequeue.FileQueue(maxsize=1000) 226 | try: 227 | file = open(output_file,"w+") 228 | except exception as e: 229 | print(e) 230 | log(e) 231 | exit(1) 232 | 233 | for i in range(concurrent): 234 | t = Thread(target=doWork) 235 | t.daemon = True 236 | t.start() 237 | 238 | checkdomain() 239 | 240 | out_file.close() 241 | if args.enablelogging: 242 | logfile.close() -------------------------------------------------------------------------------- /wordlist.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 | --------------------------------------------------------------------------------