1476 lines
61 KiB
Python
1476 lines
61 KiB
Python
# -*- coding:utf-8 -*-
|
||
#__author__:langzi
|
||
#__blog__:www.langzi.fun
|
||
import aiodns
|
||
import asyncio
|
||
import aiomultiprocess
|
||
import aiofiles
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
ESD
|
||
~~~
|
||
|
||
Implements enumeration sub domains
|
||
|
||
:author: Feei <feei@feei.cn>
|
||
:homepage: https://github.com/FeeiCN/ESD
|
||
:license: GPL, see LICENSE for more details.
|
||
:copyright: Copyright (c) 2018 Feei. All rights reserved
|
||
"""
|
||
import os
|
||
import re
|
||
import time
|
||
import ssl
|
||
import math
|
||
import string
|
||
import random
|
||
import traceback
|
||
import itertools
|
||
import datetime
|
||
import colorlog
|
||
import asyncio
|
||
import uvloop
|
||
import aiodns
|
||
import aiohttp
|
||
import logging
|
||
import requests
|
||
import backoff
|
||
import socket
|
||
import async_timeout
|
||
import dns.query
|
||
import dns.zone
|
||
import dns.resolver
|
||
import multiprocessing
|
||
import threading
|
||
import tldextract
|
||
import json
|
||
import configparser
|
||
import base64
|
||
from tqdm import *
|
||
from colorama import Fore
|
||
from shodan import Shodan
|
||
import censys.certificates
|
||
from shodan.cli.helpers import get_api_key
|
||
from optparse import OptionParser
|
||
import urllib.parse as urlparse
|
||
from collections import Counter
|
||
from aiohttp.resolver import AsyncResolver
|
||
from itertools import islice
|
||
from difflib import SequenceMatcher
|
||
|
||
__version__ = '0.0.24'
|
||
|
||
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||
|
||
handler = colorlog.StreamHandler()
|
||
formatter = colorlog.ColoredFormatter(
|
||
'%(log_color)s%(asctime)s [%(name)s] [%(levelname)s] %(message)s%(reset)s',
|
||
datefmt=None,
|
||
reset=True,
|
||
log_colors={
|
||
'DEBUG': 'cyan',
|
||
'INFO': 'green',
|
||
'WARNING': 'yellow',
|
||
'ERROR': 'red',
|
||
'CRITICAL': 'red,bg_white',
|
||
},
|
||
secondary_log_colors={},
|
||
style='%'
|
||
)
|
||
handler.setFormatter(formatter)
|
||
|
||
logger = colorlog.getLogger('ESD')
|
||
logger.addHandler(handler)
|
||
logger.setLevel(logging.INFO)
|
||
|
||
ssl.match_hostname = lambda cert, hostname: True
|
||
|
||
|
||
# 只采用了递归,速度非常慢,在优化完成前不建议开启
|
||
# TODO:优化DNS查询,递归太慢了
|
||
class DNSQuery(object):
|
||
def __init__(self, root_domain, subs, suffix):
|
||
# root domain
|
||
self.suffix = suffix
|
||
self.sub_domains = []
|
||
if root_domain:
|
||
self.sub_domains.append(root_domain)
|
||
|
||
for sub in subs:
|
||
sub = ''.join(sub.rsplit(suffix, 1)).rstrip('.')
|
||
self.sub_domains.append('{sub}.{domain}'.format(sub=sub, domain=suffix))
|
||
|
||
def dns_query(self):
|
||
"""
|
||
soa,txt,mx,aaaa
|
||
:param sub:
|
||
:return:
|
||
"""
|
||
final_list = []
|
||
for subdomain in self.sub_domains:
|
||
try:
|
||
soa = []
|
||
q_soa = dns.resolver.query(subdomain, 'SOA')
|
||
for a in q_soa:
|
||
soa.append(str(a.rname).strip('.'))
|
||
soa.append(str(a.mname).strip('.'))
|
||
except Exception as e:
|
||
logger.warning('Query failed. {e}'.format(e=str(e)))
|
||
try:
|
||
aaaa = []
|
||
q_aaaa = dns.resolver.query(subdomain, 'AAAA')
|
||
aaaa = [str(a.address).strip('.') for a in q_aaaa]
|
||
except Exception as e:
|
||
logger.warning('Query failed. {e}'.format(e=str(e)))
|
||
try:
|
||
txt = []
|
||
q_txt = dns.resolver.query(subdomain, 'TXT')
|
||
txt = [t.strings[0].decode('utf-8').strip('.') for t in q_txt]
|
||
except Exception as e:
|
||
logger.warning('Query failed. {e}'.format(e=str(e)))
|
||
try:
|
||
mx = []
|
||
q_mx = dns.resolver.query(subdomain, 'MX')
|
||
mx = [str(m.exchange).strip('.') for m in q_mx]
|
||
except Exception as e:
|
||
logger.warning('Query failed. {e}'.format(e=str(e)))
|
||
domain_set = soa + aaaa + txt + mx
|
||
domain_list = [i for i in domain_set]
|
||
for p in domain_set:
|
||
re_domain = re.findall(r'^(([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\.?)$', p)
|
||
if len(re_domain) > 0 and subdomain in re_domain[0][0] and tldextract.extract(p).suffix != '':
|
||
continue
|
||
else:
|
||
domain_list.remove(p)
|
||
final_list = domain_list + final_list
|
||
# 递归调用,在子域名的dns记录中查找新的子域名
|
||
recursive = []
|
||
# print("before: {0}".format(final_list))
|
||
# print("self.sub_domain: {0}".format(self.sub_domains))
|
||
final_list = list(set(final_list).difference(set(self.sub_domains)))
|
||
# print("after: {0}".format(final_list))
|
||
if final_list:
|
||
d = DNSQuery('', final_list, self.suffix)
|
||
recursive = d.dns_query()
|
||
return final_list + recursive
|
||
|
||
|
||
class DNSTransfer(object):
|
||
def __init__(self, domain):
|
||
self.domain = domain
|
||
|
||
def transfer_info(self):
|
||
ret_zones = list()
|
||
try:
|
||
nss = dns.resolver.query(self.domain, 'NS')
|
||
nameservers = [str(ns) for ns in nss]
|
||
ns_addr = dns.resolver.query(nameservers[0], 'A')
|
||
# dnspython 的 bug,需要设置 lifetime 参数
|
||
zones = dns.zone.from_xfr(dns.query.xfr(ns_addr, self.domain, relativize=False, timeout=2, lifetime=2), check_origin=False)
|
||
names = zones.nodes.keys()
|
||
for n in names:
|
||
subdomain = ''
|
||
for t in range(0, len(n) - 1):
|
||
if subdomain != '':
|
||
subdomain += '.'
|
||
subdomain += str(n[t].decode())
|
||
if subdomain != self.domain:
|
||
ret_zones.append(subdomain)
|
||
return ret_zones
|
||
except BaseException:
|
||
return []
|
||
|
||
|
||
class CAInfo(object):
|
||
def __init__(self, domain):
|
||
self.domain = domain
|
||
|
||
def dns_resolve(self):
|
||
padding_domain = 'www.' + self.domain
|
||
# loop = asyncio.get_event_loop()
|
||
loop = asyncio.new_event_loop()
|
||
asyncio.set_event_loop(loop)
|
||
resolver = aiodns.DNSResolver(loop=loop)
|
||
f = resolver.query(padding_domain, 'A')
|
||
result = loop.run_until_complete(f)
|
||
return result[0].host
|
||
|
||
def get_cert_info_by_ip(self, ip):
|
||
s = socket.socket()
|
||
s.settimeout(2)
|
||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||
cert_path = base_dir + '/cacert.pem'
|
||
connect = ssl.wrap_socket(s, cert_reqs=ssl.CERT_REQUIRED, ca_certs=cert_path)
|
||
connect.settimeout(2)
|
||
connect.connect((ip, 443))
|
||
cert_data = connect.getpeercert().get('subjectAltName')
|
||
return cert_data
|
||
|
||
def get_ca_domain_info(self):
|
||
domain_list = list()
|
||
try:
|
||
ip = self.dns_resolve()
|
||
cert_data = self.get_cert_info_by_ip(ip)
|
||
except Exception as e:
|
||
return domain_list
|
||
|
||
for domain_info in cert_data:
|
||
hostname = domain_info[1]
|
||
if not hostname.startswith('*') and hostname.endswith(self.domain):
|
||
domain_list.append(hostname)
|
||
|
||
return domain_list
|
||
|
||
def get_subdomains(self):
|
||
subs = list()
|
||
subdomain_list = self.get_ca_domain_info()
|
||
for sub in subdomain_list:
|
||
subs.append(sub[:len(sub) - len(self.domain) - 1])
|
||
return subs
|
||
|
||
|
||
# 使用shodan接口进行枚举,但经测试并不能增加多少成果
|
||
class ShodanEngine(object):
|
||
def __init__(self, skey, conf, domain):
|
||
self.domain = domain
|
||
self.conf = conf
|
||
self.skey = skey
|
||
self.api = None
|
||
|
||
# 初始化shodan的api
|
||
def initialize(self, base_dir):
|
||
if self.skey:
|
||
logger.info('Initializing the shodan api.')
|
||
result = os.system('shodan init {skey}'.format(skey=self.skey))
|
||
if result:
|
||
logger.warning('Initializ failed, please check your key.')
|
||
return False
|
||
self.conf.set("shodan", "shodan_key", self.skey)
|
||
self.conf.write(open(base_dir + "/key.ini", "w"))
|
||
self.api = Shodan(get_api_key())
|
||
else:
|
||
from click.exceptions import ClickException
|
||
try:
|
||
key = None if get_api_key() == '' else get_api_key()
|
||
if key:
|
||
self.api = Shodan(key)
|
||
else:
|
||
return False
|
||
except ClickException as e:
|
||
logger.warning('The shodan api is empty so you can not use shodan api.')
|
||
return False
|
||
return True
|
||
|
||
def search(self):
|
||
subs = list()
|
||
result = self.api.search('hostname:{domain}'.format(domain=self.domain))
|
||
for service in result['matches']:
|
||
domain = service['hostnames'][0]
|
||
subs.append(domain.rsplit(self.domain, 1)[0].strip('.'))
|
||
return set(subs)
|
||
|
||
|
||
# fofa的sdk不支持python3,就只能调用restful api了,但是挖掘成果比shodan多
|
||
class FofaEngine(object):
|
||
def __init__(self, fofa_struct, conf, domain):
|
||
self.base_url = "https://fofa.so/api/v1/search/all?email={email}&key={key}&qbase64={domain}"
|
||
self.email = fofa_struct['femail']
|
||
self.fkey = fofa_struct['fkey']
|
||
self.domain = base64.b64encode(domain.encode('utf-8')).decode('utf-8')
|
||
self.headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
'Accept-Language': 'en-US,en;q=0.8',
|
||
'Accept-Encoding': 'gzip',
|
||
}
|
||
self.timeout = 30
|
||
self.conf = conf
|
||
|
||
def initialize(self, base_dir):
|
||
if self.fkey is not None and self.email is not None:
|
||
self.conf.set("fofa", "fofa_key", self.fkey)
|
||
self.conf.set("fofa", "fofa_email", self.email)
|
||
self.conf.write(open(base_dir + "/key.ini", "w"))
|
||
return True
|
||
else:
|
||
self.fkey = self.conf.items("fofa")[0][1]
|
||
self.email = self.conf.items("fofa")[1][1]
|
||
if self.fkey and self.email:
|
||
return True
|
||
return False
|
||
|
||
def search(self):
|
||
result = list()
|
||
url = self.base_url.format(email=self.email, key=self.fkey, domain=self.domain)
|
||
try:
|
||
resp = requests.Session().get(url, headers=self.headers, timeout=self.timeout)
|
||
json_resp = json.loads(resp.text)
|
||
for res in json_resp['results']:
|
||
domain = urlparse.urlparse(res[0]).netloc
|
||
result.append(domain.rsplit(self.domain, 1)[0].strip('.'))
|
||
except Exception as e:
|
||
result = []
|
||
|
||
return result
|
||
|
||
|
||
# Zoomeye的效果还可以,但是比fofa还贵
|
||
class ZoomeyeEngine(object):
|
||
def __init__(self, domain, zoomeye_struct, conf):
|
||
self.headers = {
|
||
"Authorization": "JWT {token}"
|
||
}
|
||
self.url = 'https://api.zoomeye.org/web/search?query=site:{domain}&page={num}'
|
||
self.domain = domain
|
||
self.zoomeye_struct = zoomeye_struct
|
||
self.conf = conf
|
||
|
||
def initialize(self, base_dir):
|
||
username = self.zoomeye_struct['username']
|
||
password = self.zoomeye_struct['password']
|
||
if username != '' and password != '':
|
||
resp = requests.Session().post(url='https://api.zoomeye.org/user/login', data=json.dumps(self.zoomeye_struct))
|
||
resp_json = json.loads(resp.text)
|
||
else:
|
||
username = self.conf.items("zoomeye")[0][1]
|
||
password = self.conf.items("zoomeye")[1][1]
|
||
if username != '' and password != '':
|
||
self.zoomeye_struct['username'] = username
|
||
self.zoomeye_struct['password'] = password
|
||
resp = requests.Session().post(url='https://api.zoomeye.org/user/login', data=json.dumps(self.zoomeye_struct))
|
||
resp_json = json.loads(resp.text)
|
||
else:
|
||
return False
|
||
if 'error' in resp_json.keys():
|
||
# logger.warning('In Zoomeye' + resp_json['message'])
|
||
return False
|
||
self.conf.set("zoomeye", "zoomeye_username", username)
|
||
self.conf.set("zoomeye", "zoomeye_password", password)
|
||
self.conf.write(open(base_dir + "/key.ini", "w"))
|
||
self.headers['Authorization'] = "JWT {token}".format(token=resp_json['access_token'])
|
||
|
||
return True
|
||
|
||
def search(self, num):
|
||
url = self.url.format(domain=self.domain, num=num)
|
||
resp = requests.Session().get(url=url, headers=self.headers)
|
||
|
||
try:
|
||
# zoomeye对于频繁的api调用会做限制,但是降低频率又会影响效率
|
||
response = json.loads(resp.text)
|
||
except Exception:
|
||
response = None
|
||
|
||
return response
|
||
|
||
def enumerate(self):
|
||
flag = True
|
||
num = 1
|
||
result = list()
|
||
while flag:
|
||
response = self.search(num)
|
||
if response is None or 'error' in response.keys():
|
||
# print(response)
|
||
flag = False
|
||
else:
|
||
match_list = response["matches"]
|
||
for block in match_list:
|
||
domain = block['site']
|
||
result.append(domain.rsplit(self.domain, 1)[0].strip('.'))
|
||
num = num + 1
|
||
return result
|
||
|
||
|
||
# censys的接口有点不稳定,经常出现timeout的情况
|
||
class CensysEngine(object):
|
||
def __init__(self, domain, censys_struct, conf):
|
||
self.domain = domain
|
||
self.conf = conf
|
||
self.censys_struct = censys_struct
|
||
self.certificates = None
|
||
self.fields = ['parsed.subject_dn']
|
||
|
||
def initialize(self, base_dir):
|
||
uid = self.censys_struct['uid']
|
||
secret = self.censys_struct['secret']
|
||
try:
|
||
if uid is not None and secret is not None:
|
||
self.certificates = censys.certificates.CensysCertificates(uid, secret)
|
||
else:
|
||
uid = self.conf.items("censys")[0][1]
|
||
secret = self.conf.items("censys")[1][1]
|
||
if uid != '' and secret != '':
|
||
self.certificates = censys.certificates.CensysCertificates(uid, secret)
|
||
else:
|
||
return False
|
||
|
||
self.conf.set("censys", "UID", uid)
|
||
self.conf.set("censys", "SECRET", secret)
|
||
self.conf.write(open(base_dir + "/key.ini", "w"))
|
||
except Exception as e:
|
||
return False
|
||
|
||
return True
|
||
|
||
def search(self):
|
||
result = list()
|
||
try:
|
||
for c in self.certificates.search(self.domain, fields=self.fields):
|
||
subject = c['parsed.subject_dn'].strip()
|
||
reg_domain = self.domain.replace('.', '[.]')
|
||
reg_text = r'(([-a-zA-Z0-9]+[.])*{reg_domain}$)'.format(reg_domain=reg_domain)
|
||
match_list = re.findall(reg_text, subject)
|
||
if match_list:
|
||
domain = match_list[0][0]
|
||
result.append(domain.rsplit(self.domain, 1)[0].strip('.'))
|
||
except Exception as e:
|
||
logger.warning(str(e))
|
||
return result
|
||
else:
|
||
return result
|
||
|
||
|
||
class EngineBase(multiprocessing.Process):
|
||
def __init__(self, base_url, domain, q, verbose, proxy):
|
||
multiprocessing.Process.__init__(self)
|
||
self.lock = threading.Lock()
|
||
self.q = q
|
||
self.subdomains = []
|
||
self.base_url = base_url
|
||
self.domain = domain
|
||
self.session = requests.Session()
|
||
self.headers = {
|
||
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||
'Accept-Language': 'en-US,en;q=0.8',
|
||
'Accept-Encoding': 'gzip',
|
||
}
|
||
self.timeout = 30
|
||
self.verbose = verbose
|
||
self.proxy = proxy
|
||
|
||
def get_page(self, num):
|
||
return num + 10
|
||
|
||
# 应当在子类里重写
|
||
def check_response_errors(self, resp):
|
||
return True
|
||
|
||
def should_sleep(self):
|
||
time.sleep(random.randint(2, 5))
|
||
return
|
||
|
||
def get_response(self, response):
|
||
if response is None:
|
||
return 0
|
||
return response.text if hasattr(response, "text") else response.content
|
||
|
||
def check_max_pages(self, num):
|
||
if self.MAX_PAGES == 0:
|
||
return False
|
||
return num >= self.MAX_PAGES
|
||
|
||
def send_req(self, page_no=1):
|
||
url = self.base_url.format(domain=self.domain, page_no=page_no)
|
||
try:
|
||
resp = self.session.get(
|
||
url, headers=self.headers, timeout=self.timeout)
|
||
except Exception:
|
||
resp = None
|
||
|
||
return self.get_response(resp)
|
||
|
||
def enumerate(self):
|
||
flag = True
|
||
page_no = 0
|
||
prev_links = []
|
||
retries = 0
|
||
|
||
while flag:
|
||
if self.check_max_pages(page_no):
|
||
return self.subdomains
|
||
resp = self.send_req(page_no)
|
||
|
||
if not self.check_response_errors(resp):
|
||
return self.subdomains
|
||
links = self.extract_domains(resp)
|
||
|
||
if links == prev_links:
|
||
retries += 1
|
||
page_no = self.get_page(page_no)
|
||
|
||
if retries >= 3:
|
||
return self.subdomains
|
||
|
||
prev_links = links
|
||
self.should_sleep()
|
||
|
||
return self.subdomains
|
||
|
||
def run(self):
|
||
domain_list = self.enumerate()
|
||
for domain in domain_list:
|
||
self.q.append(domain.rsplit(self.domain, 1)[0].strip('.'))
|
||
|
||
|
||
class Google(EngineBase):
|
||
def __init__(self, domain, q, verbose, proxy):
|
||
base_url = "https://www.google.com/search?q=site:{domain}+-www.{domain}&start={page_no}"
|
||
super(Google, self).__init__(base_url, domain, q, verbose, proxy)
|
||
self.MAX_DOMAINS = 11
|
||
self.MAX_PAGES = 200
|
||
self.engine_name = 'Google'
|
||
|
||
def extract_domains(self, resp):
|
||
links_list = list()
|
||
link_regx = re.compile(r'<cite.*?>(.*?)<\/cite>')
|
||
try:
|
||
links_list = link_regx.findall(resp)
|
||
for link in links_list:
|
||
link = re.sub('<span.*>', '', link)
|
||
if not link.startswith('http'):
|
||
link = "http://" + link
|
||
subdomain = urlparse.urlparse(link).netloc
|
||
if subdomain and subdomain not in self.subdomains and subdomain != self.domain:
|
||
logger.info('{engine_name}: {subdomain}'.format(engine_name=self.engine_name, subdomain=subdomain))
|
||
self.subdomains.append(subdomain.strip())
|
||
except Exception:
|
||
pass
|
||
return links_list
|
||
|
||
def check_response_errors(self, resp):
|
||
if isinstance(resp, int):
|
||
logger.warning("Please use proxy to access Google!")
|
||
logger.warning("Finished now the Google Enumeration ...")
|
||
return False
|
||
return True
|
||
|
||
def send_req(self, page_no=1):
|
||
url = self.base_url.format(domain=self.domain, page_no=page_no)
|
||
try:
|
||
resp = self.session.get(url, proxies=self.proxy, headers=self.headers, timeout=self.timeout)
|
||
except Exception as e:
|
||
resp = None
|
||
|
||
return self.get_response(resp)
|
||
|
||
|
||
class Bing(EngineBase):
|
||
def __init__(self, domain, q, verbose, proxy):
|
||
base_url = 'https://www.bing.com/search?q=domain%3A{domain}%20-www.{domain}&go=Submit&first={page_no}'
|
||
super(Bing, self).__init__(base_url, domain, q, verbose, proxy)
|
||
self.MAX_PAGES = 30
|
||
self.engine_name = 'Bing'
|
||
|
||
def extract_domains(self, resp):
|
||
links_list = list()
|
||
link_regx = re.compile('<li class="b_algo"><div class="b_title"><h2><a target="_blank" href="(.*?)"')
|
||
link_regx2 = re.compile('<li class="b_algo"><h2><a target="_blank" href="(.*?)"')
|
||
try:
|
||
links1 = link_regx.findall(resp)
|
||
links2 = link_regx2.findall(resp)
|
||
links_list = links1 + links2
|
||
for link in links_list:
|
||
link = re.sub(r'<(\/)?strong>|<span.*?>|<|>', '', link)
|
||
if not (link.startswith('http') or link.startswith('https')):
|
||
link = "http://" + link
|
||
subdomain = urlparse.urlparse(link).netloc
|
||
if subdomain not in self.subdomains and subdomain != self.domain:
|
||
logger.info('{engine_name}: {subdomain}'.format(engine_name=self.engine_name, subdomain=subdomain))
|
||
self.subdomains.append(subdomain.strip())
|
||
except Exception:
|
||
pass
|
||
|
||
return links_list
|
||
|
||
|
||
class Yahoo(EngineBase):
|
||
def __init__(self, domain, q, verbose, proxy):
|
||
base_url = "https://search.yahoo.com/search?p=site%3A{domain}%20-domain%3Awww.{domain}&b={page_no}"
|
||
super(Yahoo, self).__init__(base_url, domain, q, verbose, proxy)
|
||
self.engine_name = "Yahoo"
|
||
self.MAX_DOMAINS = 10
|
||
self.MAX_PAGES = 0
|
||
|
||
def extract_domains(self, resp):
|
||
link_regx2 = re.compile('<span class=" fz-.*? fw-m fc-12th wr-bw.*?">(.*?)</span>')
|
||
link_regx = re.compile('<span class="txt"><span class=" cite fw-xl fz-15px">(.*?)</span>')
|
||
links_list = []
|
||
try:
|
||
links = link_regx.findall(resp)
|
||
links2 = link_regx2.findall(resp)
|
||
links_list = links + links2
|
||
for link in links_list:
|
||
link = re.sub(r"<(\/)?b>", "", link)
|
||
if not link.startswith('http'):
|
||
link = "http://" + link
|
||
subdomain = urlparse.urlparse(link).netloc
|
||
if not subdomain.endswith(self.domain):
|
||
continue
|
||
if subdomain and subdomain not in self.subdomains and subdomain != self.domain:
|
||
logger.info('{engine_name}: {subdomain}'.format(engine_name=self.engine_name, subdomain=subdomain))
|
||
self.subdomains.append(subdomain.strip())
|
||
except Exception:
|
||
pass
|
||
|
||
return links_list
|
||
|
||
def check_response_errors(self, resp):
|
||
if isinstance(resp, int):
|
||
logger.warning("Please use proxy to access Yahoo!")
|
||
logger.warning("Finished now the Yahoo Enumeration ...")
|
||
return False
|
||
return True
|
||
|
||
def send_req(self, page_no=1):
|
||
url = self.base_url.format(domain=self.domain, page_no=page_no)
|
||
try:
|
||
resp = self.session.get(url, proxies=self.proxy, headers=self.headers, timeout=self.timeout)
|
||
except Exception as e:
|
||
resp = None
|
||
|
||
return self.get_response(resp)
|
||
|
||
|
||
class Baidu(EngineBase):
|
||
def __init__(self, domain, q, verbose, proxy):
|
||
base_url = "https://www.baidu.com/s?ie=UTF-8&wd=site%3A{domain}%20-site%3Awww.{domain}&pn={page_no}"
|
||
super(Baidu, self).__init__(base_url, domain, q, verbose, proxy)
|
||
self.MAX_PAGES = 30
|
||
self.engine_name = 'Baidu'
|
||
|
||
def extract_domains(self, resp):
|
||
links = list()
|
||
found_newdomain = False
|
||
subdomain_list = []
|
||
link_regx = re.compile('<a.*?class="c-showurl".*?>(.*?)</a>')
|
||
try:
|
||
links = link_regx.findall(resp)
|
||
for link in links:
|
||
link = re.sub('<.*?>|>|<| ', '', link)
|
||
if not (link.startswith('http') or link.startswith('https')):
|
||
link = "http://" + link
|
||
subdomain = urlparse.urlparse(link).netloc
|
||
if subdomain.endswith(self.domain):
|
||
subdomain_list.append(subdomain)
|
||
if subdomain not in self.subdomains and subdomain != self.domain:
|
||
found_newdomain = True
|
||
logger.info('{engine_name}: {subdomain}'.format(engine_name=self.engine_name, subdomain=subdomain))
|
||
self.subdomains.append(subdomain.strip())
|
||
except Exception:
|
||
pass
|
||
if not found_newdomain and subdomain_list:
|
||
self.querydomain = self.findsubs(subdomain_list)
|
||
return links
|
||
|
||
def findsubs(self, subdomains):
|
||
count = Counter(subdomains)
|
||
subdomain1 = max(count, key=count.get)
|
||
count.pop(subdomain1, "None")
|
||
subdomain2 = max(count, key=count.get) if count else ''
|
||
return (subdomain1, subdomain2)
|
||
|
||
|
||
class EnumSubDomain(object):
|
||
def __init__(self, domain, response_filter=None, dns_servers=None, skip_rsc=False, debug=False,
|
||
split=None, engines=[Baidu, Google, Bing, Yahoo], proxy={}, multiresolve=False,
|
||
shodan_key=None, fofa={'fkey': None, 'femail': None}, zoomeye={'username': None, 'password': None}, censys={'uid': None, 'secret': None}):
|
||
self.project_directory = os.path.abspath(os.path.dirname(__file__))
|
||
logger.info('Version: {v}'.format(v=__version__))
|
||
logger.info('----------')
|
||
logger.info('Start domain: {d}'.format(d=domain))
|
||
self.engines = engines
|
||
self.proxy = proxy
|
||
self.data = {}
|
||
self.domain = domain
|
||
self.skip_rsc = skip_rsc
|
||
self.split = split
|
||
self.multiresolve = multiresolve
|
||
self.skey = shodan_key
|
||
self.fofa_struct = fofa
|
||
self.conf = configparser.ConfigParser()
|
||
self.zoomeye_struct = zoomeye
|
||
self.censys_struct = censys
|
||
self.stable_dns_servers = ['1.1.1.1', '223.5.5.5']
|
||
if dns_servers is None:
|
||
dns_servers = [
|
||
'223.5.5.5', # AliDNS
|
||
'114.114.114.114', # 114DNS
|
||
'1.1.1.1', # Cloudflare
|
||
'119.29.29.29', # DNSPod
|
||
'1.2.4.8', # sDNS
|
||
# '11.1.1.1' # test DNS, not available
|
||
# '8.8.8.8', # Google DNS, 延时太高了
|
||
]
|
||
|
||
random.shuffle(dns_servers)
|
||
self.dns_servers = dns_servers
|
||
self.resolver = None
|
||
self.loop = asyncio.get_event_loop()
|
||
self.general_dicts = []
|
||
# Mark whether the current domain name is a pan-resolved domain name
|
||
self.is_wildcard_domain = False
|
||
# Use a nonexistent domain name to determine whether
|
||
# there is a pan-resolve based on the DNS resolution result
|
||
self.wildcard_sub = 'feei-esd-{random}'.format(random=random.randint(0, 9999))
|
||
self.wildcard_sub3 = 'feei-esd-{random}.{random}'.format(random=random.randint(0, 9999))
|
||
# There is no domain name DNS resolution IP
|
||
self.wildcard_ips = []
|
||
# No domain name response HTML
|
||
self.wildcard_html = None
|
||
self.wildcard_html_len = 0
|
||
self.wildcard_html3 = None
|
||
self.wildcard_html3_len = 0
|
||
# Subdomains that are consistent with IPs that do not have domain names
|
||
self.wildcard_subs = []
|
||
# Wildcard domains use RSC
|
||
self.wildcard_domains = {}
|
||
# Corotines count
|
||
self.coroutine_count = None
|
||
self.coroutine_count_dns = 100000
|
||
self.coroutine_count_request = 100
|
||
# dnsaio resolve timeout
|
||
self.resolve_timeout = 2
|
||
# RSC ratio
|
||
self.rsc_ratio = 0.8
|
||
self.remainder = 0
|
||
self.count = 0
|
||
# Request Header
|
||
self.request_headers = {
|
||
'Connection': 'keep-alive',
|
||
'Pragma': 'no-cache',
|
||
'Cache-Control': 'no-cache',
|
||
'Upgrade-Insecure-Requests': '1',
|
||
'User-Agent': 'Baiduspider',
|
||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
|
||
'DNT': '1',
|
||
'Referer': 'http://www.baidu.com/',
|
||
'Accept-Encoding': 'gzip, deflate',
|
||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'}
|
||
# Filter the domain's response(regex)
|
||
self.response_filter = response_filter
|
||
# debug mode
|
||
self.debug = debug
|
||
if self.debug:
|
||
logger.setLevel(logging.DEBUG)
|
||
# collect redirecting domains and response domains
|
||
self.domains_rs = []
|
||
self.domains_rs_processed = []
|
||
|
||
def generate_general_dicts(self, line):
|
||
"""
|
||
Generate general subdomains dicts
|
||
:param line:
|
||
:return:
|
||
"""
|
||
letter_count = line.count('{letter}')
|
||
number_count = line.count('{number}')
|
||
letters = itertools.product(string.ascii_lowercase, repeat=letter_count)
|
||
letters = [''.join(l) for l in letters]
|
||
numbers = itertools.product(string.digits, repeat=number_count)
|
||
numbers = [''.join(n) for n in numbers]
|
||
for l in letters:
|
||
iter_line = line.replace('{letter}' * letter_count, l)
|
||
self.general_dicts.append(iter_line)
|
||
number_dicts = []
|
||
for gd in self.general_dicts:
|
||
for n in numbers:
|
||
iter_line = gd.replace('{number}' * number_count, n)
|
||
number_dicts.append(iter_line)
|
||
if len(number_dicts) > 0:
|
||
return number_dicts
|
||
else:
|
||
return self.general_dicts
|
||
|
||
def load_sub_domain_dict(self):
|
||
"""
|
||
Load subdomains from files and dicts
|
||
:return:
|
||
"""
|
||
dicts = []
|
||
if self.debug:
|
||
path = '{pd}/subs-test.esd'.format(pd=self.project_directory)
|
||
else:
|
||
path = '{pd}/subs.esd'.format(pd=self.project_directory)
|
||
with open(path, encoding='utf-8') as f:
|
||
for line in f:
|
||
line = line.strip().lower()
|
||
# skip comments and space
|
||
if '#' in line or line == '':
|
||
continue
|
||
if '{letter}' in line or '{number}' in line:
|
||
self.general_dicts = []
|
||
dicts_general = self.generate_general_dicts(line)
|
||
dicts += dicts_general
|
||
else:
|
||
# compatibility other dicts
|
||
line = line.strip('.')
|
||
dicts.append(line)
|
||
dicts = list(set(dicts))
|
||
|
||
# split dict
|
||
if self.split is not None:
|
||
s = self.split.split('/')
|
||
dicts_choose = int(s[0])
|
||
dicts_count = int(s[1])
|
||
dicts_every = int(math.ceil(len(dicts) / dicts_count))
|
||
dicts = [dicts[i:i + dicts_every] for i in range(0, len(dicts), dicts_every)][dicts_choose - 1]
|
||
logger.info('Sub domain dict split {count} and get {choose}st'.format(count=dicts_count, choose=dicts_choose))
|
||
|
||
# root domain
|
||
dicts.append('@')
|
||
|
||
return dicts
|
||
|
||
async def query(self, sub):
|
||
"""
|
||
Query domain
|
||
:param sub:
|
||
:return:
|
||
"""
|
||
ret = None
|
||
# root domain
|
||
if sub == '@' or sub == '':
|
||
sub_domain = self.domain
|
||
else:
|
||
sub = ''.join(sub.rsplit(self.domain, 1)).rstrip('.')
|
||
sub_domain = '{sub}.{domain}'.format(sub=sub, domain=self.domain)
|
||
try:
|
||
ret = await self.resolver.query(sub_domain, 'A')
|
||
except aiodns.error.DNSError as e:
|
||
err_code, err_msg = e.args[0], e.args[1]
|
||
# 1: DNS server returned answer with no data
|
||
# 4: Domain name not found
|
||
# 11: Could not contact DNS servers
|
||
# 12: Timeout while contacting DNS servers
|
||
if err_code not in [1, 4, 11, 12]:
|
||
logger.warning('{domain} {exception}'.format(domain=sub_domain, exception=e))
|
||
except Exception as e:
|
||
logger.info(sub_domain)
|
||
logger.warning(traceback.format_exc())
|
||
|
||
else:
|
||
ret = [r.host for r in ret]
|
||
domain_ips = [s for s in ret]
|
||
# It is a wildcard domain name and
|
||
# the subdomain IP that is burst is consistent with the IP
|
||
# that does not exist in the domain name resolution,
|
||
# the response similarity is discarded for further processing.
|
||
if self.is_wildcard_domain and (sorted(self.wildcard_ips) == sorted(domain_ips) or set(domain_ips).issubset(self.wildcard_ips)):
|
||
if self.skip_rsc:
|
||
logger.debug('{sub} maybe wildcard subdomain, but it is --skip-rsc mode now, it will be drop this subdomain in results'.format(sub=sub_domain))
|
||
else:
|
||
logger.debug('{r} maybe wildcard domain, continue RSC {sub}'.format(r=self.remainder, sub=sub_domain, ips=domain_ips))
|
||
else:
|
||
if sub != self.wildcard_sub:
|
||
self.data[sub_domain] = sorted(domain_ips)
|
||
print('', end='\n')
|
||
self.count += 1
|
||
logger.info('{r} {sub} {ips}'.format(r=self.remainder, sub=sub_domain, ips=domain_ips))
|
||
self.remainder += -1
|
||
return sub_domain, ret
|
||
|
||
@staticmethod
|
||
def limited_concurrency_coroutines(coros, limit):
|
||
futures = [
|
||
asyncio.ensure_future(c)
|
||
for c in islice(coros, 0, limit)
|
||
]
|
||
|
||
async def first_to_finish():
|
||
while True:
|
||
await asyncio.sleep(0)
|
||
for f in futures:
|
||
if f.done():
|
||
futures.remove(f)
|
||
try:
|
||
nf = next(coros)
|
||
futures.append(asyncio.ensure_future(nf))
|
||
except StopIteration:
|
||
pass
|
||
return f.result()
|
||
|
||
while len(futures) > 0:
|
||
yield first_to_finish()
|
||
|
||
async def start(self, tasks, tasks_num):
|
||
"""
|
||
Limit the number of coroutines for reduce memory footprint
|
||
:param tasks:
|
||
:return:
|
||
"""
|
||
for res in tqdm(self.limited_concurrency_coroutines(tasks, self.coroutine_count), bar_format="%s{l_bar}%s{bar}%s{r_bar}%s" % (Fore.YELLOW, Fore.YELLOW, Fore.YELLOW, Fore.RESET), total=tasks_num):
|
||
await res
|
||
|
||
@staticmethod
|
||
def data_clean(data):
|
||
try:
|
||
html = re.sub(r'\s', '', data)
|
||
html = re.sub(r'<script(?!.*?src=).*?>.*?</script>', '', html)
|
||
return html
|
||
except BaseException:
|
||
return data
|
||
|
||
@staticmethod
|
||
@backoff.on_exception(backoff.expo, TimeoutError, max_tries=3)
|
||
async def fetch(session, url):
|
||
"""
|
||
Fetch url response with session
|
||
:param session:
|
||
:param url:
|
||
:return:
|
||
"""
|
||
try:
|
||
async with async_timeout.timeout(20):
|
||
async with session.get(url) as response:
|
||
return await response.text(), response.history
|
||
except Exception as e:
|
||
# TODO 当在随机DNS场景中只做响应相似度比对的话,如果域名没有Web服务会导致相似度比对失败从而丢弃
|
||
logger.warning('fetch exception: {e} {u}'.format(e=type(e).__name__, u=url))
|
||
return None, None
|
||
|
||
async def similarity(self, sub):
|
||
"""
|
||
Enumerate subdomains by responding to similarities
|
||
:param sub:
|
||
:return:
|
||
"""
|
||
# root domain
|
||
if sub == '@' or sub == '':
|
||
sub_domain = self.domain
|
||
else:
|
||
sub = ''.join(sub.rsplit(self.domain, 1)).rstrip('.')
|
||
sub_domain = '{sub}.{domain}'.format(sub=sub, domain=self.domain)
|
||
|
||
if sub_domain in self.domains_rs:
|
||
self.domains_rs.remove(sub_domain)
|
||
full_domain = 'http://{sub_domain}'.format(sub_domain=sub_domain)
|
||
# 如果跳转中的域名是以下情况则不加入下一轮RSC
|
||
skip_domain_with_history = [
|
||
# 跳到主域名了
|
||
'{domain}'.format(domain=self.domain),
|
||
'www.{domain}'.format(domain=self.domain),
|
||
# 跳到自己本身了,比如HTTP跳HTTPS
|
||
'{domain}'.format(domain=sub_domain),
|
||
]
|
||
try:
|
||
regex_domain = r"((?!\/)(?:(?:[a-z\d-]*\.)+{d}))".format(d=self.domain)
|
||
resolver = AsyncResolver(nameservers=self.dns_servers)
|
||
conn = aiohttp.TCPConnector(resolver=resolver)
|
||
async with aiohttp.ClientSession(connector=conn, headers=self.request_headers) as session:
|
||
html, history = await self.fetch(session, full_domain)
|
||
html = self.data_clean(html)
|
||
if history is not None and len(history) > 0:
|
||
location = str(history[-1].headers['location'])
|
||
if '.' in location:
|
||
location_split = location.split('/')
|
||
if len(location_split) > 2:
|
||
location = location_split[2]
|
||
else:
|
||
location = location
|
||
try:
|
||
location = re.match(regex_domain, location).group(0)
|
||
except AttributeError:
|
||
location = location
|
||
status = history[-1].status
|
||
if location in skip_domain_with_history and len(history) >= 2:
|
||
logger.debug('domain in skip: {s} {r} {l}'.format(s=sub_domain, r=status, l=location))
|
||
return
|
||
else:
|
||
# cnsuning.com suning.com
|
||
if location[-len(self.domain) - 1:] == '.{d}'.format(d=self.domain):
|
||
# collect redirecting's domains
|
||
if sub_domain != location and location not in self.domains_rs and location not in self.domains_rs_processed:
|
||
print('', end='\n')
|
||
logger.info('[{sd}] add redirect domain: {l}({len})'.format(sd=sub_domain, l=location, len=len(self.domains_rs)))
|
||
self.domains_rs.append(location)
|
||
self.domains_rs_processed.append(location)
|
||
else:
|
||
print('', end='\n')
|
||
logger.info('not same domain: {l}'.format(l=location))
|
||
else:
|
||
print('', end='\n')
|
||
logger.info('not domain(maybe path): {l}'.format(l=location))
|
||
if html is None:
|
||
print('', end='\n')
|
||
logger.warning('domain\'s html is none: {s}'.format(s=sub_domain))
|
||
return
|
||
# collect response html's domains
|
||
response_domains = re.findall(regex_domain, html)
|
||
response_domains = list(set(response_domains) - set([sub_domain]))
|
||
for rd in response_domains:
|
||
rd = rd.strip().strip('.')
|
||
if rd.count('.') >= sub_domain.count('.') and rd[-len(sub_domain):] == sub_domain:
|
||
continue
|
||
if rd not in self.domains_rs:
|
||
if rd not in self.domains_rs_processed:
|
||
print('', end='\n')
|
||
logger.info('[{sd}] add response domain: {s}({l})'.format(sd=sub_domain, s=rd, l=len(self.domains_rs)))
|
||
self.domains_rs.append(rd)
|
||
self.domains_rs_processed.append(rd)
|
||
|
||
if len(html) == self.wildcard_html_len:
|
||
ratio = 1
|
||
else:
|
||
# SPEED 4 2 1, but here is still the bottleneck
|
||
# real_quick_ratio() > quick_ratio() > ratio()
|
||
# TODO bottleneck
|
||
if sub.count('.') == 0: # secondary sub, ex: www
|
||
ratio = SequenceMatcher(None, html, self.wildcard_html).real_quick_ratio()
|
||
ratio = round(ratio, 3)
|
||
else: # tertiary sub, ex: home.dev
|
||
ratio = SequenceMatcher(None, html, self.wildcard_html3).real_quick_ratio()
|
||
ratio = round(ratio, 3)
|
||
self.remainder += -1
|
||
if ratio > self.rsc_ratio:
|
||
# passed
|
||
logger.debug('{r} RSC ratio: {ratio} (passed) {sub}'.format(r=self.remainder, sub=sub_domain, ratio=ratio))
|
||
else:
|
||
# added
|
||
# for def distinct func
|
||
# self.wildcard_domains[sub_domain] = html
|
||
if self.response_filter is not None:
|
||
for resp_filter in self.response_filter.split(','):
|
||
if resp_filter in html:
|
||
logger.debug('{r} RSC filter in response (passed) {sub}'.format(r=self.remainder, sub=sub_domain))
|
||
return
|
||
else:
|
||
continue
|
||
self.data[sub_domain] = self.wildcard_ips
|
||
else:
|
||
self.data[sub_domain] = self.wildcard_ips
|
||
print('', end='\n')
|
||
logger.info('{r} RSC ratio: {ratio} (added) {sub}'.format(r=self.remainder, sub=sub_domain, ratio=ratio))
|
||
except Exception as e:
|
||
logger.debug(traceback.format_exc())
|
||
return
|
||
|
||
def distinct(self):
|
||
for domain, html in self.wildcard_domains.items():
|
||
for domain2, html2 in self.wildcard_domains.items():
|
||
ratio = SequenceMatcher(None, html, html2).real_quick_ratio()
|
||
if ratio > self.rsc_ratio:
|
||
# remove this domain
|
||
if domain2 in self.data:
|
||
del self.data[domain2]
|
||
m = 'Remove'
|
||
else:
|
||
m = 'Stay'
|
||
logger.info('{d} : {d2} {ratio} {m}'.format(d=domain, d2=domain2, ratio=ratio, m=m))
|
||
|
||
def dnspod(self):
|
||
"""
|
||
http://feei.cn/esd
|
||
:return:
|
||
"""
|
||
# noinspection PyBroadException
|
||
try:
|
||
content = requests.get('http://www.dnspod.cn/proxy_diagnose/recordscan/{domain}?callback=feei'.format(domain=self.domain), timeout=5).text
|
||
domains = re.findall(r'[^": ]*{domain}'.format(domain=self.domain), content)
|
||
domains = list(set(domains))
|
||
tasks = (self.query(''.join(domain.rsplit(self.domain, 1)).rstrip('.')) for domain in domains)
|
||
self.loop.run_until_complete(self.start(tasks, len(domains)))
|
||
except Exception as e:
|
||
domains = []
|
||
return domains
|
||
|
||
def check(self, dns):
|
||
logger.info("Checking if DNS server {dns} is available".format(dns=dns))
|
||
msg = b'\x5c\x6d\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x05baidu\x03com\x00\x00\x01\x00\x01'
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
sock.settimeout(3)
|
||
repeat = {
|
||
1: 'first',
|
||
2: 'second',
|
||
3: 'third'
|
||
}
|
||
for i in range(3):
|
||
logger.info("Sending message to DNS server a {times} time".format(times=repeat[i + 1]))
|
||
sock.sendto(msg, (dns, 53))
|
||
try:
|
||
sock.recv(4096)
|
||
break
|
||
except socket.timeout as e:
|
||
logger.warning('Failed!')
|
||
if i == 2:
|
||
return False
|
||
return True
|
||
|
||
def run(self):
|
||
"""
|
||
Run
|
||
:return:
|
||
"""
|
||
start_time = time.time()
|
||
subs = self.load_sub_domain_dict()
|
||
self.remainder = len(subs)
|
||
logger.info('Sub domain dict count: {c}'.format(c=len(subs)))
|
||
logger.info('Generate coroutines...')
|
||
# Verify that all DNS server results are consistent
|
||
stable_dns = []
|
||
wildcard_ips = None
|
||
last_dns = []
|
||
only_similarity = False
|
||
for dns in self.dns_servers:
|
||
delay = self.check(dns)
|
||
if not delay:
|
||
logger.warning("@{dns} is not available, skip this DNS server".format(dns=dns))
|
||
continue
|
||
self.resolver = aiodns.DNSResolver(loop=self.loop, nameservers=[dns], timeout=self.resolve_timeout)
|
||
job = self.query(self.wildcard_sub)
|
||
sub, ret = self.loop.run_until_complete(job)
|
||
logger.info('@{dns} {sub} {ips}'.format(dns=dns, sub=sub, ips=ret))
|
||
if ret is None:
|
||
ret = None
|
||
else:
|
||
ret = sorted(ret)
|
||
|
||
if dns in self.stable_dns_servers:
|
||
wildcard_ips = ret
|
||
stable_dns.append(ret)
|
||
|
||
if ret:
|
||
equal = [False for r in ret if r not in last_dns]
|
||
if len(last_dns) != 0 and False in equal:
|
||
only_similarity = self.is_wildcard_domain = True
|
||
logger.info('Is a random resolve subdomain.')
|
||
break
|
||
else:
|
||
last_dns = ret
|
||
|
||
is_all_stable_dns = stable_dns.count(stable_dns[0]) == len(stable_dns)
|
||
if not is_all_stable_dns:
|
||
logger.info('Is all stable dns: NO, use the default dns server')
|
||
self.resolver = aiodns.DNSResolver(loop=self.loop, nameservers=self.stable_dns_servers, timeout=self.resolve_timeout)
|
||
# Wildcard domain
|
||
is_wildcard_domain = not (stable_dns.count(None) == len(stable_dns))
|
||
if is_wildcard_domain or self.is_wildcard_domain:
|
||
if not self.skip_rsc:
|
||
logger.info('This is a wildcard domain, will enumeration subdomains use by DNS+RSC.')
|
||
else:
|
||
logger.info('This is a wildcard domain, but it is --skip-rsc mode now, it will be drop all random resolve subdomains in results')
|
||
self.is_wildcard_domain = True
|
||
if wildcard_ips is not None:
|
||
self.wildcard_ips = wildcard_ips
|
||
else:
|
||
self.wildcard_ips = stable_dns[0]
|
||
logger.info('Wildcard IPS: {ips}'.format(ips=self.wildcard_ips))
|
||
if not self.skip_rsc:
|
||
try:
|
||
self.wildcard_html = requests.get('http://{w_sub}.{domain}'.format(w_sub=self.wildcard_sub, domain=self.domain), headers=self.request_headers, timeout=10, verify=False).text
|
||
self.wildcard_html = self.data_clean(self.wildcard_html)
|
||
self.wildcard_html_len = len(self.wildcard_html)
|
||
self.wildcard_html3 = requests.get('http://{w_sub}.{domain}'.format(w_sub=self.wildcard_sub3, domain=self.domain), headers=self.request_headers, timeout=10, verify=False).text
|
||
self.wildcard_html3 = self.data_clean(self.wildcard_html3)
|
||
self.wildcard_html3_len = len(self.wildcard_html3)
|
||
logger.info('Wildcard domain response html length: {len} 3length: {len2}'.format(len=self.wildcard_html_len, len2=self.wildcard_html3_len))
|
||
except requests.exceptions.SSLError:
|
||
logger.warning('SSL Certificate Error!')
|
||
except requests.exceptions.ConnectTimeout:
|
||
logger.warning('Request response content failed, check network please!')
|
||
except requests.exceptions.ReadTimeout:
|
||
self.wildcard_html = self.wildcard_html3 = ''
|
||
self.wildcard_html_len = self.wildcard_html3_len = 0
|
||
logger.warning('Request response content timeout, {w_sub}.{domain} and {w_sub3}.{domain} maybe not a http service, content will be set to blank!'.format(w_sub=self.wildcard_sub,
|
||
domain=self.domain,
|
||
w_sub3=self.wildcard_sub3))
|
||
except requests.exceptions.ConnectionError:
|
||
logger.error('ESD can\'t get the response text so the rsc will be skipped. ')
|
||
self.skip_rsc = True
|
||
else:
|
||
logger.info('Not a wildcard domain')
|
||
|
||
if not only_similarity:
|
||
self.coroutine_count = self.coroutine_count_dns
|
||
tasks = (self.query(sub) for sub in subs)
|
||
self.loop.run_until_complete(self.start(tasks, len(subs)))
|
||
logger.info("Brute Force subdomain count: {total}".format(total=self.count))
|
||
dns_time = time.time()
|
||
time_consume_dns = int(dns_time - start_time)
|
||
|
||
# DNSPod JSONP API
|
||
logger.info('Collect DNSPod JSONP API\'s subdomains...')
|
||
dnspod_domains = self.dnspod()
|
||
logger.info('DNSPod JSONP API Count: {c}'.format(c=len(dnspod_domains)))
|
||
|
||
# CA subdomain info
|
||
ca_subdomains = []
|
||
logger.info('Collect subdomains in CA...')
|
||
ca_subdomains = CAInfo(self.domain).get_subdomains()
|
||
if len(ca_subdomains):
|
||
tasks = (self.query(sub) for sub in ca_subdomains)
|
||
self.loop.run_until_complete(self.start(tasks, len(ca_subdomains)))
|
||
logger.info('CA subdomain count: {c}'.format(c=len(ca_subdomains)))
|
||
|
||
# DNS Transfer Vulnerability
|
||
transfer_info = []
|
||
logger.info('Check DNS Transfer Vulnerability in {domain}'.format(domain=self.domain))
|
||
transfer_info = DNSTransfer(self.domain).transfer_info()
|
||
if len(transfer_info):
|
||
logger.warning('DNS Transfer Vulnerability found in {domain}!'.format(domain=self.domain))
|
||
tasks = (self.query(sub) for sub in transfer_info)
|
||
self.loop.run_until_complete(self.start(tasks, len(transfer_info)))
|
||
logger.info('DNS Transfer subdomain count: {c}'.format(c=len(transfer_info)))
|
||
|
||
# Use search engines to enumerate subdomains (support Baidu,Bing,Google,Yahoo)
|
||
subdomains = []
|
||
if self.engines:
|
||
logger.info('Enumerating subdomains with search engine')
|
||
subdomains_queue = multiprocessing.Manager().list()
|
||
enums = [enum(self.domain, q=subdomains_queue, verbose=False, proxy=self.proxy) for enum in self.engines]
|
||
for enum in enums:
|
||
enum.start()
|
||
for enum in enums:
|
||
enum.join()
|
||
subdomains = set(subdomains_queue)
|
||
if len(subdomains):
|
||
tasks = (self.query(sub) for sub in subdomains)
|
||
self.loop.run_until_complete(self.start(tasks, len(subdomains)))
|
||
logger.info('Search engines subdomain count: {subdomains_count}'.format(subdomains_count=len(subdomains)))
|
||
|
||
# Use shodan to enumerate subdomains (need key and money)
|
||
shodan_result = []
|
||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||
self.conf.read(base_dir + "/key.ini")
|
||
shodan = ShodanEngine(self.skey, self.conf, self.domain)
|
||
is_success = shodan.initialize(base_dir)
|
||
if is_success:
|
||
logger.info('Enumerating subdomains with Shodan')
|
||
shodan_result = shodan.search()
|
||
if len(shodan_result):
|
||
tasks = (self.query(sub) for sub in shodan_result)
|
||
self.loop.run_until_complete(self.start(tasks, len(shodan_result)))
|
||
logger.info("Shodan subdomain count: {subdomains_count}".format(subdomains_count=len(shodan_result)))
|
||
|
||
# Use fofa to enumerate subdomains (need key and money)
|
||
fofa_result = []
|
||
fofa = FofaEngine(self.fofa_struct, self.conf, self.domain)
|
||
is_success = fofa.initialize(base_dir)
|
||
if is_success:
|
||
logger.info("Enumerating subdomains with Fofa")
|
||
fofa_result = fofa.search()
|
||
if len(fofa_result):
|
||
tasks = (self.query(sub) for sub in fofa_result)
|
||
self.loop.run_until_complete(self.start(tasks, len(fofa_result)))
|
||
logger.info("Fofa subdomain count: {subdomains_count}".format(subdomains_count=len(fofa_result)))
|
||
|
||
# Use zoomeye to enumerate subdomains (need account or money)
|
||
zoomeye_result = []
|
||
zoomeye = ZoomeyeEngine(self.domain, self.zoomeye_struct, self.conf)
|
||
is_success = zoomeye.initialize(base_dir)
|
||
if is_success:
|
||
logger.info("Enumerating subdomains with Zoomeye")
|
||
zoomeye_result = zoomeye.enumerate()
|
||
if len(zoomeye_result):
|
||
tasks = (self.query(sub) for sub in zoomeye_result)
|
||
self.loop.run_until_complete(self.start(tasks, len(zoomeye_result)))
|
||
logger.info("Zoomeye subdomain count: {subdomains_count}".format(subdomains_count=len(zoomeye_result)))
|
||
|
||
censys_result = []
|
||
censys = CensysEngine(self.domain, self.censys_struct, self.conf)
|
||
is_success = censys.initialize(base_dir)
|
||
if is_success:
|
||
logger.info("Enumerating subdomains with Censys")
|
||
censys_result = censys.search()
|
||
if len(censys_result):
|
||
tasks = (self.query(sub) for sub in censys_result)
|
||
self.loop.run_until_complete(self.start(tasks, len(censys_result)))
|
||
logger.info("Censys subdomain count: {subdomains_count}".format(subdomains_count=len(censys_result)))
|
||
|
||
total_subs = set(subs + dnspod_domains + list(subdomains) + transfer_info + ca_subdomains + list(shodan_result) + fofa_result + zoomeye_result + censys_result)
|
||
|
||
# Use TXT,SOA,MX,AAAA record to find sub domains
|
||
if self.multiresolve:
|
||
logger.info('Enumerating subdomains with TXT, SOA, MX, AAAA record...')
|
||
dnsquery = DNSQuery(self.domain, total_subs, self.domain)
|
||
record_info = dnsquery.dns_query()
|
||
tasks = (self.query(record[:record.find('.')]) for record in record_info)
|
||
self.loop.run_until_complete(self.start(tasks, len(record_info)))
|
||
logger.info('DNS record subdomain count: {c}'.format(c=len(record_info)))
|
||
|
||
if self.is_wildcard_domain and not self.skip_rsc:
|
||
# Response similarity comparison
|
||
total_subs = set(subs + dnspod_domains + list(subdomains) + transfer_info + ca_subdomains)
|
||
self.wildcard_subs = list(set(subs).union(total_subs))
|
||
logger.info('Enumerates {len} sub domains by DNS mode in {tcd}.'.format(len=len(self.data), tcd=str(datetime.timedelta(seconds=time_consume_dns))))
|
||
logger.info('Will continue to test the distinct({len_subs}-{len_exist})={len_remain} domains used by RSC, the speed will be affected.'.format(len_subs=len(subs), len_exist=len(self.data),
|
||
len_remain=len(self.wildcard_subs)))
|
||
self.coroutine_count = self.coroutine_count_request
|
||
self.remainder = len(self.wildcard_subs)
|
||
tasks = (self.similarity(sub) for sub in self.wildcard_subs)
|
||
self.loop.run_until_complete(self.start(tasks, len(self.wildcard_subs)))
|
||
|
||
# Distinct last domains use RSC
|
||
# Maybe misinformation
|
||
# self.distinct()
|
||
|
||
time_consume_request = int(time.time() - dns_time)
|
||
logger.info('Requests time consume {tcr}'.format(tcr=str(datetime.timedelta(seconds=time_consume_request))))
|
||
# RS(redirect/response) domains
|
||
while len(self.domains_rs) != 0:
|
||
logger.info('RS(redirect/response) domains({l})...'.format(l=len(self.domains_rs)))
|
||
tasks = (self.similarity(''.join(domain.rsplit(self.domain, 1)).rstrip('.')) for domain in self.domains_rs)
|
||
|
||
self.loop.run_until_complete(self.start(tasks, len(self.domains_rs)))
|
||
|
||
# write output
|
||
tmp_dir = '/tmp/esd'
|
||
if not os.path.isdir(tmp_dir):
|
||
os.mkdir(tmp_dir, 0o777)
|
||
output_path_with_time = '{td}/.{domain}_{time}.esd'.format(td=tmp_dir, domain=self.domain, time=datetime.datetime.now().strftime("%Y-%m_%d_%H-%M"))
|
||
output_path = '{td}/.{domain}.esd'.format(td=tmp_dir, domain=self.domain)
|
||
if len(self.data):
|
||
max_domain_len = max(map(len, self.data)) + 2
|
||
else:
|
||
max_domain_len = 2
|
||
output_format = '%-{0}s%-s\n'.format(max_domain_len)
|
||
with open(output_path_with_time, 'w') as opt, open(output_path, 'w') as op:
|
||
for domain, ips in self.data.items():
|
||
# The format is consistent with other scanners to ensure that they are
|
||
# invoked at the same time without increasing the cost of
|
||
# resolution
|
||
if ips is None or len(ips) == 0:
|
||
ips_split = ''
|
||
else:
|
||
ips_split = ','.join(ips)
|
||
con = output_format % (domain, ips_split)
|
||
op.write(con)
|
||
opt.write(con)
|
||
|
||
logger.info('Output: {op}'.format(op=output_path))
|
||
logger.info('Output with time: {op}'.format(op=output_path_with_time))
|
||
logger.info('Total domain: {td}'.format(td=len(self.data)))
|
||
time_consume = int(time.time() - start_time)
|
||
logger.info('Time consume: {tc}'.format(tc=str(datetime.timedelta(seconds=time_consume))))
|
||
return self.data
|
||
|
||
|
||
def banner():
|
||
print("""\033[94m
|
||
______ _____ _____
|
||
| ____| / ____| | __ \
|
||
| |__ | (___ | | | |
|
||
| __| \___ \ | | | |
|
||
| |____ ____) | | |__| |
|
||
|______| |_____/ |_____/\033[0m\033[93m
|
||
# Enumeration sub domains @version: %s\033[92m
|
||
""" % __version__)
|
||
|
||
|
||
def main():
|
||
banner()
|
||
parser = OptionParser('Usage: python ESD.py -d feei.cn -F response_filter -e baidu,google,bing,yahoo -p user:pass@host:port')
|
||
parser.add_option('-d', '--domain', dest='domains', help='The domains that you want to enumerate')
|
||
parser.add_option('-f', '--file', dest='input', help='Import domains from this file')
|
||
parser.add_option('-F', '--filter', dest='filter', help='Response filter')
|
||
parser.add_option('-s', '--skip-rsc', dest='skiprsc', help='Skip response similary compare', action='store_true', default=False)
|
||
parser.add_option('-e', '--engines', dest='engines', help='Choose an engine in baidu,google,bing or yahoo, split with ","')
|
||
parser.add_option('-S', '--split', dest='split', help='Split the dict into several parts', default='1/1')
|
||
parser.add_option('-p', '--proxy', dest='proxy', help='Use socks5 proxy to access Google and Yahoo')
|
||
parser.add_option('-m', '--multi-resolve', dest='multiresolve', help='Use TXT, AAAA, MX, SOA record to find subdomains', action='store_true', default=False)
|
||
parser.add_option('--skey', '--shodan-key', dest='shodankey', help='Define the api of shodan')
|
||
parser.add_option('--fkey', '--fofa-key', dest='fofakey', help='Define the key of fofa')
|
||
parser.add_option('--femail', '--fofa-email', dest='fofaemail', help='The email of your fofa account')
|
||
parser.add_option('--zusername', '--zoomeye-username', dest='zoomeyeusername', help='The username of your zoomeye account')
|
||
parser.add_option('--zpassword', '--zoomeye-password', dest='zoomeyepassword', help='The password of your zoomeye account')
|
||
parser.add_option('--cuid', '--censys-uid', dest='censysuid', help="The uid of your censys account")
|
||
parser.add_option('--csecret', '--censys-secret', dest='censyssecret', help='The secret of your censys account')
|
||
(options, args) = parser.parse_args()
|
||
|
||
support_engines = {
|
||
'baidu': Baidu,
|
||
'google': Google,
|
||
'bing': Bing,
|
||
'yahoo': Yahoo,
|
||
}
|
||
|
||
domains = []
|
||
engines = []
|
||
response_filter = options.filter
|
||
skip_rsc = options.skiprsc
|
||
split_list = options.split.split('/')
|
||
split = options.split
|
||
multiresolve = options.multiresolve
|
||
skey = options.shodankey
|
||
|
||
fofa_struct = {
|
||
'fkey': options.fofakey,
|
||
'femail': options.fofaemail,
|
||
}
|
||
|
||
zoomeye_struct = {
|
||
'username': options.zoomeyeusername,
|
||
'password': options.zoomeyepassword,
|
||
}
|
||
|
||
censys_struct = {
|
||
'uid': options.censysuid,
|
||
'secret': options.censyssecret,
|
||
}
|
||
|
||
try:
|
||
if len(split_list) != 2 or int(split_list[0]) > int(split_list[1]):
|
||
logger.error('Invaild split parameter,can not split the dict')
|
||
split = None
|
||
except:
|
||
logger.error('Split validation failed: {d}'.format(d=split_list))
|
||
exit(0)
|
||
|
||
if options.proxy:
|
||
proxy = {
|
||
'http': 'socks5h://%s' % options.proxy,
|
||
'https': 'socks5h://%s' % options.proxy
|
||
}
|
||
else:
|
||
proxy = {}
|
||
|
||
if options.engines:
|
||
for engine in options.engines.split(','):
|
||
if engine.lower() in support_engines:
|
||
engines.append(support_engines[engine])
|
||
else:
|
||
engines = [Baidu, Google, Bing, Yahoo]
|
||
|
||
if options.domains is not None:
|
||
for p in options.domains.split(','):
|
||
p = p.strip().lower()
|
||
re_domain = re.findall(r'^(([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,})$', p)
|
||
if len(re_domain) > 0 and re_domain[0][0] == p and tldextract.extract(p).suffix != '':
|
||
domains.append(p.strip())
|
||
else:
|
||
logger.error('Domain validation failed: {d}'.format(d=p))
|
||
elif options.input and os.path.isfile(options.input):
|
||
with open(options.input) as fh:
|
||
for line_domain in fh:
|
||
line_domain = line_domain.strip().lower()
|
||
re_domain = re.findall(r'^(([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,})$', line_domain)
|
||
if len(re_domain) > 0 and re_domain[0][0] == line_domain and tldextract.extract(line_domain).suffix != '':
|
||
domains.append(line_domain)
|
||
else:
|
||
logger.error('Domain validation failed: {d}'.format(d=line_domain))
|
||
else:
|
||
logger.error('Please input vaild parameter. ie: "esd -d feei.cn" or "esd -f /Users/root/domains.txt"')
|
||
|
||
if 'esd' in os.environ:
|
||
debug = os.environ['esd']
|
||
else:
|
||
debug = False
|
||
logger.info('Debug: {d}'.format(d=debug))
|
||
logger.info('--skip-rsc: {rsc}'.format(rsc=skip_rsc))
|
||
|
||
logger.info('Total target domains: {ttd}'.format(ttd=len(domains)))
|
||
try:
|
||
for d in domains:
|
||
esd = EnumSubDomain(d, response_filter, skip_rsc=skip_rsc, debug=debug, split=split, engines=engines, proxy=proxy,
|
||
multiresolve=multiresolve, shodan_key=skey, fofa=fofa_struct, zoomeye=zoomeye_struct, censys=censys_struct)
|
||
esd.run()
|
||
except KeyboardInterrupt:
|
||
print('', end='\n')
|
||
logger.info('Bye :)')
|
||
exit(0)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|