├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.rst ├── common.txt ├── default.txt ├── pip.req ├── useragents └── waldo.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.log 4 | resume.txt 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Waldo 2 | 3 | Bug fixes, feature additions, tests, documentation and more can be contributed via [issues](https://github.com/red-team-labs/waldo/issues) and/or [pull requests](https://github.com/red-team-labs/waldo/issues). All contributions are welcome. 4 | 5 | ### Guidelines 6 | 7 | - Separate code commits from reformatting commits. 8 | - Provide tests for any newly added code. 9 | - Follow [PEP8](https://www.python.org/dev/peps/pep-0008/). We recommend using Flake8. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Red Team Labs 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Waldo is a lightweight and multithreaded directory and subdomain bruteforcer implemented in Python. It can be used to locate hidden web resources and undiscovered subdomains of the specified target. 2 | 3 | Key Features 4 | ------------ 5 | 6 | - Quickly and easily generate a list of all subdomains of target domain 7 | - Discover hidden web resources that can be potentially leveraged as part of an attack 8 | - Written in Python and very portable 9 | - Fast, multithreaded design 10 | 11 | Setup 12 | ----- 13 | 14 | Dependencies can be installed by running: 15 | 16 | $ pip install -r pip.req 17 | 18 | To run the waldo: 19 | 20 | $ python waldo.py 21 | 22 | Usage 23 | ----- 24 | 25 | To enumerate subdomains at some-fake-site.example, execute the following: 26 | 27 | $ python waldo.py -m s -d some-fake-site.example 28 | 29 | To enumerate directories at some-fake-site.example, execute the following: 30 | 31 | $ python waldo.py -m d -d some-fake-site.example 32 | 33 | By default, output will be logged to waldo-output.txt. To specify a custom 34 | output file, use the -l flag: 35 | 36 | $ python waldo.py -m s -l my-log-file.txt -d some-fake-site.example 37 | 38 | Waldo uses 4 threads by default. To specify a custom threadpool size, use 39 | the -t flag: 40 | 41 | $ python waldo.py -m s -d some-fake-site.example -t 15 42 | -------------------------------------------------------------------------------- /common.txt: -------------------------------------------------------------------------------- 1 | www 2 | portal 3 | webmail 4 | cpanel 5 | mail 6 | chat 7 | member 8 | members 9 | forum 10 | support 11 | wiki 12 | jira 13 | hipchat 14 | store 15 | dev 16 | test 17 | login 18 | panel 19 | admin 20 | -------------------------------------------------------------------------------- /pip.req: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /useragents: -------------------------------------------------------------------------------- 1 | # Linux 2 | Linux / Firefox 29: Mozilla/5.0 (X11; Linux x86_64; rv:29.0) Gecko/20100101 Firefox/29.0 3 | Linux / Chrome 34: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/537.36 4 | Mozilla/5.0 (Windows NT 6.1; WOW64; rv:24.0) Gecko/20100101 5 | 6 | # Mac 7 | Mac / Firefox 29: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:29.0) Gecko/20100101 Firefox/29.0 8 | Mac / Chrome 34: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/537.36 9 | Mac / Safari 7: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/537.75.14 10 | 11 | # Windows 12 | Windows / Firefox 29: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0 13 | Windows / Chrome 34: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/537.36 14 | Windows / IE 6: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1) 15 | Windows / IE 7: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1) 16 | Windows / IE 8: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0) 17 | Windows / IE 9: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0) 18 | Windows / IE 10: Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0) 19 | Windows / IE 11: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko 20 | 21 | # Android 22 | Android / Firefox 29: Mozilla/5.0 (Android; Mobile; rv:29.0) Gecko/29.0 Firefox/29.0 23 | Android / Chrome 34: Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36 24 | 25 | # iOS 26 | iOS / Chrome 34: Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) CriOS/34.0.1847.18 Mobile/11B554a Safari/9537.53 27 | iOS / Safari 7: Mozilla/5.0 (iPad; CPU OS 7_0_4 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11B554a Safari/9537.53 28 | 29 | # Custom 30 | 31 | Mozilla/5.0 Waldo 32 | -------------------------------------------------------------------------------- /waldo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # Waldo / Version 0.1.2 4 | # Red|Team|Labs - Top-Hat-Sec 5 | # By: R4v3N & s0lst1ce 6 | 7 | import sys 8 | import requests 9 | import re 10 | import json 11 | 12 | from socket import gaierror, gethostbyname 13 | from argparse import ArgumentParser 14 | from Queue import Queue 15 | from threading import Thread 16 | from datetime import datetime 17 | 18 | __version__ = '0.1.2' 19 | 20 | # configs 21 | MAX_WORKERS = 4 22 | 23 | DEFAULT_LIST = 'default.txt' 24 | 25 | LOG_FILE = 'waldo-output.txt' 26 | MODE_DIRS = 'd' 27 | MODE_SUB = 's' 28 | 29 | class OutputThread(Thread): 30 | 31 | def __init__(self, queue, build_subdomain_map=False): 32 | 33 | Thread.__init__(self) 34 | self.queue = queue 35 | self.build_subdomain_map = build_subdomain_map 36 | 37 | def run(self): 38 | 39 | while True: 40 | 41 | result = self.queue.get() 42 | 43 | self.write_result(result['line_number'], 44 | result['file_len'], 45 | result['status_code'], 46 | result['url'], 47 | result['ip_addr'], 48 | result['success']) 49 | 50 | if self.build_subdomain_map: 51 | 52 | self.add_to_subdomain_map(result) 53 | 54 | self.queue.task_done() 55 | 56 | def write_result(self, line_number, file_len, status_code, url, ip_addr, success): 57 | 58 | sys.stdout.write('\r') 59 | if success: 60 | print "[ %d ] Target: %s --> IP: %s" %\ 61 | (status_code, url, ip_addr) 62 | 63 | sys.stdout.write("Progress [ %d - %d ]" % (line_number, file_len)) 64 | sys.stdout.flush() 65 | 66 | resume_handle.write('%d\n' % line_number) 67 | output_handle.write('%d %s %s\n' % (status_code, url, ip_addr)) 68 | 69 | def add_to_subdomain_map(self, result): 70 | 71 | ip_addr = result['ip_addr'] 72 | url = result['url'] 73 | status_code = '%d' % result['status_code'] 74 | 75 | if ip_addr in subdomain_map: 76 | if status_code in subdomain_map[ip_addr]: 77 | subdomain_map[ip_addr][status_code].append(url) 78 | else: 79 | subdomain_map[ip_addr][status_code] = [url] 80 | else: 81 | subdomain_map[ip_addr] = {status_code: [url]} 82 | 83 | 84 | class WorkerThread(Thread): 85 | 86 | def __init__(self, in_queue, out_queue): 87 | 88 | Thread.__init__(self) 89 | self.in_queue = in_queue 90 | self.out_queue = out_queue 91 | 92 | def run(self): 93 | 94 | while True: 95 | 96 | params = self.in_queue.get() 97 | 98 | params['status_code'] = self.get_status(params['url']) 99 | 100 | if self.status_ok(params['status_code']): 101 | 102 | params['ip_addr'] = self.get_ip(params) 103 | params['success'] = True 104 | else: 105 | params['ip_addr'] = None 106 | params['success'] = False 107 | 108 | self.out_queue.put(params) 109 | 110 | self.in_queue.task_done() 111 | 112 | def get_status(self, url): 113 | 114 | try: 115 | response = requests.head('http://%s' % url) 116 | except requests.exceptions.ConnectionError: 117 | return -1 118 | 119 | return response.status_code 120 | 121 | 122 | class DirThread(WorkerThread): 123 | 124 | def status_ok(self, status_code): 125 | 126 | if status_code >= 200 and status_code < 300: 127 | return True 128 | return False 129 | 130 | def get_ip(self, params): 131 | return params['domain_ip'] 132 | 133 | 134 | class SubThread(WorkerThread): 135 | 136 | def status_ok(self, status_code): 137 | 138 | if status_code >= 200 and status_code < 400: 139 | return True 140 | return False 141 | 142 | def get_ip(self, params): 143 | return gethostbyname(params['url']) 144 | 145 | # auxiliary functions --------------------------------------------------------- 146 | 147 | 148 | def print_header(): 149 | 150 | print ''' 151 | 152 | _/ _/ _/ _/ 153 | _/ _/ _/_/_/ _/ _/_/_/ _/_/ 154 | _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ 155 | _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ 156 | _/ _/ _/_/_/ _/ _/_/_/ _/_/ 157 | 158 | Red|Team|Labs <> Top-Hat-Sec 159 | Waldo - Version '''+__version__+''' 160 | 161 | ''' 162 | 163 | def print_configs(configs): 164 | 165 | print 166 | print '[*] Target domain:', configs['domain'] 167 | print '[*] Target IP: ', configs['domain_ip'] 168 | if configs['mode'] == MODE_DIRS: 169 | print '[*] Mode: enumerate directories' 170 | else: 171 | print '[*] Mode: enumerate subdomains' 172 | print '[*] Wordlist: ', configs['wordlist'] 173 | print 174 | 175 | def error_handler(msg): 176 | 177 | print '[!]', msg 178 | sys.exit(1) 179 | 180 | 181 | def run_initial_check(url): 182 | 183 | try: 184 | ip_addr = gethostbyname(url) 185 | except gaierror: 186 | error_handler('Invalid target %s' % url) 187 | 188 | print '[Setup] Checking %s' % url 189 | response = requests.head('http://%s' % url) 190 | # https://www.youtube.com/watch?v=3cEQX632D1M 191 | if response.status_code < 200 or response.status_code >= 400: 192 | error_handler('Invalid target %s' % url) 193 | 194 | print '[Setup] %s is valid... continuing' % url 195 | 196 | return ip_addr 197 | 198 | 199 | def gen_logfile_name(domain): 200 | 201 | now = datetime.now() 202 | return now.strftime('{domain}-%Y-%m-%d-%H-%M-%S.log').format(domain=domain) 203 | 204 | 205 | def parse_args(): 206 | 207 | parser = ArgumentParser() 208 | 209 | parser.add_argument('-m', '--mode', 210 | dest='mode', 211 | required=True, 212 | type=str, 213 | metavar='', 214 | choices=['d', 's'], 215 | help='Set mode to "s" for subdomains, "d" for dire"\ 216 | "ctories.') 217 | 218 | parser.add_argument('-w', '--wordlist', 219 | dest='wordlist', 220 | required=False, 221 | default=DEFAULT_LIST, 222 | type=str, 223 | metavar='', 224 | help='Bruteforce useing ') 225 | 226 | parser.add_argument('-l', '--log-file', 227 | dest='log_file', 228 | required=False, 229 | default=None, 230 | type=str, 231 | metavar='', 232 | help='Log results to ') 233 | 234 | parser.add_argument('-d', '--domain', 235 | dest='domain', 236 | required=True, 237 | type=str, 238 | metavar='', 239 | help='The target domain') 240 | 241 | parser.add_argument('-b', '--build-subdomain-map', 242 | dest='build_subdomain_map', 243 | action='store_true', 244 | required=False, 245 | default=False, 246 | help='Build a subdomain map') 247 | 248 | parser.add_argument('-t', '--threads', 249 | dest='max_workers', 250 | required=False, 251 | default=MAX_WORKERS, 252 | type=int, 253 | metavar='', 254 | help='Specify the maximum number of threads to use.') 255 | 256 | args = parser.parse_args() 257 | 258 | return { 259 | 'mode': args.mode, 260 | 'wordlist': args.wordlist, 261 | 'domain': re.sub('http[s]?://', '', args.domain).rstrip('/'), 262 | 'max_workers': args.max_workers, 263 | 'log_file': args.log_file, 264 | 'build_subdomain_map': args.build_subdomain_map, 265 | } 266 | 267 | 268 | def set_configs(): 269 | 270 | configs = parse_args() 271 | configs['domain_ip'] = run_initial_check(configs['domain']) 272 | 273 | if configs['mode'] == MODE_DIRS: 274 | configs['url_builder'] = '%s/%%s' % configs['domain'] 275 | configs['worker_thread'] = DirThread 276 | else: 277 | configs['url_builder'] = '%%s.%s' % configs['domain'] 278 | configs['worker_thread'] = SubThread 279 | 280 | if configs['log_file'] is None: 281 | configs['log_file'] = gen_logfile_name(configs['domain']) 282 | 283 | # get number of lines in wordlist file 284 | configs['file_len'] = sum(1 for line in open(configs['wordlist'])) 285 | 286 | return configs 287 | 288 | 289 | output_handle = None 290 | resume_handle = None 291 | subdomain_map = {} 292 | 293 | 294 | def main(): 295 | 296 | global output_handle 297 | global resume_handle 298 | 299 | print_header() 300 | configs = set_configs() 301 | print_configs(configs) 302 | 303 | in_queue = Queue(configs['max_workers'] * 2) 304 | out_queue = Queue() 305 | 306 | output_handle = open(configs['log_file'], 'w') 307 | resume_handle = open('resume.txt', 'a') 308 | 309 | output_thread = OutputThread(out_queue, 310 | build_subdomain_map=configs['build_subdomain_map']) 311 | output_thread.daemon = True 312 | output_thread.start() 313 | 314 | threads = [] 315 | for i in xrange(configs['max_workers']): 316 | t = configs['worker_thread'](in_queue=in_queue, out_queue=out_queue) 317 | t.daemon = True 318 | t.start() 319 | threads.append(t) 320 | 321 | print_interrupt_message_on_exit = False 322 | try: 323 | with open(configs['wordlist']) as input_handle: 324 | 325 | for line_number, line in enumerate(input_handle): 326 | 327 | 328 | in_queue.put({ 329 | 'url': configs['url_builder'] % line.strip(), 330 | 'line_number': line_number, 331 | 'file_len': configs['file_len'], 332 | 'domain_ip': configs['domain_ip'], 333 | }) 334 | in_queue.join() 335 | 336 | except KeyboardInterrupt: 337 | print_interrupt_message_on_exit = True 338 | 339 | out_queue.join() 340 | output_handle.close() 341 | resume_handle.close() 342 | 343 | if print_interrupt_message_on_exit: 344 | print '\n\n[!] Exiting on user interupt.\n' 345 | 346 | if configs['build_subdomain_map']: 347 | print json.dumps(subdomain_map, indent=4, sort_keys=True) 348 | sys.exit(0) 349 | 350 | if __name__ == '__main__': 351 | main() 352 | --------------------------------------------------------------------------------