1.SubDomainsBrute简介
SubDomainsBrute是一款目标域名收集工具,工具可从github上直接搜索SubDomainsBrute找到,
dirsearch的使用是通过命令行获取参数,例如,
python SubDomainsBrute.py
可以看到SubDomainsBrute文件结构如下:
dict(字典)、Lib(源码)、tmp(结果生成)文件夹、subDomainsBrute.py
dict包含dns_servers、next_sub、next_sub_full、sample_qq.com、subnames、subnames_all_5_letters、subnames_full。lib包含cmdline、common、consle_width.py
在运行结束后生成,domain_full.txt,有最终的子域名信息
具体的参数可看Github上如下的usage。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | Usage: subDomainsBrute.py [options] target.com Options: --version show program's version number and exit -h, --help show this help message and exit -f FILE File contains new line delimited subs, default is subnames.txt. --full Full scan, NAMES FILE subnames_full.txt will be used to brute -i, --ignore-intranet Ignore domains pointed to private IPs -t THREADS, --threads=THREADS Num of scan threads, 200 by default -p PROCESS, --process=PROCESS Num of scan Process, 6 by default -o OUTPUT, --output=OUTPUT Output file name. default is {target}.txt |
2. SubDomainsBrute源码分析
想要对源码分析,首先找到程序入口,即SubDomainsBrute.py。
1 2 3 4 5 6 7 8 | #main tmp_dir = 'tmp/%s_%s' % (args[0], int(time.time())) multiprocessing.freeze_support() dns_servers = load_dns_servers() next_subs = load_next_sub(options) scan_count = multiprocessing.Value('i', 0) found_count = multiprocessing.Value('i', 0) queue_size_array = multiprocessing.Array('i', options.process) |
首先根据传入的域名创建了tmp子文件,然后dns_servers调用load_dns_servers(),该函数在common.py中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #load_dns_servers() def load_dns_servers(): print_msg('[+] Validate DNS servers', line_feed=True) dns_servers = [] pool = Pool(5) for server in open('dict/dns_servers.txt').readlines(): server = server.strip() if server and not server.startswith('#'): pool.apply_async(test_server, (server, dns_servers)) pool.join() server_count = len(dns_servers) print_msg(' [+] %s DNS Servers found' % server_count, line_feed=True) if server_count == 0: print_msg('[ERROR] No valid DNS Server !', line_feed=True) sys.exit(-1) return dns_servers |
dns_servers.txt中包含了
1 2 3 4 5 6 7 | # dns_servers.txt 119.29.29.29 182.254.116.116 # 223.5.5.5 # 223.6.6.6 114.114.115.115 114.114.114.114 |
119.29.29.29是腾讯DNSPod公共DNS,还有其他的公共DNS如,114dns(114.114.114.114),阿里(223.5.5.5.5),百度(180.76.76.76),360(101.226.4.6),google(8.8.8.8)等。load_dns_servers中循环读取dns_servers.txt中的dns服务,strip()函数去除换行符,并使用 test_server()判断是否是可用的dns服务,并采用协程执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | # test_server def test_server(server, dns_servers): resolver = dns.resolver.Resolver(configure=False) resolver.lifetime = resolver.timeout = 5.0 try: resolver.nameservers = [server] answers = resolver.query('public-dns-a.baidu.com') # an existed domain if answers[0].address != '180.76.76.76': raise Exception('Incorrect DNS response') try: resolver.query('test.bad.dns.lijiejie.com') # non-existed domain with open('bad_dns_servers.txt', 'a') as f: f.write(server + ' ') print('[+] Bad DNS Server found %s' % server) except Exception as e: dns_servers.append(server) print('[+] Server %s < OK > Found %s' % (server.ljust(16), len(dns_servers))) except Exception as e: print('[+] Server %s <Fail> Found %s' % (server.ljust(16), len(dns_servers))) |
首先指定了一个存在的网址,如果解析出来的ip正确,那么就将这个server添加到dns_server中,然后指定一个不存在的网址,无法解析,直接except得到dns_servers。得到最终的dns_servers列表后,load_dns_servers()中会继续判断如果列表长度等于0,那么就是No valid DNS Server,程序退出,否则返回dns_server。主函数调用load_dns_servers()后,又调用了common.py中的load_next_sub函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #common.py def load_next_sub(options): next_subs = [] _file = 'dict/next_sub_full.txt' if options.full_scan else 'dict/next_sub.txt' with open(_file) as f: for line in f: sub = line.strip() if sub and sub not in next_subs: tmp_set = {sub} while tmp_set: item = tmp_set.pop() if item.find('{alphnum}') >= 0: for _letter in 'abcdefghijklmnopqrstuvwxyz0123456789': tmp_set.add(item.replace('{alphnum}', _letter, 1)) elif item.find('{alpha}') >= 0: for _letter in 'abcdefghijklmnopqrstuvwxyz': tmp_set.add(item.replace('{alpha}', _letter, 1)) elif item.find('{num}') >= 0: for _letter in '0123456789': tmp_set.add(item.replace('{num}', _letter, 1)) elif item not in next_subs: next_subs.append(item) return next_subs |
根据options是否为full选择不同的字典,逐行读取字典,strip函数去除换行,如果没有在next_subs列表中,暂时村放入tmp_set,弹出该元素后进行相应的if判断,如果都不符合,直接加在next_subs列表中,最后next_subs列表有该字典中的内容。如果读取的字符串有{alphnum}、{alpha}、{num}这样的,函数将会对这些进行替换,例如含有test{alphanum},则会循环被替换为,['testa'、'testb'...、'testz'、'test0'...、'test9']。
接着主程序中执行了如下步骤
1 2 3 | scan_count = multiprocessing.Value('i', 0) found_count = multiprocessing.Value('i', 0) queue_size_array = multiprocessing.Array('i', options.process) |
multiprocessing为多进程。i代表数字,设置扫描数量,找到数量初始为0。
进程和线程存在区别,比如之前分析dirsearch,该工具就是用的多线程threading。对于进程multiprocessing,通常有两种方式
1 2 | from multiprocessing import Processing from multiprocessing import Pool |
Pool类主要用于多目标,如果目标少且不用控制进程数量则可以用Process类。其中Pool可以控制进程数。
接着主程序调用了wildcard_test函数和get_sub_file_path函数。
1 2 | domain = wildcard_test(args[0]) options.file = get_sub_file_path() |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #SubDomainsBrute.py def wildcard_test(domain, level=1): try: r = dns.resolver.Resolver(configure=False) r.nameservers = dns_servers answers = r.query('lijiejie-not-existed-test.%s' % domain) ips = ', '.join(sorted([answer.address for answer in answers])) if level == 1: print('any-sub.%s %s' % (domain.ljust(30), ips)) wildcard_test('any-sub.%s' % domain, 2) elif level == 2: exit(0) except Exception as e: return domain |
wildcard_test部分和test_server部分较为相似,最终返回我们输入的域名
1 2 3 4 5 6 7 8 9 10 11 12 | def get_sub_file_path(): if options.full_scan and options.file == 'subnames.txt': path = 'dict/subnames_full.txt' else: if os.path.exists(options.file): path = options.file elif os.path.exists('dict/%s' % options.file): path = 'dict/%s' % options.file else: print_msg('[ERROR] Names file not found: %s' % options.file) exit(-1) return path |
在full扫描的情况下,返回路径dict/subnames_full.txt,否则在dict目录下寻找所输入的目录。找不到则显示ERROR。
接着,主程序内容如下
1 2 3 4 5 | for process_num in range(options.process): p = multiprocessing.Process(target=run_process, args=(domain, options, process_num, dns_servers, next_subs, scan_count, found_count, queue_size_array, tmp_dir)) all_process.append(p) p.start() |
options.process为用户从命令行输入的进程数,默认为6,接着设置多进程,并添加到进程列表中,开始进程。在进程设置过程中设置了run_process()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | def run_process(*params): signal.signal(signal.SIGINT, user_abort) s = SubNameBrute(*params) s.run() def user_abort(sig, frame): exit(-1) def run(self): threads = [gevent.spawn(self.scan, i) for i in range(self.options.threads)] gevent.joinall(threads) def scan(self, j): self.resolvers[j].nameservers = [self.dns_servers[j % self.dns_count]] + self.dns_servers while True: try: self.lock.acquire() if time.time() - self.count_time > 1.0: self.scan_count.value += self.scan_count_local self.scan_count_local = 0 self.queue_size_array[self.process_num] = self.queue.qsize() if self.found_count_local: self.found_count.value += self.found_count_local self.found_count_local = 0 self.count_time = time.time() self.lock.release() brace_count, sub = self.queue.get(timeout=3.0) if brace_count > 0: brace_count -= 1 if sub.find('{next_sub}') >= 0: for _ in self.next_subs: self.queue.put((0, sub.replace('{next_sub}', _))) if sub.find('{alphnum}') >= 0: for _ in 'abcdefghijklmnopqrstuvwxyz0123456789': self.queue.put((brace_count, sub.replace('{alphnum}', _, 1))) elif sub.find('{alpha}') >= 0: for _ in 'abcdefghijklmnopqrstuvwxyz': self.queue.put((brace_count, sub.replace('{alpha}', _, 1))) elif sub.find('{num}') >= 0: for _ in '0123456789': self.queue.put((brace_count, sub.replace('{num}', _, 1))) continue except gevent.queue.Empty as e: break try: if sub in self.found_subs: continue self.scan_count_local += 1 cur_domain = sub + '.' + self.domain answers = self.resolvers[j].query(cur_domain) if answers: self.found_subs.add(sub) ips = ', '.join(sorted([answer.address for answer in answers])) if ips in ['1.1.1.1', '127.0.0.1', '0.0.0.0', '0.0.0.1']: continue if self.options.i and is_intranet(answers[0].address): continue try: self.scan_count_local += 1 answers = self.resolvers[j].query(cur_domain, 'cname') cname = answers[0].target.to_unicode().rstrip('.') if cname.endswith(self.domain) and cname not in self.found_subs: cname_sub = cname[:len(cname) - len(self.domain) - 1] # new sub if cname_sub not in self.normal_names_set: self.found_subs.add(cname) self.queue.put((0, cname_sub)) except Exception as e: pass first_level_sub = sub.split('.')[-1] if (first_level_sub, ips) not in self.ip_dict: self.ip_dict[(first_level_sub, ips)] = 1 else: self.ip_dict[(first_level_sub, ips)] += 1 if self.ip_dict[(first_level_sub, ips)] > 30: continue self.found_count_local += 1 self.outfile.write(cur_domain.ljust(30) + ' ' + ips + ' ') self.outfile.flush() try: self.scan_count_local += 1 self.resolvers[j].query('lijiejie-test-not-existed.' + cur_domain) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e: if self.queue.qsize() < 10000: for _ in self.next_subs: self.queue.put((0, _ + '.' + sub)) else: self.queue.put((1, '{next_sub}.' + sub)) except Exception as e: pass except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer) as e: pass except dns.resolver.NoNameservers as e: self.queue.put((0, sub)) # Retry except dns.exception.Timeout as e: self.timeout_subs[sub] = self.timeout_subs.get(sub, 0) + 1 if self.timeout_subs[sub] <= 2: self.queue.put((0, sub)) # Retry except Exception as e: import traceback traceback.print_exc() with open('errors.log', 'a') as errFile: errFile.write('[%s] %s ' % (type(e), str(e))) |
siganal.signal,发起信号,当按下键盘的ctrl+c的时候 exit退出程序。run函数调用了协程。
在linux系统中,线程就是轻量级的进程,而轻量级的线程即微线程会被称为协程。
线程是在进程中做切换,协程则相当于在线程当中做切换。
如果队列不是空的,就从队列中取出,设定timeout为3,且本地扫描数+1。接着,和load_next_sub类似,如果队列中有符合条件的则进行替换,优先级增加。遍历子目录,如果sub在found_subs中就跳过,如果不在,扫描数量+1,当前域名cur_domain=sub.domain是把主域名前面加上子域名,用dns进行查询,如果存在则在子域名列表中增加,并把其对应的ip写在ips中,如果ip对应为1.1.1.1、127.0.0.1、0.0.0.0、0.0.0.1或是内网(通过common.py的is_intranet函数判断,函数如下)则跳过,并且查询cname,如果子域名,ip这个键值对不存在则设为1,存在则加1,如果字典值大于30就结束,找到的子域名总数加1,将子域名写到文件中。
1 2 3 4 5 6 7 8 9 10 11 | def is_intranet(ip): ret = ip.split('.') if len(ret) != 4: return True if ret[0] == '10': return True if ret[0] == '172' and 16 <= int(ret[1]) <= 31: return True if ret[0] == '192' and ret[1] == '168': return True return False |
以10开头或者172.16~31开头,或者192.168开头的判断为内网,返回True。
3.总体思路总结
总体的函数调用关系图如下所示,红线代表调用,白线代表程序执行顺序。
函数调用关系图.png
程序首先load_dns_servsers,根据字典获取DNS服务,然后测试服务是否可用。然后根据用户选择,通过load_next_sub进行子域名字典的读取,将子域名加在到队列中,拼接成完整域名,DNS服务连接从而判断其域名是否存在,多次循环迭代访问,得到最终的域名列表。然后run_process,用协程的方式进行扫描,scan函数为程序的主要功能函数,通过对每个子域名的DNS响应,根据其answer进行结果列表的添加。得到最终的子域名列表。