├── .gitignore ├── requirements.txt ├── config ├── test_sub.txt ├── config.py ├── html_template.py ├── remove_repeat.py └── next_subdomains.txt ├── scripts ├── base.py ├── virustotal.py ├── threatcrowd.py └── ca.py ├── LICENSE ├── lib ├── parser.py ├── title.py └── core.py ├── README.md └── subdomain.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | .vscode/ 4 | output 5 | # Environments 6 | .env 7 | .venv 8 | env/ 9 | venv/ 10 | ENV/ 11 | env.bak/ 12 | venv.bak/ 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiodns==3.0.0 2 | certifi==2019.6.16 3 | cffi==1.12.3 4 | chardet==3.0.4 5 | dnspython==2.1.0 6 | idna==2.8 7 | pycares==4.0.0 8 | pycparser==2.19 9 | requests==2.22.0 10 | urllib3==1.25.8 11 | -------------------------------------------------------------------------------- /config/test_sub.txt: -------------------------------------------------------------------------------- 1 | bbs 2 | a 3 | b 4 | c 5 | d 6 | test 7 | wjsso 8 | oa 9 | my 10 | eprint 11 | o 12 | j 13 | w 14 | www 15 | sms 16 | test 17 | app 18 | admin 19 | bdp 20 | oil 21 | 60s 22 | gsp 23 | ufs 24 | iotx 25 | -------------------------------------------------------------------------------- /scripts/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-30 18:05:15 6 | @LastEditTime: 2019-05-31 16:20:40 7 | ''' 8 | class Base(object): 9 | def __init__(self, scan_domain): 10 | self.scan_domain = scan_domain 11 | self.sub = set() 12 | self.enable = True 13 | 14 | def run(self): 15 | print("base") 16 | 17 | 18 | -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-30 16:08:13 6 | @LastEditTime: 2019-07-19 17:40:48 7 | ''' 8 | 9 | VERSION = "0.1" 10 | 11 | BANNER = """ 12 | 13 | __ __ _____ _ ______ _ 14 | \ \ / / / ___| | | | _ \ (_) 15 | \ V /_____\ `--. _ _| |__ | | | |___ _ __ ___ __ _ _ _ __ 16 | \ /______|`--. \ | | | '_ \| | | / _ \| '_ ` _ \ / _` | | '_ \ 17 | | | /\__/ / |_| | |_) | |/ / (_) | | | | | | (_| | | | | | 18 | \_/ \____/ \__,_|_.__/|___/ \___/|_| |_| |_|\__,_|_|_| |_| 19 | https://github.com/Ciyfly/Y-SubDomain Author: @Recar 20 | """ 21 | 22 | 23 | VIRUSTOTAL_APIKEY = "" 24 | WEIBU_APIKEY = "" 25 | # 是对重复的ip达到多少进行删除数据 jd的话差不多超过50个域名指向一个ip就可以删除了 26 | DNS_THRESHOLD = 50 -------------------------------------------------------------------------------- /config/html_template.py: -------------------------------------------------------------------------------- 1 | html_head = """ 2 | 3 | 4 | 5 | 6 | """ 7 | 8 | html_title ="""{0} 子域名扫描结果""" 9 | 10 | html_body_head = """""" 11 | 12 | html_body_title = """

{0} 子域名扫描结果

""" 13 | 14 | html_body_a = """

{0}

""" 15 | 16 | html_body_end = """""" 17 | 18 | html_style = """ 36 | """ 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Recar 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 | -------------------------------------------------------------------------------- /scripts/virustotal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-30 18:05:15 6 | @LastEditTime: 2019-06-27 17:10:18 7 | ''' 8 | from base import Base 9 | import sys 10 | sys.path.append("..") 11 | from y_subdomain.config.config import VIRUSTOTAL_APIKEY 12 | import requests 13 | import json 14 | 15 | class Scan(Base): 16 | """一分钟只能使用四次""" 17 | def __init__(self, scan_domain): 18 | super().__init__(scan_domain) 19 | self.name = "virustotal" 20 | self.base_url = 'https://www.virustotal.com/vtapi/v2/domain/report' 21 | 22 | def run(self): 23 | try: 24 | if not VIRUSTOTAL_APIKEY:# 如果没有配置 api key 直接返回空 25 | return set() 26 | params = {'apikey': VIRUSTOTAL_APIKEY,'domain': self.scan_domain} 27 | response = requests.get(self.base_url, params=params) 28 | if response.status_code == 200: 29 | data = json.loads(response.content).get("subdomains") 30 | for domain in data: 31 | self.sub.add(domain) 32 | return self.sub 33 | else: 34 | return set() 35 | except Exception as e: 36 | print("ERROR: "+self.name+" : "+str(e)) 37 | return set() 38 | 39 | -------------------------------------------------------------------------------- /config/remove_repeat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-06-11 18:04:51 6 | @LastEditTime: 2019-06-11 18:18:57 7 | ''' 8 | import os 9 | import sys 10 | 11 | def main(add_subdomains_path): 12 | print("start ") 13 | subs = set() 14 | base_path = os.path.dirname(os.path.abspath(__file__)) 15 | big_subdomains_path = os.path.join(base_path, "big_subdomains.txt") 16 | tmp_file_path = os.path.join(base_path, "tmp.txt") 17 | 18 | with open(big_subdomains_path, "r") as f: 19 | line = f.readline() 20 | while line: 21 | subs.add(line) 22 | line = f.readline() 23 | old_dic_len = len(subs) 24 | with open(add_subdomains_path, "r") as f: 25 | line = f.readline() 26 | while line: 27 | subs.add(line) 28 | line = f.readline() 29 | print(f"add dict: {len(subs) - old_dic_len}") 30 | print("remove repeat after: {0} ".format(len(subs))) 31 | with open(tmp_file_path, "w") as f: 32 | for sub in subs: 33 | f.write(str(sub)) 34 | os.remove(big_subdomains_path) 35 | print(big_subdomains_path) 36 | os.rename(tmp_file_path, big_subdomains_path) 37 | print("end ") 38 | 39 | if __name__ == "__main__": 40 | add_subdomains_path = sys.argv[1] 41 | print(f"merge {add_subdomains_path}") 42 | main(add_subdomains_path) -------------------------------------------------------------------------------- /scripts/threatcrowd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-30 18:05:15 6 | @LastEditTime: 2019-06-27 17:09:51 7 | ''' 8 | from base import Base 9 | import requests 10 | import json 11 | 12 | class Scan(Base): 13 | def __init__(self, scan_domain): 14 | super().__init__(scan_domain) 15 | self.name = "threatcrowd" 16 | self.base_url = "https://www.threatcrowd.org/searchApi/v2/domain/report/?domain={0}" 17 | self.headers = { 18 | 'authority': 'www.threatcrowd.org', 19 | 'accept': 'application/json', 20 | 'content-type': 'application/json', 21 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36' 22 | } 23 | self.enable = False 24 | 25 | def run(self): 26 | try: 27 | get_url = self.base_url.format(self.scan_domain) 28 | response = requests.get(url=get_url, headers = self.headers ) 29 | if response.status_code == 200: 30 | data = json.loads(response.content).get('subdomains') 31 | # data = str(data,encoding='utf-8') 32 | for domain in data: 33 | self.sub.add(domain) 34 | return self.sub 35 | else: 36 | return set() 37 | except Exception as e: 38 | print("ERROR: "+self.name+" : "+str(e)) 39 | return set() 40 | -------------------------------------------------------------------------------- /config/next_subdomains.txt: -------------------------------------------------------------------------------- 1 | test 2 | test2 3 | test3 4 | test4 5 | t 6 | dev 7 | 1 8 | 2 9 | 3 10 | s1 11 | s2 12 | s3 13 | admin 14 | adm 15 | a 16 | b 17 | c 18 | m 19 | ht 20 | adminht 21 | webht 22 | web 23 | gm 24 | sys 25 | system 26 | manage 27 | manager 28 | mgr 29 | passport 30 | bata 31 | wei 32 | weixin 33 | wechat 34 | wx 35 | wiki 36 | upload 37 | ftp 38 | pic 39 | jira 40 | zabbix 41 | nagios 42 | bug 43 | bugzilla 44 | sql 45 | mysql 46 | db 47 | stmp 48 | pop 49 | imap 50 | mail 51 | zimbra 52 | exchange 53 | forum 54 | bbs 55 | list 56 | count 57 | counter 58 | img 59 | img01 60 | img02 61 | img03 62 | img04 63 | api 64 | cache 65 | js 66 | css 67 | app 68 | apps 69 | wap 70 | sms 71 | zip 72 | monitor 73 | proxy 74 | update 75 | upgrade 76 | stat 77 | stats 78 | data 79 | portal 80 | blog 81 | autodiscover 82 | en 83 | search 84 | so 85 | oa 86 | database 87 | home 88 | sso 89 | help 90 | vip 91 | s 92 | w 93 | down 94 | download 95 | downloads 96 | dl 97 | svn 98 | git 99 | log 100 | staff 101 | vpn 102 | sslvpn 103 | ssh 104 | scanner 105 | sandbox 106 | ldap 107 | lab 108 | go 109 | demo 110 | console 111 | cms 112 | auth 113 | crm 114 | erp 115 | res 116 | static 117 | old 118 | new 119 | beta 120 | image 121 | service 122 | login 123 | 3g 124 | docs 125 | it 126 | e 127 | live 128 | library 129 | files 130 | i 131 | d 132 | cp 133 | connect 134 | gateway 135 | lib 136 | preview 137 | backup 138 | share 139 | status 140 | assets 141 | user 142 | vote 143 | bugs 144 | cas 145 | feedback 146 | id 147 | edm 148 | survey 149 | union 150 | ceshi 151 | dev1 152 | updates 153 | phpmyadmin 154 | pma 155 | edit 156 | master 157 | xml 158 | control 159 | profile 160 | zhidao 161 | tool 162 | toolbox 163 | boss 164 | activity 165 | www 166 | -------------------------------------------------------------------------------- /lib/parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-30 16:07:49 6 | LastEditTime: 2021-05-20 14:56:30 7 | ''' 8 | from optparse import OptionParser 9 | from config.config import VERSION, BANNER 10 | USAGE = "python domain -d xxx.com" 11 | import sys 12 | def get_options(): 13 | print(BANNER) 14 | parser = OptionParser(usage=USAGE,version=VERSION) 15 | 16 | parser.add_option('-d', type=str, dest="domain", help="指定要测试的域名") 17 | 18 | parser.add_option('-e', type=str, dest="engine", help="指定使用的引擎 逗号间隔") 19 | 20 | parser.add_option('-c', type=str, dest="sub_dict", help="指定使用的字典 不指定默认使用默认的") 21 | 22 | parser.add_option('-f', type=str, dest="domain_file", help="指定域名列表文件 默认使用小字典2w") 23 | 24 | parser.add_option('-t', type=int, dest="thread_count", help="指定线程数 默认500") 25 | 26 | parser.add_option('--private',action='store_true', dest="is_private", default=False, help="是否对内网ip进行清除") 27 | 28 | parser.add_option('--title',action='store_true', dest="get_title", default=False, help="是否直接获取域名的title信息") 29 | 30 | parser.add_option('--gen',action='store_true', dest="gen_rule", default=False, help="是否加上动态规则生成的字典") 31 | 32 | parser.add_option('--exh',action='store_true', dest="exhaustion", default=False, help="是否进行暴力穷举") 33 | 34 | parser.add_option('--exo',action='store_true', dest="exhaustion_only", default=False, help="只进行暴力穷举") 35 | 36 | parser.add_option('--next',action='store_true', dest="next_sub", default=False, help="默认穷举到三级域名 开启则穷举到四级域名") 37 | 38 | parser.add_option('--big_dict',action='store_true', dest="big_dict", default=False, help="默认使用小字典2w 开启则使用大字典200w") 39 | 40 | parser.add_option('--json',action='store_true', dest="is_json", default=False, help="是否生成json报告") 41 | 42 | parser.add_option('--html',action='store_true', dest="is_html", default=False, help="是否生成html报告") 43 | (options,args) = parser.parse_args() 44 | if options.domain==None and options.domain_file==None: 45 | parser.print_help() 46 | sys.exit(0) 47 | if options.domain: 48 | if "www" in options.domain or "http://" in options.domain : 49 | options.domain =options.domain.replace("www.","").replace("http://", "") 50 | if options.engine: 51 | options.engine = options.engine.split(",") 52 | 53 | return options,args 54 | -------------------------------------------------------------------------------- /scripts/ca.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import asyncio 4 | import aiodns 5 | import socket 6 | import ssl 7 | import traceback 8 | import sys 9 | sys.path.append("../") 10 | from base import Base 11 | 12 | class Scan(Base): 13 | def __init__(self, scan_domain): 14 | super().__init__(scan_domain) 15 | self.name = "ca" 16 | 17 | @staticmethod 18 | def query_a(domain): 19 | """ 20 | 查询根域名A记录 21 | :param domain: 被查询的域名(某些情况下根域名可能没有A记录,可尝试www的A记录) 22 | :return: 23 | """ 24 | loop = asyncio.new_event_loop() 25 | asyncio.set_event_loop(loop) 26 | resolver = aiodns.DNSResolver(loop=loop) 27 | f = resolver.query(domain, 'A') 28 | result = loop.run_until_complete(f) 29 | # 可以多个IP都尝试下,但正常情况下没有区别 30 | return result[0].host 31 | 32 | @staticmethod 33 | def get_cert_domains(ip): 34 | """ 35 | 向IP的443端口查询支持的域名情况 36 | :param ip: 37 | :return: 38 | """ 39 | s = socket.socket() 40 | s.settimeout(2) 41 | cert_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cacert.pem') 42 | connect = ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED, ca_certs=cert_path) 43 | connect.settimeout(2) 44 | connect.connect((ip, 443)) 45 | cert_data = connect.getpeercert().get('subjectAltName') 46 | return cert_data 47 | 48 | def run(self, only_subdomains=False): 49 | """ 50 | 根据HTTPS证书获取支持的域名 51 | :param only_subdomains: 是否只需要子域名,比如搜索baidu.com只需要*.baidu.com,不需要baidu.cn 52 | :return: 53 | """ 54 | domains = [] 55 | try: 56 | ip = self.query_a(self.scan_domain) 57 | cert_domains = self.get_cert_domains(ip) 58 | for cert_domain in cert_domains: 59 | domain = cert_domain[1] 60 | if not domain.startswith('*'): 61 | if only_subdomains: 62 | if domain.endswith(self.scan_domain): 63 | domains.append(domain) 64 | else: 65 | domains.append(domain) 66 | return set(domains) 67 | except Exception as e: 68 | return set(domains) 69 | 70 | 71 | # if __name__ == '__main__': 72 | # try: 73 | # ret_domains = Scan('baidu.com').run(only_subdomains=False) 74 | # print(ret_domains) 75 | # except Exception as e: 76 | # traceback.print_exc() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # y_subdomain 2 | 3 | 子域名获取工具 4 | 5 | 6 | ## 使用 7 | 8 | 使用加速下载 9 | `git clone https://github.com.cnpmjs.org/Ciyfly/y_subdomain.git` 10 | 11 | **如果是下载的zip 或者tar包需要修改文件夹名为 y_subdomain** 12 | 13 | **基于python3 linux环境下** 14 | 15 | 适用于 扫描器前置信息收集 简单子域名获取 学习等 16 | 基本使用内置原生库 17 | 18 | [![asciicast](https://asciinema.org/a/m7mqlsHux1TinM2oWB6D6LZoD.png)](https://asciinema.org/a/m7mqlsHux1TinM2oWB6D6LZoD) 19 | 20 | 21 | ### 泛解析的解决办法 22 | 先测试一个不存在的域名然后是否成功解析 23 | 24 | 25 | ### 为什么要做 26 | 1. 重复造轮子 深入理解轮子 做出更好用的轮子 27 | 2. 是大扫描器的信息收集的一部分功能的实现 28 | 3. 工作中擅于使用轮子 学习中擅于造轮子 29 | 30 | 31 | ## 实现 32 | ### 接口的实现 33 | 这里参考 poc的形式 动态的从script文件夹下的脚本 34 | 动态载入实例化并执 35 | 继承Base类默认有个 `self.enable=True` 可以控制是否开启脚本 36 | 如果使用 -e 指定接口 enable是False也会强制执行 37 | 对于接口返回的域名会使用线程池再去dns解析验证 (默认线程池大小 50) 38 | 39 | **接口引擎脚本完成:** 40 | 1. 百度云检测 41 | 2. hackertarget 42 | 3. virustotal (**需要在config/config.py 中添加api key才能使用不然默认不会执行**) 43 | 4. 通过证书获取 44 | 45 | ### 暴力穷举的实现 46 | 一口气加载字典到内存中 占内存不大 47 | 使用共享队列+多线程的形式进行解析dns 默认线程开启100个 48 | 这个地方有进度条功能 可以在实例化穷举类的时候进行选择是否开启 49 | 50 | 51 | ## 版本 52 | 53 | **v1.2** 54 | 2021 0520 55 | 去掉大字典 影响下载速度 56 | 保存的结果拆分为 ip ipc段 域名 域名+ip 57 | 增加获取title功能 `--title` 58 | 增加动态生成字典加入测试字典功能 `--gen` 59 | 60 | **v1.1.0** 61 | 修改一些bug 62 | 增加三级四级域名穷举 63 | 修改为默认小字典2w (使用的onforall的字典) 64 | 指定`big_dict` 可以使用大字典200w 65 | 增加可以指定字典参数 66 | 穷举增加超时时间 默认为2h 67 | dns增加超时时间10s 68 | 69 | **v1.0.0** 70 | 修改bug 71 | 进度条增加find输出 72 | 增加对内网ip的剔除参数 73 | 增加指定字典的参数 74 | 75 | **V0.2** 76 | 修改 穷举采用共享队列+多线程的形式 77 | 去除多余代码 78 | 修改了代码结构 便于作为api使用 79 | 穷举增加了进度条 80 | 扩大了字典 目前字典 2350706 (235w) (跑一次大概 40分钟左右) 81 | 增加了HUP小调试 发起HUP信号会输出 此时队列大小 (在 api形式使用不输出进度条的时候 如果长时间未结束可调试使用) 82 | `kill -HUP 进程id` 83 | 84 | **V0.1** 85 | 采用多接口的形式获取域名 动态插件形式添加接口脚本 86 | 接口验证和穷举都采用线程池的形式进行dns解析 87 | 88 | 89 | 90 | # api形式使用 91 | 使用接口解析 92 | ```python 93 | from y_subdomain.lib.core import EngineScan 94 | 95 | engine_scan = EngineScan(scan_domain, engine) 96 | # scan_domain 为扫描的域名 engine 是指定的接口 空的话就全部都跑 默认也是全部都跑 97 | engine_domain_ips_dict = engine_scan.run() 98 | # 调用 run方法会返回 解析的结果 是 字典形式 子域名对应ip列表 99 | 100 | 接口类如下: 101 | class EngineScan(object): 102 | """接口解析类 103 | :param scan_domain 测试域名 104 | :param engine 指定引擎列表 默认全部 105 | :param thread_count 线程 默认100 106 | :param get_black_ip 是否返回泛解析的ip 默认不返回 107 | :param is_private 是否保留内网ip 默认不保留 108 | :return domain_ips_dict, (black_ip) 选择返回泛解析ip则有第二个 否则只返回域名对于ip 109 | 110 | """ 111 | ``` 112 | 使用穷举解析 113 | ```python 114 | from y_subdomain.lib.core import ExhaustionScan 115 | scan_domain = "测试域名" # 穷举只支持单个域名 116 | exhaustion_scan = ExhaustionScan(scan_domain, thread_count=100, is_output=True) 117 | # is_output 是否输出进度条 默认是False的 118 | exh_domain_ips_dict = exhaustion_scan.run() 119 | 120 | 穷举类如下: 121 | class ExhaustionScan(object): 122 | """暴力穷举 123 | :param scan_domain 要测试的域名 124 | :param thread_count 线程数 默认100 125 | :param is_output 是否输出进度条 默认不输出 126 | :param black_ip 泛解析的ip 默认为空 127 | :param is_private 是否保留内网ip 默认不保留 128 | :param sub_dict 指定的字典 默认读取配置文件下字典 129 | :param next_sub 是否是三级或者四级的域名扫描 130 | :param timeout 超时时间 默认穷举超时时间为 2h 131 | :return domain_ips_dict 域名对应解析的ip结果 132 | 133 | """ 134 | ``` 135 | 136 | ## update 137 | 2019 0813 138 | 修改一些bug 139 | 增加三级四级域名穷举 140 | 修改为默认小字典2w (使用的onforall的字典) 141 | 指定`big_dict` 可以使用大字典200w 142 | 增加可以指定字典参数 143 | 穷举增加超时时间 默认为2h 144 | dns增加超时时间10s 145 | 146 | --- 147 | 148 | 2019 0719 149 | 修改bug 150 | 进度条增加find输出 151 | 增加对内网ip的剔除参数 152 | 增加指定字典的参数 153 | 154 | --- 155 | 2019 0711 156 | 增加对内网ip的忽略 默认是清除掉的 可以通过命令行参数进行控制 157 | 增加指定字典的功能参数 158 | 增加只进行穷举的功能参数 159 | 没有把去除内网ip的方法写到savedata中是为了当接口api使用的时候 对内网ip的处理类内部就进行处理 160 | 161 | --- 162 | 2019 0709 163 | 经测试 修改对于泛解析的解决 164 | 如果随机字符串跑出来泛解析还继续进行穷举但是过滤掉这个泛解析的ip 165 | 对于接口里的也进行泛解析剔除 166 | 对于暴力穷举的会默认使用配置文件中的 50 阈值 作为判断泛解析的条件 当超过50个域名指向一个ip进行剔除 167 | 168 | 169 | # TODO 170 | 多增加几个模块 171 | 优化下代码结构 172 | 173 | ## 参考 174 | 175 | [ESD](https://github.com/FeeiCN/ESD) 176 | 177 | [wydomain](https://github.com/ring04h/wydomain) 178 | -------------------------------------------------------------------------------- /lib/title.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | Author: Recar 5 | Date: 2020-09-02 10:31:45 6 | LastEditTime: 2021-03-25 18:25:54 7 | ''' 8 | from optparse import OptionParser 9 | import traceback 10 | import threading 11 | import urllib3 12 | import logging 13 | import signal 14 | try: 15 | import queue 16 | except: 17 | import Queue as queue 18 | import time 19 | import sys 20 | import re 21 | import os 22 | 23 | 24 | logging.getLogger("urllib3").setLevel(logging.ERROR) 25 | urllib3.disable_warnings() 26 | header = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.108 Safari/537.36'} 27 | 28 | result = [] 29 | 30 | def ctrl_c(signum, frame): 31 | print() 32 | print("ctrl c") 33 | sys.exit() 34 | # ctrl+c 35 | signal.signal(signal.SIGINT, ctrl_c) 36 | 37 | class BaseWork(object): 38 | def __init__(self, consumer_count=50): 39 | self.consumer_count = consumer_count 40 | self.work_queue = queue.Queue() 41 | 42 | def put(self, item): 43 | ''' 44 | @params item 数据 45 | ''' 46 | try: 47 | if type(item) == list: 48 | for d in item: 49 | self.work_queue.put(d) 50 | else: 51 | self.work_queue.put(item) 52 | except Exception as e: 53 | pass 54 | 55 | def producer(self, func): 56 | pass 57 | 58 | def consumer(self, func): 59 | ''' 60 | 消费者 61 | @params func 消费者函数 62 | ''' 63 | while not self.work_queue.empty(): 64 | item = self.work_queue.get(timeout=3) 65 | if item is None: 66 | break 67 | func(item) 68 | 69 | def run(self, consumer_func): 70 | ''' 71 | 运行方法 72 | @params consumer_func 消费者函数 73 | ''' 74 | start_time = time.time() 75 | threads = [] 76 | for i in range(self.consumer_count): 77 | t = threading.Thread(target=self.consumer,args=(consumer_func,)) 78 | t.setDaemon(True) 79 | t.start() 80 | threads.append(t) 81 | while not self.work_queue.empty(): 82 | # logging.debug("queue size: {0}".format(self.work_queue.qsize())) 83 | time.sleep(1) 84 | alive = True 85 | while alive: 86 | alive = False 87 | for thread in threads: 88 | if thread.isAlive(): 89 | alive = True 90 | time.sleep(0.1) 91 | use_time = time.time() - start_time 92 | 93 | class Worker(BaseWork): 94 | '''普通消费队列''' 95 | def __init__(self, consumer_count=50): 96 | super(Worker, self).__init__(consumer_count) 97 | 98 | class WorkerPrior(BaseWork): 99 | '''优先消费队列''' 100 | def __init__(self, consumer_count=50): 101 | super(WorkerPrior, self).__init__(consumer_count) 102 | from queue import PriorityQueue 103 | self.work_queue = PriorityQueue() 104 | 105 | def put(self, item, priority=1): 106 | ''' 107 | @params item 数据 108 | @params priority 优先级 默认是1 109 | ''' 110 | try: 111 | if type(item) == list: 112 | for d in item: 113 | self.work_queue.put((priority, d)) 114 | else: 115 | self.work_queue.put((priority, item)) 116 | except Exception as e: 117 | pass 118 | 119 | def consumer(self, func): 120 | ''' 121 | 消费者 122 | @params func 消费者函数 123 | ''' 124 | while not self.work_queue.empty(): 125 | item = self.work_queue.get(timeout=3) 126 | priority, data = item 127 | if data is None: 128 | break 129 | func(data) 130 | 131 | 132 | class GetTitle(object): 133 | def __init__(self, urls, logger=logging): 134 | self.urls = urls 135 | self.logger = logger 136 | self.result = list() 137 | 138 | def get_title(self, url): 139 | try: 140 | http = urllib3.PoolManager() 141 | response = http.request('GET',url,headers=header,timeout=1) 142 | re_title = re.findall("(.+)", response.data.decode()) 143 | title = re_title[0] if re_title else "" 144 | info = { 145 | "status_code": response.status, 146 | "title": title, 147 | } 148 | return info 149 | except Exception as e: 150 | return None 151 | 152 | def create_worker(self): 153 | worker = Worker(consumer_count=100) 154 | for url in self.urls: 155 | worker.put({ 156 | "url": url 157 | }) 158 | return worker 159 | 160 | def consumer(self, data): 161 | url = data["url"] 162 | info = self.get_title(url) 163 | if not info: 164 | return 165 | output = "{:30}\t{:5}\t{:30}\n".format(url, info["status_code"], info["title"]) 166 | self.logger.info("[+] {0}".format(output)) 167 | self.result.append(output) 168 | 169 | def run(self): 170 | worker = self.create_worker() 171 | self.logger.info("[*] task create success {0}".format(worker.work_queue.qsize())) 172 | output = "{:30}\t{:5}\t{:30}".format("url","status", "title") 173 | self.logger.info("[*] {0}".format(output)) 174 | worker.run(self.consumer) 175 | self.logger.info("[+] find title: {0}".format(len(self.result))) 176 | return self.result 177 | 178 | -------------------------------------------------------------------------------- /subdomain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-15 18:40:51 6 | LastEditTime: 2021-05-20 17:38:54 7 | ''' 8 | 9 | from lib.parser import get_options 10 | from lib.core import ( 11 | EngineScan, ExhaustionScan, SaveDate, 12 | print_log, print_info, print_progress, 13 | GenSubdomain 14 | ) 15 | from lib.title import GetTitle 16 | import time 17 | import sys 18 | import os 19 | 20 | def main(): 21 | # 获取命令行参数 22 | options,args = get_options() 23 | scan_domain = options.domain 24 | domain_file = options.domain_file 25 | is_html = options.is_html 26 | is_json = options.is_json 27 | is_private = options.is_private 28 | engine = options.engine 29 | thread_count = options.thread_count 30 | sub_dict = options.sub_dict 31 | big_dict = options.big_dict 32 | exhaustion = options.exhaustion 33 | exhaustion_only = options.exhaustion_only 34 | get_title = options.get_title 35 | gen_rule = options.gen_rule 36 | domains = list() 37 | if not thread_count: 38 | thread_count = 500 39 | if domain_file: 40 | with open(domain_file, "r") as f: 41 | for domain in f: 42 | domains.append(domain.strip()) 43 | else: 44 | domains.append(scan_domain) 45 | for scan_domain in domains: 46 | print_info("scan {0}\n".format(scan_domain)) 47 | 48 | engine_domain_ips_dict =None # 初始化 49 | if not exhaustion_only: # 是否只进行穷举解析 50 | # # 接口解析 51 | start = time.perf_counter() 52 | engine_scan = EngineScan(scan_domain, engine, is_private=is_private) 53 | engine_domain_ips_dict = engine_scan.run() 54 | print_info(len(engine_domain_ips_dict)) 55 | engine_end = (time.perf_counter() - start) 56 | print_info(f"引擎接口消耗时间:{engine_end}s") 57 | 58 | exh_domain_ips_dict = None 59 | all_exh_domain_ips_dict = dict() 60 | if exhaustion or exhaustion_only: 61 | # 穷举解析 62 | start = time.perf_counter() 63 | exhaustion_scan = ExhaustionScan( 64 | scan_domain, thread_count=thread_count, 65 | is_output=True, is_private=is_private, 66 | sub_dict = sub_dict, big_dict=big_dict, 67 | gen_rule=gen_rule 68 | ) 69 | exh_domain_ips_dict = exhaustion_scan.run() 70 | if exh_domain_ips_dict: 71 | print_info(f"穷举发现 : {len(exh_domain_ips_dict)}") 72 | all_exh_domain_ips_dict.update(exh_domain_ips_dict) 73 | exh_end = (time.perf_counter() - start) 74 | print_info(f"穷举消耗时间:{exh_end}s") 75 | print_info(f"开始三级域名穷举") 76 | engine_exh_domain = set() 77 | # 先去重 78 | if engine_domain_ips_dict: 79 | for domain in engine_domain_ips_dict.keys(): 80 | engine_exh_domain.add(domain) 81 | for domain in list(exh_domain_ips_dict.keys()): 82 | engine_exh_domain.add(domain) 83 | # 三级域名穷举 84 | three_exh_domain_ips_dict = dict() 85 | # all size 用于输出进度条 86 | all_size = len(engine_exh_domain) 87 | start = time.perf_counter() 88 | for i, domain in enumerate(engine_exh_domain): 89 | print_info(f"开始测试 {domain}") 90 | next_exhaustion_scan = ExhaustionScan( 91 | domain, thread_count=100, 92 | is_output=False, is_private=is_private, 93 | next_sub=True 94 | ) 95 | # run & update result echo progress 96 | next_exh_domain_ips_dict = next_exhaustion_scan.run() 97 | three_exh_domain_ips_dict.update(next_exh_domain_ips_dict) 98 | all_exh_domain_ips_dict.update(next_exh_domain_ips_dict) 99 | # 输出进度条 100 | print_progress(all_size-i, all_size, start, len(three_exh_domain_ips_dict)) 101 | print() 102 | print_info(f"三级域名穷举发现 : {len(three_exh_domain_ips_dict)}") 103 | # 如果穷举的三级域名有结果则进行四级域名穷举 104 | four_exh_domain_ips_dict = dict() 105 | all_size = len(three_exh_domain_ips_dict) 106 | start = time.perf_counter() 107 | if three_exh_domain_ips_dict: 108 | print_info("开始四级域名穷举") 109 | for i, domain in enumerate(three_exh_domain_ips_dict.keys()): 110 | next_exhaustion_scan = ExhaustionScan( 111 | domain, thread_count=100, 112 | is_output=False, is_private=is_private, 113 | next_sub=True 114 | ) 115 | next_exh_domain_ips_dict = next_exhaustion_scan.run() 116 | four_exh_domain_ips_dict.update(next_exh_domain_ips_dict) 117 | all_exh_domain_ips_dict.update(next_exh_domain_ips_dict) 118 | # 输出进度条 119 | print_progress(all_size-i, all_size, start, len(four_exh_domain_ips_dict)) 120 | print() 121 | print_info(f"四级域名穷举发现: {len(four_exh_domain_ips_dict)}") 122 | print_info(f"所有穷举发现: {len(all_exh_domain_ips_dict)}") 123 | print_info("start save result") 124 | # 保存结果 125 | save_data = SaveDate( 126 | scan_domain, 127 | engine_domain_ips_dict= engine_domain_ips_dict, 128 | exh_domain_ips_dict=all_exh_domain_ips_dict, 129 | is_text=True, 130 | is_json=is_json, 131 | is_html=is_html 132 | ) 133 | domain_ips_dict = save_data.save_doamin_ips() 134 | # 增加title 135 | if not get_title: 136 | return 137 | urls = domain_ips_dict.keys() 138 | print_info("get title") 139 | title_result = GetTitle(urls).run() 140 | print_info(f"title get count: {len(title_result)}") 141 | title_path = os.path.join(save_data.out_out_dir, "title.txt") 142 | with open(title_path, "w") as f: 143 | for title in title_result: 144 | f.write(title) 145 | print_info(f"title save: {title_path}") 146 | 147 | 148 | if __name__ == "__main__": 149 | main() 150 | -------------------------------------------------------------------------------- /lib/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/import pdb;pdb.set_trace() 2 | # coding=UTF-8 3 | ''' 4 | @Author: recar 5 | @Date: 2019-05-30 17:49:08 6 | LastEditTime: 2021-05-20 17:19:42 7 | ''' 8 | 9 | import sys 10 | sys.path.append("..") 11 | from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor 12 | from collections import defaultdict 13 | from y_subdomain.config.html_template import ( 14 | html_head, html_title, html_body_head, html_body_title, 15 | html_body_a, html_body_end, html_style 16 | ) 17 | 18 | from y_subdomain.config.config import DNS_THRESHOLD 19 | import dns 20 | from dns import resolver 21 | import os 22 | import importlib 23 | import ipaddress 24 | import sys 25 | import re 26 | import json 27 | import random 28 | import string 29 | import queue 30 | import threading 31 | import time 32 | import signal 33 | 34 | 35 | def ctrl_c(signum,frame): 36 | print() 37 | print("[-] input ctrl c") 38 | sys.exit() 39 | 40 | # def print_queue_size(signum, frame): 41 | # print() 42 | # print(f"[*] queue size: {frame.f_locals['self'].sub_dict_queue.qsize()}") 43 | # print(f"[*] find subdomain: {len(frame.f_locals['self'].domain_ips_dict)}") 44 | 45 | # ctrl+c 46 | signal.signal(signal.SIGINT, ctrl_c) 47 | # HUP 48 | # signal.signal(signal.SIGHUP, print_queue_size) 49 | 50 | def print_log(message): 51 | # ljust(50) 实现长度不够存在显示残留 左对齐以空格达到指定长度 52 | print ("\r[*] {0}".format(message).ljust(50), end="") 53 | 54 | def print_flush(): 55 | print ("\r\r", end="") 56 | 57 | def print_info(message): 58 | print(("[+] {0}".format(message))) 59 | 60 | def print_debug(message): 61 | print("[*] {0}".format(message)) 62 | 63 | def print_error(message): 64 | print(("\n[-] {0}".format(message))) 65 | 66 | def print_progress(currne_size, all_size, start, find_size): 67 | """输出进度条 68 | :param currne_size 当前队列大小 69 | :param all_size 总体队列大小 70 | :param start 开启时间 71 | :param find_size 找到多少条 72 | """ 73 | out_u = int(currne_size/all_size*50) # ## 74 | out_l = 50 - out_u 75 | percentage = 100-(currne_size/all_size*100) 76 | use_time = time.perf_counter() - start 77 | print( 78 | '\r'+'[' + '>' * out_l + '-' * out_u +']' 79 | + f'{percentage:.2f}%' 80 | + f'|size: {currne_size}' 81 | + f'|use time: {use_time:.2f}s' 82 | + f'|find: {find_size} ', end="") 83 | 84 | class SaveDate(object): 85 | """用于保存域名结果""" 86 | def __init__( 87 | self, scan_domain, engine_domain_ips_dict=None, exh_domain_ips_dict=None, 88 | is_text=False, is_json=False, is_html=False 89 | ): 90 | self.engine_domain_ips_dict = engine_domain_ips_dict 91 | self.exh_domain_ips_dict = exh_domain_ips_dict 92 | self.domain_ips_dict = dict() 93 | self.clean_data() 94 | self.scan_domain = scan_domain 95 | self.is_text = is_text 96 | self.is_json = is_json 97 | self.is_html = is_html 98 | 99 | self.get_output() 100 | 101 | def clean_data(self): 102 | if self.engine_domain_ips_dict and not self.exh_domain_ips_dict: 103 | # 只有 engine_domain_ips_dict 104 | self.domain_ips_dict = self.engine_domain_ips_dict 105 | elif self.exh_domain_ips_dict and not self.engine_domain_ips_dict: 106 | # 只有 exh_domain_ips_dict 107 | self.domain_ips_dict = self.exh_domain_ips_dict 108 | elif self.engine_domain_ips_dict and self.exh_domain_ips_dict: 109 | # 都有 110 | for domain, ips in self.engine_domain_ips_dict.items(): 111 | if domain in self.exh_domain_ips_dict.keys(): 112 | self.exh_domain_ips_dict[domain] = self.exh_domain_ips_dict[domain] + ips 113 | else: 114 | self.exh_domain_ips_dict[domain] = ips 115 | self.domain_ips_dict = self.exh_domain_ips_dict 116 | 117 | def get_output(self): 118 | base_path = os.path.dirname(os.path.abspath(__file__)) 119 | output_dir = os.path.join(base_path, "../", "output", self.scan_domain) 120 | if not os.path.isdir(output_dir): 121 | os.makedirs(output_dir) 122 | self.out_out_dir = output_dir 123 | self.output_txt = os.path.join(output_dir, "subdomain_ips.txt") 124 | self.output_html = os.path.join(output_dir, "subdomain_ips.html") 125 | self.output_json = os.path.join(output_dir, "subdomain_ips.json") 126 | 127 | def save_text(self): 128 | with open(self.output_txt, "w") as f: 129 | for domain, ips in self.domain_ips_dict.items(): 130 | f.write(f"{domain} {ips}\n") 131 | print_info("save txt success\n") 132 | 133 | def save_json(self): 134 | with open(self.output_json, "w") as f: 135 | json.dump(self.domain_ips_dict, f, indent=3) 136 | print_info("save json success\n") 137 | 138 | def save_html(self): 139 | html = html_head 140 | html += html_title.format(self.scan_domain) 141 | html += html_body_head 142 | html += html_body_title.format(self.scan_domain) 143 | for domain in self.domain_ips_dict.keys(): 144 | html += html_body_a.format(domain) 145 | html += html_body_end 146 | html += html_style 147 | with open(self.output_html, "w") as f: 148 | f.write(html) 149 | print_info("save html success\n") 150 | 151 | def save_other(self): 152 | subdomain_txt = os.path.join(self.out_out_dir, "subdomain.txt") 153 | ip_txt = os.path.join(self.out_out_dir, "ip.txt") 154 | ipc_txt = os.path.join(self.out_out_dir, "ipc.txt") 155 | domains = self.domain_ips_dict.keys() 156 | ips_list = self.domain_ips_dict.values() 157 | ipcs = list() 158 | for ips in ips_list: 159 | for ip in ips: 160 | ip_net = ".".join([str(i) for i in ip.split(".")[0:-1]+[0]]) 161 | ipcs.append(ip_net+"/24") 162 | ipcs = list(set(ipcs)) 163 | with open(subdomain_txt, "w") as f: 164 | for domain in domains: 165 | f.write(domain+"\n") 166 | with open(ip_txt, "w") as f: 167 | for ips in ips_list: 168 | for ip in ips: 169 | f.write(ip+"\n") 170 | with open(ipc_txt, "w") as f: 171 | for ipc in ipcs: 172 | f.write(ipc+"\n") 173 | 174 | def save_doamin_ips(self): 175 | # 应朋友需求这里将结果都分开 176 | if not self.domain_ips_dict: # 空的话就不保存文件 177 | return 178 | print_info(f"output_dir {self.out_out_dir}") 179 | if self.is_text: 180 | self.save_text() 181 | if self.is_json: 182 | self.save_json() 183 | if self.is_html: 184 | self.save_html() 185 | self.save_other() 186 | return self.domain_ips_dict 187 | 188 | class EngineScan(object): 189 | """接口解析类 190 | :param scan_domain 测试域名 191 | :param engine 指定引擎列表 默认全部 192 | :param thread_count 线程 默认100 193 | :param get_black_ip 是否返回泛解析的ip 默认不返回 194 | :param is_private 是否保留内网ip 默认不保留 195 | :return domain_ips_dict, (black_ip) 选择返回泛解析ip则有第二个 否则只返回域名对于ip 196 | 197 | """ 198 | def __init__(self, scan_domain, engine=None, thread_count=100, get_black_ip=False, is_private=False): 199 | self.scan_domain = scan_domain 200 | self.engine = engine 201 | self.thread_count = thread_count 202 | # dns 203 | #self.resolver = resolver 204 | self.resolver = dns.resolver.Resolver() 205 | # 设置dns超时时间 206 | self.resolver.timeout = 2 207 | self.resolver.lifetime = 2 208 | #self.resolver.nameservers=['8.8.8.8', '114.114.114.114'] 209 | # 存储变量 210 | self.domains_set = set() 211 | self.domain_ips_dict = defaultdict(list) 212 | self.get_black_ip = get_black_ip 213 | # 去除泛解析 214 | self.black_ip = list() 215 | # {ip:{ domains: [域名], count: 计数} } 216 | self.ip_domain_count_dict = dict() 217 | # 去除内网ip 218 | self.is_private = is_private 219 | def add_domain(self, domains): 220 | # 去除掉其他后缀不是 .scan_domain的域名 221 | for domain in domains: 222 | if domain.endswith(f".{self.scan_domain}"): 223 | self.domains_set.add(domain) 224 | 225 | def run_scripts(self): 226 | base_path = os.path.dirname(os.path.abspath(__file__)) 227 | scripts_path = os.path.join(base_path, "../","scripts") 228 | # 添加到搜索路径 229 | sys.path.append(scripts_path) 230 | scrips_list = list() 231 | scripts_class = list() 232 | if not self.engine: # 没有指定引擎 遍历scrips文件夹 233 | for root, dirs, files in os.walk(scripts_path): 234 | for filename in files: 235 | name = os.path.splitext(filename)[0] 236 | suffix = os.path.splitext(filename)[1] 237 | if suffix == '.py' and name!="base": 238 | metaclass=importlib.import_module(os.path.splitext(filename)[0]) 239 | # 通过脚本的 enable属性判断脚本是否执行 240 | if metaclass.Scan(self.scan_domain).enable: 241 | print_info("run script: "+metaclass.Scan(self.scan_domain).name) 242 | result = metaclass.Scan(self.scan_domain).run() 243 | self.add_domain(result) 244 | print_info(f"add: {len(result)} all count: {len(self.domains_set)}") 245 | else: # 指定了引擎 246 | for name in self.engine: # 这里不判断是否开启引擎 直接使用 247 | metaclass=importlib.import_module(name) 248 | print_info("run script: "+metaclass.Scan(self.scan_domain).name) 249 | result = metaclass.Scan(self.scan_domain).run() 250 | self.domains_set = self.domains_set | result 251 | print_info(f"add {len(result)} all count: {len(self.domains_set)}") 252 | 253 | def threadpool_dns(self): 254 | pool = ThreadPoolExecutor(self.thread_count) # 定义线程池 255 | all_task = list() 256 | for domain in self.domains_set: 257 | all_task.append(pool.submit(self.analysis_dns, domain)) 258 | for task in all_task: 259 | task.result() 260 | 261 | def analysis_dns(self, domain): 262 | try: 263 | ans = self.resolver.resolve(domain, "A") 264 | if ans: 265 | ips = list() 266 | for i in ans.response.answer: 267 | for j in i.items: 268 | if hasattr(j, "address"): 269 | self.domain_ips_dict[domain].append(j.address) 270 | except dns.resolver.NoAnswer: 271 | pass 272 | except dns.exception.Timeout: 273 | pass 274 | except Exception as e: 275 | pass 276 | 277 | def remove_black_ip(self): 278 | # 对于接口返回域名的泛解析结果的去除 279 | for domain, ips in self.domain_ips_dict.items(): 280 | for ip in ips: 281 | if ip in self.ip_domain_count_dict.keys(): 282 | self.ip_domain_count_dict[ip]["count"] +=1 283 | self.ip_domain_count_dict[ip]["domains"].append(domain) 284 | else: 285 | self.ip_domain_count_dict[ip] = {"domains": [domain], "count": 1} 286 | # remove 287 | for ip, domains_count in self.ip_domain_count_dict.items(): 288 | if domains_count["count"] > DNS_THRESHOLD: # 有50个域名指向了同一个ip jd的有大量指向一个ip 289 | # 将泛解析的ip存下来 返回回去 290 | self.black_ip.append(ip) 291 | for domain in domains_count["domains"]: 292 | if domain in self.domain_ips_dict.keys(): 293 | # print(domain, ip, domains_count["count"]) 294 | self.domain_ips_dict.pop(domain) 295 | 296 | def remove_private(self): 297 | # 移除内网ip 298 | print_info("del private ip domain") 299 | for domain in list(self.domain_ips_dict.keys()): 300 | ips = self.domain_ips_dict[domain] 301 | if ipaddress.ip_address(ips[0]).is_private: # if private ip del 302 | self.domain_ips_dict.pop(domain) 303 | 304 | def run(self): 305 | # 先用script下的接口获取子域名 306 | self.run_scripts() 307 | # 对这些接口进行dns解析 获取对应的ip列表 308 | print_info("start dns query") 309 | self.threadpool_dns() 310 | # 是否保留内网ip结果 311 | if not self.is_private: 312 | self.remove_private() 313 | # 对接口返回含有泛解析的域名去除 314 | self.remove_black_ip() 315 | if self.get_black_ip: # 是否返回泛解析的ip 316 | return self.domain_ips_dict, self.black_ip 317 | else: 318 | return self.domain_ips_dict 319 | 320 | 321 | # 穷举类 322 | class ExhaustionScan(object): 323 | """暴力穷举 324 | :param scan_domain 要测试的域名 325 | :param thread_count 线程数 默认100 326 | :param is_output 是否输出进度条 默认不输出 327 | :param black_ip 泛解析的ip 默认为空 328 | :param is_private 是否保留内网ip 默认不保留 329 | :param sub_dict 指定的字典 默认读取配置文件下字典 330 | :param next_sub 是否是三级或者四级的域名扫描 331 | :param timeout 超时时间 默认穷举超时时间为 2h 332 | :return domain_ips_dict 域名对应解析的ip结果 333 | 334 | """ 335 | def __init__ ( 336 | self, scan_domain, thread_count=100, 337 | is_output=False, black_ip=list(), 338 | is_private=False, sub_dict=None, next_sub=False, 339 | big_dict=False, timeout=7200, gen_rule=False 340 | ): 341 | self.base_path = os.path.dirname(os.path.abspath(__file__)) 342 | # dns 343 | self.resolver = resolver 344 | #self.resolver.nameservers=['8.8.8.8', '114.114.114.114'] 345 | self.scan_domain = scan_domain 346 | # 默认线程100个 347 | self.thread_count = thread_count 348 | self.is_output = is_output 349 | self.timeout = timeout 350 | self.sub_dict = sub_dict 351 | self.big_dict = big_dict 352 | self.next_sub = next_sub 353 | self.domain_ips_dict = defaultdict(list) 354 | self.sub_dict_queue = queue.Queue() 355 | # 是否添加动态字典 356 | self.gen_rule = gen_rule 357 | self.load_subdomain_dict() 358 | self.all_size = self.sub_dict_queue.qsize() 359 | # 泛解析的ip 默认是空 可以将接口返回的 black_ip 传入这里 360 | self.black_ip = black_ip 361 | # {ip:{ domains: [域名], count: 计数} } 362 | self.ip_domain_count_dict = dict() 363 | # 内网ip 364 | self.is_private = is_private 365 | 366 | 367 | def load_subdomain_dict(self): 368 | if not self.next_sub: 369 | print_info("load sub dict") 370 | if self.sub_dict: # 使用指定的字典 371 | if os.path.exists( self.sub_dict): 372 | dict_path = self.sub_dict 373 | else: 374 | print_error("字典不存在") 375 | else: 376 | dict_path = os.path.join(self.base_path, "../","config", "subdomains.txt") 377 | # 如果使用大字典 378 | if self.big_dict: 379 | dict_path = os.path.join(self.base_path, "../","config", "big_subdomains.txt") 380 | # 是否是三级或者四级扫描使用 小字典 381 | if self.next_sub: 382 | dict_path = os.path.join(self.base_path, "../","config", "next_subdomains.txt") 383 | with open(dict_path, "r") as f: 384 | for sub in f: 385 | self.sub_dict_queue.put(f"{sub.strip()}.{self.scan_domain}") 386 | if self.gen_rule: 387 | genrule_subdomain_list = GenSubdomain(self.scan_domain).gen() 388 | for sub in genrule_subdomain_list: 389 | self.sub_dict_queue.put(f"{sub.strip()}.{self.scan_domain}") 390 | #if not self.next_sub: 391 | print_info(f"quque all size: {self.sub_dict_queue.qsize()}") 392 | 393 | def is_analysis(self): 394 | """ 395 | 泛解析判断 396 | 通过不存在的域名进行判断 397 | """ 398 | try: 399 | ans = self.resolver.resolve( 400 | ''.join(random.sample(string.ascii_lowercase,5))+"."+self.scan_domain , "A") 401 | if ans: 402 | ips = list() 403 | for i in ans.response.answer: 404 | for j in i.items: 405 | ip = j.to_text() 406 | if ip: 407 | return True 408 | except dns.resolver.NoAnswer: 409 | return False 410 | except dns.exception.Timeout: 411 | return False 412 | except dns.resolver.NXDOMAIN: 413 | return False 414 | except Exception as e: 415 | print(e) 416 | return False 417 | 418 | def analysis_dns(self, domain): 419 | try: 420 | # print(domain) 421 | ans = resolver.query(domain, "A") 422 | if ans: 423 | ips = list() 424 | for i in ans.response.answer: 425 | for j in i.items: 426 | if hasattr(j, "address"): 427 | # 增加对内网ip的判断 if not save private 428 | if not self.is_private: 429 | if not ipaddress.ip_address(j.address).is_private: 430 | # 增加对泛解析的操作 431 | if self.remove_black_ip(domain, j.address): 432 | self.domain_ips_dict[domain].append(j.address) 433 | else: # 如果对内网结果进行保存 434 | if self.remove_black_ip(domain, j.address): 435 | self.domain_ips_dict[domain].append(j.address) 436 | except dns.resolver.NoAnswer: 437 | pass 438 | except dns.exception.Timeout: 439 | pass 440 | except Exception as e: 441 | pass 442 | 443 | def remove_black_ip(self, domain, ip): 444 | """ 对解析后的域名ip判断是否是泛解析 超过阈值进行忽略和删除""" 445 | if ip in self.black_ip: 446 | return False 447 | if ip in self.ip_domain_count_dict.keys(): 448 | self.ip_domain_count_dict[ip]["count"] +=1 449 | self.ip_domain_count_dict[ip]["domains"].append(domain) 450 | else: 451 | self.ip_domain_count_dict[ip] = {"domains": [domain], "count": 1} 452 | # 判断是否达到泛解析ip阈值 达到则删除记录下来的并增加黑ip 453 | if self.ip_domain_count_dict[ip]["count"] > DNS_THRESHOLD: 454 | for domain in self.ip_domain_count_dict[ip]["domains"]: 455 | if domain in self.domain_ips_dict.keys(): 456 | self.domain_ips_dict.pop(domain) 457 | self.black_ip.append(ip) 458 | return False 459 | else:# 没有达到阈值 460 | return True 461 | 462 | def worker(self): 463 | while not self.sub_dict_queue.empty(): 464 | domain = self.sub_dict_queue.get() 465 | if domain is None: 466 | break 467 | self.analysis_dns(domain) 468 | # self.sub_dict_queue.task_done() 469 | 470 | def all_done(self, threads): 471 | done_count = 0 472 | for t in threads: 473 | if not t.is_alive(): 474 | done_count +=1 475 | if done_count == len(threads): 476 | return True 477 | return False 478 | 479 | def run(self): 480 | # 先进行泛解析判断 481 | if self.is_analysis(): 482 | if not self.next_sub: 483 | print_debug("存在泛解析") 484 | threads = [] 485 | for i in range(self.thread_count): 486 | t = threading.Thread(target=self.worker) 487 | t.setDaemon(True) 488 | t.start() 489 | threads.append(t) 490 | # 阻塞 等待队列消耗完 491 | if not self.next_sub: 492 | print_info("start thread ") 493 | start = time.perf_counter() 494 | #while not self.sub_dict_queue.empty() and self.all_done(threads): 495 | while not self.sub_dict_queue.empty(): 496 | time.sleep(1) 497 | if self.is_output: 498 | # 输出进度条 499 | print_progress( 500 | self.sub_dict_queue.qsize(), self.all_size, 501 | start, len(self.domain_ips_dict) 502 | ) 503 | if not self.next_sub: # 不然下一级扫描的进度条会有问题 504 | print() # 最后输出的问题加一个print来实现换行 505 | # self.sub_dict_queue.join() # 这里暂时不需要使用队列来阻塞 506 | return self.domain_ips_dict 507 | 508 | class GenSubdomain(object): 509 | def __init__(self, domain): 510 | # 我们这里拿三级子域名字典与domain结合生成字典 511 | self.scan_domain = domain 512 | self.base_dir = os.path.dirname(os.path.abspath(__file__)) 513 | 514 | def gen(self): 515 | subdomain_dict_path = os.path.join(self.base_dir, "../","config", "next_subdomains.txt") 516 | gen_subdomain_list = list() 517 | rule_list = [ 518 | "-", 519 | ".", 520 | "_", 521 | "@" 522 | ] 523 | with open(subdomain_dict_path, "r") as f: 524 | for sub in f: 525 | for rule in rule_list: 526 | gen_subdomain_list.append(f'{self.scan_domain}{sub}') 527 | gen_subdomain_list.append(f'{sub}{self.scan_domain}') 528 | gen_subdomain_list.append(f'{sub}{rule}{self.scan_domain}') 529 | gen_subdomain_list.append(f'{self.scan_domain}{rule}{sub}') 530 | print_info(f"gen subdomain count: {len(gen_subdomain_list)}") 531 | return gen_subdomain_list 532 | --------------------------------------------------------------------------------