前言
在信息收集中,子域名收集基本上是一个绕不开的话题。
网上子域名收集工具很多,每个工具都有自己的有点和缺点,每次收集子域名的时候,经常是几个常见工具挨个用,所以希望能够用一款工具来应对绝大多数场景,最终选择了学习和改造一下oneforall这个工具。
分析
整体流程
oneforall除提供oneforall.py这个入口文件外, 提供了三个常用入口
brute.py 用于爆破子域名
export.py 用于导出数据
takeover.py #子域接管漏洞 这个不收集子域名
概括oneforall核心流程如下:
run======>检测泛域名=====>收集模块(Collect)=====>SRV爆破模块(BruteSRV)=====>爆破模块(Brute)=====>dns验证(run_resolve)=====>http验证(run_request)======>子域爬取模块(Finder)=====>子域置换模块(Altdns)=====>丰富输出(Enrich)=====>子域接管模块(Takeover)=====>return
代码如下:
def main(self):
"""
OneForAll main process
:return: subdomain results
:rtype: list
"""
utils.init_table(self.domain)
if not self.access_internet:
logger.log('ALERT', 'Because it cannot access the Internet, '
'OneForAll will not execute the subdomain collection module!')
if self.access_internet:
self.enable_wildcard = wildcard.detect_wildcard(self.domain)
collect = Collect(self.domain) #收集模块
collect.run()
srv = BruteSRV(self.domain) # 通过查询SRV记录查询子域名
srv.run()
if self.brute:
# Due to there will be a large number of dns resolution requests,
# may cause other network tasks to be error
brute = Brute(self.domain, word=True, export=False) # 爆破模块
brute.enable_wildcard = self.enable_wildcard
brute.in_china = self.in_china
brute.quite = True
brute.run()
utils.deal_data(self.domain) # 去除不正确域名和重复域名
# Export results without resolve
if not self.dns: # 如果不进行dns验证,则直接返回收集结果
self.data = self.export_data()
self.datas.extend(self.data)
return self.data
self.data = utils.get_data(self.domain) # get_data方法返回已获得的所有的子域名信息
# Resolve subdomains
utils.clear_data(self.domain)
self.data = resolve.run_resolve(self.domain, self.data) # dns验证 使用massdns进行查询
# Save resolve results
resolve.save_db(self.domain, self.data)
# Export results without HTTP request
if not self.req: # 如果不进行http验证,则返回结果
self.data = self.export_data()
self.datas.extend(self.data)
return self.data
if self.enable_wildcard:
# deal wildcard
self.data = wildcard.deal_wildcard(self.data) # 处理泛解析域名
# HTTP request
utils.clear_data(self.domain)
request.run_request(self.domain, self.data, self.port) # 进行http验证,会保存http返回内容
# Finder module
if settings.enable_finder_module: # 从页面中查找子域名 都是将请求保存数据库后 再提取子域名的
finder = Finder()
finder.run(self.domain, self.data, self.port)
# altdns module
if settings.enable_altdns_module: # 根据已爆破出来的子域名,配合特定规则动态生成新的子域名,再次扫描
altdns = Altdns(self.domain)
altdns.run(self.data, self.port)
# Information enrichment module
if settings.enable_enrich_module:
enrich = Enrich(self.domain) # 只是丰富输出结果,没有收集子域名的功能
enrich.run()
self.data = self.export_data()
self.datas.extend(self.data)
# Scan subdomain takeover
if self.takeover:
subdomains = utils.get_subdomains(self.data)
takeover = Takeover(targets=subdomains) # 子域接管风险检查
takeover.run()
return self.data
这里的检测泛域名方法为:生成3个随机域名,如果dns验证和http验证均成功,则判断页面是否相同,如果页面相似就是泛域名;如果dns验证均成功但http验证不成功,那么这个域名也是泛域名;否则不是泛域名。
def to_detect_wildcard(domain):
"""
Detect use wildcard dns record or not
:param str domain: domain
:return bool use wildcard dns record or not
"""
logger.log('INFOR', f'Detecting {domain} use wildcard dns record or not')
random_subdomains = gen_random_subdomains(domain, 3)
if not all_resolve_success(random_subdomains):
return False
is_all_success, all_request_resp = all_request_success(random_subdomains)
if not is_all_success:
return True
return any_similar_html(all_request_resp)
处理泛域名的方法:根据所有域名解析后ip
和cname
出现的次数来判断,如果一个子域名的ip或cname的出现次数超过设置的阈值,则丢弃这个子域名
def check_valid_subdomain(appear_times, info):
ip_str = info.get('ip')
if ip_str:
ips = ip_str.split(',')
for ip in ips:
ip_num = appear_times.get(ip)
isvalid, reason = is_valid_subdomain(ip=ip, ip_num=ip_num)
if not isvalid:
return False, reason
cname_str = info.get('cname')
if cname_str:
cnames = cname_str.split(',')
for cname in cnames:
cname_num = appear_times.get(cname)
isvalid, reason = is_valid_subdomain(cname=cname, cname_num=cname_num)
if not isvalid:
return False, reason
return True, 'OK'
收集模块(Collect)
动态加载certificates
, check
, datasets
, dnsquery
, intelligence
, search
文件夹中的模块查询域名这些模块都是一些能一次查询出多个子域名的模块,且模块运行时间相对较少,可进行快速查询,同时使用了多线程加快扫描速度。
certificates:利用证书透明度收集子域模块 使用在线证书查询接口查询
check:常规检查收集子域模块 例如检测网站证书 robots.txt crossdomain等
datasets:在线子域名查询借口
dnsquery:利用DNS查询收集子域模块
intelligence:利用威胁情报查询
search:利用搜索引擎查询 如google baidu fofa zoomeye等
具体的模块作用可以见 oneforall文档中的 收集模块说明
SRV爆破模块(BruteSRV)
这个收集方式还是是第一次遇见,原理是 通过爆破常见的SRV前缀来获取域名
SRV记录是dns记录的一种,一般是为Microsoft的活动目录设置时的应用。DNS可以独立于活动目录,但是活动目录必须有DNS的帮助才能工作。为了活动目录能够正常的工作,DNS服务器必须支持服务定位(SRV)资源记录,资源记录把服务名字映射为提供服务的服务器名字。活动目录客户和域控制器使用SRV资源记录决定域控制器的IP地址。
查询SRV记录的方式:
nslookup -q=srv _sip._tcp.zonetransfer.me #windows
dig srv _sip._tcp.zonetransfer.me #linux
爆破模块(Brute)
爆破模块主要利用massdns工具来爆破,在这里会进行一次更严格的泛域名检测
执行流程简单来说就是首先检测是不是泛域名,生成字典,通过massdns工具爆破,再然后根据结果生成字典递归爆破二级以上,最后处理泛域名
massdns爆破
massdns爆破主要依赖字典和dns服务器,爆破模块字典没什么说的;dns服务器主要根据是不是在国内,是否是泛域名来选择:不在国内使用国外的dns服务器,在国内则使用国内服务器,如果是泛域名使用域名绑定的dns服务器。
泛域名的检测机制
这里的检测主要依赖IPS和TTL两种方式
IPS:如果是泛域名,那么不存在的域名会解析到固定的一些ip上。
TTL:在权威 DNS 中,泛解析记录的 TTL 肯定是相同的,如果子域名记录相同,但 TTL 不同,那这条记录可以说肯定不是泛解析记录。
获取泛域名IPS和TTL
循环生成随机域名并解析 如果连续5次解析失败或者连续5次TTL不同 说明不是泛解析,退出循环
统计所有解析的ip次数,当大多数ip次数大于2次时退出循环 记录泛域名ips
def collect_wildcard_record(domain, authoritative_ns):
logger.log('INFOR', f'Collecting wildcard dns record for {domain}')
if not authoritative_ns:
return list(), int()
resolver = utils.dns_resolver()
resolver.nameservers = authoritative_ns # 使用权威名称服务器
resolver.rotate = True # 随机使用NS
resolver.cache = None # 不使用DNS缓存
ips = set()
ttl = int()
ttls_check = list()
ips_stat = dict()
ips_check = list()
while True:
token = secrets.token_hex(4)
random_subdomain = f'{token}.{domain}'
try:
ip, ttl = get_wildcard_record(random_subdomain, resolver)
except Exception as e:
logger.log('DEBUG', e.args)
logger.log('ALERT', f'Multiple query errors,'
f'try to query a new random subdomain')
continue
# 每5次查询检查结果列表 如果都没结果则结束查询
ips_check.append(ip) #空的也会加入列表
ttls_check.append(ttl)
if len(ips_check) == 5:
if not any(ips_check):#五个都是空的
logger.log('ALERT', 'The query ends because there are '
'no results for 5 consecutive queries.')
break
ips_check = list()
if len(ttls_check) == 5 and len(set(ttls_check)) == 5:
logger.log('ALERT', 'The query ends because there are '
'5 different TTL results for 5 consecutive queries.')
ips, ttl = set(), int()
break
if ip is None:
continue
ips.update(ip)
# 统计每个泛解析IP出现次数
for addr in ip:
count = ips_stat.setdefault(addr, 0)
ips_stat[addr] = count + 1
# 筛选出出现次数2次以上的IP地址
addrs = list()
for addr, times in ips_stat.items():
if times >= 2:
addrs.append(addr)
# 大部分的IP地址出现次数大于2次停止收集泛解析IP记录
if len(addrs) / len(ips) >= 0.8:
break
logger.log('DEBUG', f'Collected the wildcard dns record of {domain}\n{ips}\n{ttl}')
return ips, ttl
处理泛域名
统计解析ip和cname出现的次数,过滤ip次数超过阈值,解析到泛域名ip,解析的ttl等于泛域名ttl的域名
def is_valid_subdomain(ip=None, ip_num=None, cname=None, cname_num=None,
ttl=None, wc_ttl=None, wc_ips=None):
ip_blacklist = settings.brute_ip_blacklist
cname_blacklist = settings.brute_cname_blacklist
if cname and cname in cname_blacklist:
return 0, 'cname blacklist' # 有些泛解析会统一解析到一个cname上
if ip and ip in ip_blacklist: # 解析ip在黑名单ip则为非法子域
return 0, 'IP blacklist'
if all([wc_ips, wc_ttl]): # 有泛解析记录才进行对比
if check_by_compare(ip, ttl, wc_ips, wc_ttl):
return 0, 'IP wildcard'
if ip_num and check_ip_times(ip_num):
return 0, 'IP exceeded'
if cname_num and check_cname_times(cname_num):
return 0, 'cname exceeded'
return 1, 'OK'
dns验证(run_resolve)
通过call_massdns
函数调用massdns进行dns验证,这里会将爆破模块扫描出来的子域名再次扫描一次
http验证(run_request)
将收集到的域名和配置文件中设置的端口生成url,使用requests访问url并验证,记录http请求结果
验证http存活的依据是状态码小于500且不是400
子域爬取模块(Finder)
这里主要通过三种方式寻找子域名
1.find_in_history
:从URL跳转历史中查找子域名
2.find_in_resp
:从返回内容种查找子域名
3.find_js_urls
: 从js中查找子域名
def find_subdomains(domain, data):
subdomains = set()
js_urls = set()
db = Database()
for infos in data:
jump_history = infos.get('history')
req_url = infos.get('url')
subdomains.update(find_in_history(domain, req_url, jump_history))
rsp_html = db.get_resp_by_url(domain, req_url)
if not rsp_html:
logger.log('DEBUG', f'an abnormal response occurred in the request {req_url}')
continue
subdomains.update(find_in_resp(domain, req_url, rsp_html))
js_urls.update(find_js_urls(domain, req_url, rsp_html))
子域置换模块(Altdns)
子域替换技术可根据已有的子域生成输出大量可能存在的潜在子域,利用这些潜在子域名进一步获取存活子域名,目前oneforall中主要有5种置换方式,生成规则如下:
increase_num: test.1.foo.example.com -> test.2.foo.example.com, test.3.foo.example.com, ...
decrease_num: test.4.foo.example.com -> test.3.foo.example.com, test.2.foo.example.com, ...
insert_word: test.1.foo.example.com -> WORD.test.1.foo.example.com,test.WORD.1.foo.example.com,test.1.WORD.foo.example.com,...
add_word: test.1.foo.example.com ->test-WORD.1.foo.example.com #WORD 替换为字典
replace_word: WORD1.1.foo.example.com -> WORD2.1.foo.example.com,WORD3.1.foo.example.com,WORD4.1.foo.example.com,...
在生成子域名后该模块会对生成的潜在子域名进行dns验证和http验证,利用的也是massdns和requests。
def run(self, data, port):
logger.log('INFOR', f'Start altdns module')
self.now_subdomains = utils.get_subdomains(data)
self.get_words()
self.extract_words()
self.gen_new_subdomains()
self.subdomains = self.new_subdomains - self.now_subdomains
count = len(self.subdomains)
logger.log('INFOR', f'The altdns module generated {count} new subdomains')
self.end = time.time()
self.elapse = round(self.end - self.start, 1)
self.gen_result() # 生成的潜在域名
resolved_data = resolve.run_resolve(self.domain, self.results) # dns验证
valid_data = wildcard.deal_wildcard(resolved_data) # 强制开启泛解析处理
request.run_request(self.domain, valid_data, port) # http验证
丰富输出(Enrich)
增加cidr,asn,org,addr,isp等内容,只是丰富结果,并不会收集新的子域名。
子域接管模块(Takeover)
检测是否存在子域名接管漏洞,不收集新的子域名,具体可参考以下两篇文章
https://zhuanlan.zhihu.com/p/136694063
https://zhuanlan.zhihu.com/p/137001519
改造思路
oneforall检测上存在的一些问题
1.对子域名比较多的域名进行http验证时耗时过长,容易卡死
2.根据流程可发现,如果选择不进行http验证,则收集流程在爆破完成就结束了,后面的泛域名处理,域置换等模块完全不会执行
3.主流程里的泛域名处理方式和爆破里面的处理方式并不一样,主流程只是用出现cname,ip的次数来校验泛域名的,并没有爆破模块那么严格,所以如果阈值不合理,还是有可能多出来一些无效域名
4.递归爆破的问题,如果a.b.xxx.com存在,b.xxx.com不存在,这种情况用递归爆破是扫不出来的
5.只使用了A记录来获取域名,可能会丢失一部分域名
6.没有统一dns服务器,爆破模块和dns验证模块使用的dns服务器可能不一致
改造
1.我对oneforall的定位是发现子域名,而不是http信息收集,使用时一般是直接--req False
,对于后续的http相关的信息我会交给我的其他工具来做,各司其职,所以http的问题直接跳过了。
2.修改整体的逻辑,不http验证只是跳过http验证模块和finder模块,仍可进行后续的流程
3.增加cname记录
4.增加暴力爆破模块
5.增加检测上级域名是否存在的功能,如果不存在则扫描这个上级域名的子域名
ps: 还有一些可以优化的地方,但是会影响到整理框架,改动量有点大,后续在慢慢修改啦。
工具获取
关注公众号 回复oneforall
获取工具