红日打造内网自动漏洞工具分析系列
来自红日安全-工具研究小组成员(招募中)
简介
红日安全工具小组,主要打造一些内网安全工具。本次主要分享一些业界安全工具,以开发者的角度去分析一些想法,这个也会做成一个系列,让天下没有难写的工具,人人成为工具开发师。
0x01. 整体简介
引言
pentestEr_Fully-automatic-scanner
为了省去繁琐的手工测试和常用漏洞的搜索工作,提升工作的效率,才有了此工具,工具对于前期的收集采用了市面上大量的工具集合,不漏扫的原则,最大化的提升工具可用性,可扩展性等要求,开发次扫描器。使用方法
可以直接执行 python main.py -d cert.org.cn
思维导图
目录结构
|--dict # 存放子域名,dns服务器字典的文件夹
|--dns_server.txt
|--...
|--lib # BBSScan漏洞扫描脚本的lib库
|-- __init__.py
|--cmdline.py
|--...
|--listen # 用于监听AWVS扫描器
|--filer.py
|--report # 存放各脚本执行完成后,转换为html文件结果的文件夹,最后报告生成的文件夹
|--result # 临时存放各个脚本结果的地方,包括一些txt文件,json文件等
|--rules # 存放规则的的文件夹
|--wahtweb.json # 应用程序指纹
|--commom.txt # 漏洞扫描规则
|--subbrute # 收集信息时sublist3r.py的第三方库
|--thirdlib # 第三方库,一个轻量级的线程池
|--utils # 第三方库,包含了各种第三方网站收集子域名的方法
|--api.keys # 存放bing api的文件
|--BBScan.py
|--bingAPI
|--captcha.py
|--config.py
|--dnsbrute.py
|--gxfr.py
|--import1.php
|--main.py
|--report_all.py
|--subDomainBrute.py
|--sublist3r.py
|--upload.py
|--wahtweb.py
|--wydomain.py
|--启动程序.bat
|--wvs.bat
这个目录结构让我感觉很乱,尤其后面一大推py文件,缺少点软件设计的思想,感觉时即兴写出来的代码,很多文件还有错误,注释很少,很多时候需要debug才能知道该段代码实现的功能。
0x02.信息收集
1.域名信息收集
在进行扫描之前,按照惯例需要对目标网站的域名信息进行whois
查询,该脚本whois的实现是通过第三方网站查询得到的,不过原查询函数因为日期久远,而网站代码也已经更新了,该函数已经无法准确的获取到目标网站域名信息了
def sub_domain_whois(url_domain):
"""
通过第三方网站查询得到whois结果,然后对网页进行正则匹配,以获取到网页内容中的whois结果
"""
um=[]
a1=requests.get("http://whois.chinaz.com/%s" %(url_domain))
if 'Registration' not in a1.text:
print 'whois error'
else:
print a1.text
# 使用正则匹配想要获取的内容,假如目标网站的前端代码改变了,那么该正则就失效了
out_result=re.findall(r'<pre class="whois-detail">([\s\S]*)</pre>', a1.text.encode("GBK",'ignore'))
out_result_register=re.findall(r'http://(.*?)"', a1.text.encode("GBK",'ignore'))
for x in out_result_register:
if 'reverse_registrant/?query=' in x:
um.append(x)
break
for x in out_result_register:
if 'reverse_mail/?query=' in x:
um.append(x)
break
print um[0], um[1]
print out_result[0]
# 将获取到的结果存放在的一个html文件中,以便最后生成报告
with open('report/whois_email_user.html','w') as fwrite:
fwrite.write('register_user:')
fwrite.write('<a href="http://' + um[0] + '">注册者反查询</a>')
fwrite.write('<br>')
fwrite.write('email:')
fwrite.write('<a href="http://' + um[1] + '">邮箱反查询</a>')
fwrite.write('<br>')
fwrite.write('<pre>')
fwrite.write(out_result[0])
fwrite.write('</pre>')
def sub_domain_whois(url_domain):
import json
a = requests.get("http://api.whoapi.com/?domain={}&r=whois&apikey=demokey".format(url_domain))
result = a.text
r = json.loads(result)
for k,v in r.items():
print(k,v)
当然如果需要一些详细的信息,可能还是需要对一些网站的内容进行爬取才行。
2.子域名收集
对于子域名收集,这个系统在实现的时候,为了收集到尽可能多的代码,使用了很多第三方脚本,这里就出现了一个问题,这种使用脚本的方法让代码可读性很差,而且维护困难,很多代码现在已经不适用了。
使用到的脚本名称与介绍
脚本名称 | 介绍 | 使用方法 | 返回内容 |
---|---|---|---|
gxfr.py | 使用高级搜索引擎(bing,baidu,google)查询来枚举子域并执行dns查找,这个程序使用的是bing的API对子域名进行收集 | python gxfr.py --bxfr --dns-lookup -o --domain url_domain | 程序会将结果保存到一个domain_gxfr1.txt这样的文件中,api已经不可用 |
subDomainsBrute.py | 提供的常用的子域名字符串字典,然后通过与域名进行组合,配合DNS服务器确定是否存在组合后的子域名 | python subDomainsBrute.py domain | 将字典枚举组合解析成功后的域名存放在domain_jiejie.txt文件中 |
wydomain.py | 通过使用互联网上的第三方接口或者爬取查询结果来获取目标的子域名 | python wydomain domain | 通过不同网站获取的结果会存在本地的不同的.json文件中 |
sublist3r.py | 使用百度,雅虎,bing等第三方引擎对目标域名进行子域名收集,而且还提供字典枚举的功能和端口扫描的功能 | python sublist3r -d doamin -o domain_sublistdir.txt | 将获取到的子域名结果存在本地的txt文件中 |
gxfr.py文件
该py文件是使用bing的API,谷歌的搜索引擎对目标域名的子域名进行查询。主要的两个函数为bxfr
函数和gxfr
函数。
bxfr
函数,使用Bing的API进行子域名解析和查询,该函数需要提供Bing相关功能的API Key。然后访问` https://api.datamarket.azure.com/Data.ashx/Bing/Search/Web?Query=domain&Misplaced &format=json经过测试该API接口已经不可用。通过该API获取子域名结果后,使用
lookup_subs函数进行socket函数获取地址并成功后(
socket.getaddrinfo(site, 80)`),将结果存储在txt文件中中。gxfr
函数,使用google搜索引擎的hack语法进行查询(site:baidu.com
),然后通过正则表达式进行匹配pattern = '>([\.\w-]*)\.%s.+?<' % (domain)
。然后获取匹配结果。最后经过lookkup_subs
函数验证之后写入txt文件中。
subDomainsBrute.py文件
该py文件通过提供的子域名字典,对目标域名的子域名进行枚举然后解析测试,最后输出结果。这里提供了两个字典文件,分别为dict/subnames.txt
和dict/next_sub.txt
。还有一个dns服务器列表
114.114.114.114
8.8.8.8
180.76.76.76
223.5.5.5
223.6.6.6
程序简介
def __init__(self, target, names_file, ignore_intranet, threads_num, output):
self.target = target.strip() #目标域名
self.names_file = names_file # 自定义的域名字典路径
self.ignore_intranet = ignore_intranet # 是否忽略私网地址对应的域名,这里关系到是否运行is_intranet函数
self.thread_count = self.threads_num = threads_num # 线程数量
self.scan_count = self.found_count = 0 # 初始话子域名扫描数量和已确定存在的数量
self.lock = threading.Lock()
self.console_width = getTerminalSize()[0] - 2 # Cal terminal width when starts up
self.resolvers = [dns.resolver.Resolver() for _ in range(threads_num)] # 使用第三方模块dns.resolver解析子域名已确认是否解析成功
self._load_dns_servers() # 加载dns服务器
self._load_sub_names() # 加载子域名字典
self._load_next_sub() # 加载二级子域名字典
outfile = target + '_jiejie.txt' if not output else output# 输出结果保存的文件名称
self.outfile = open(outfile, 'w') # won't close manually
self.ip_dict = {} # 已经成功解析的ip及其对应的数量
self.STOP_ME = False
该程序的执行流程就是,先从字典中加载字符串,然后将字符串与目标域名进行拼接得到一个子域名,通过第三方模块dns.resolver
对其进行解析,解析成功就保存在txt文件中。关键代码如下:
cur_sub_domain = sub + '.' + self.target
# 第三方模块,判断子域名是否存在
answers = d.resolvers[thread_id].query(cur_sub_domain)
is_wildcard_record = False
if answers:
for answer in answers:
self.lock.acquire()
if answer.address not in self.ip_dict:
self.ip_dict[answer.address] = 1
else:
self.ip_dict[answer.address] += 1
if self.ip_dict[answer.address] > 2: # a wildcard DNS record
is_wildcard_record = True
self.lock.release()
wydomain.py文件
该程序是通过调用多个第三方接口对目标域名进行子域名查询,然后将查询结果分别存储在一个json文件中。
Alexa
fetch_chinaz
函数
url = 'http://alexa.chinaz.com/?domain={0}'.format(self.domain)
r = http_request_get(url).content
subs = re.compile(r'(?<="\>\r\n<li>).*?(?=</li>)')
fetch_alexa_cn
函数
url = 'http://www.alexa.cn/index.php?url={0}'.format(self.domain)
r = http_request_get(url).text
sign = re.compile(r'(?<=showHint\(\').*?(?=\'\);)').findall(r)
Threatcrowd
class Threatcrowd(object)
url = "{0}/searchApi/v2/domain/report/?domain={1}".format(self.website, self.domain)
content = http_request_get(url).content
for sub in json.loads(content).get('subdomains'):
Threatminer
class Threatminer(object)
url = "{0}/getData.php?e=subdomains_container&q={1}&t=0&rt=10&p=1".format(self.website, self.domain)
content = http_request_get(url).content
_regex = re.compile(r'(?<=<a href\="domain.php\?q=).*?(?=">)')
for sub in _regex.findall(content):
Sitedossier
class Sitedossier(object)
url = 'http://www.sitedossier.com/parentdomain/{0}'.format(self.domain)
r = self.get_content(url) # 获取服务器回复的内容
self.parser(r) # 从内容中提取我们想要的子域名
"""
部分代码如下
npage = re.search('<a href="/parentdomain/(.*?)"><b>Show', response)
if npage:
for sub in self.get_subdomain(response):
self.subset.append(sub)
"""
return list(set(self.subset))
Netcraft
class Netcraft(object)
self.site = 'http://searchdns.netcraft.com'
self.cookie = self.get_cookie().get('cookie')
url = '{0}/?restriction=site+contains&position=limited&host=.{1}'.format(
self.site, self.domain)
r = http_request_get(url, custom_cookie=self.cookie)
self.parser(r.text) # 对服务器返回的内容进行解析,获取子域名信息
"""
部分代码信息
npage = re.search('<A href="(.*?)"><b>Next page</b></a>', response)
"""
return list(set(self.subset))
Ilinks
class Ilinks(object)
self.url = 'http://i.links.cn/subdomain/'
payload = {
'b2': 1,
'b3': 1,
'b4': 1,
'domain': self.domain
}
r = http_request_post(self.url,payload=payload).text
subs = re.compile(r'(?<=value\=\"http://).*?(?=\"><input)')
Chaxunla
class Chaxunla(object)
self.url = 'http://api.chaxun.la/toolsAPI/getDomain/'
url = "{0}?0.{1}&callback=&k={2}&page=1&order=default&sort=desc&action=moreson&_={3}&verify={4}".format(
self.url, timestemp, self.domain, timestemp, self.verify)
result = json.loads(req.get(url).content)
json_data = read_json(_burte_file)
if json_data:
subdomains.extend(json_data)
......
subdomains = list(set(subdomains))
_result_file = os.path.join(script_path, outfile)
save_result(_result_file, subdomains)
sublist3r.py文件
该文件使用百度,雅虎,bing等第三方引擎对目标域名进行子域名收集,而且还提供字典枚举的功能和端口扫描的功能在该系统中只用到了该程序的子域名收集功能。使用到的模块也与之前的wydomain.py文件有很多重复的地方
enumratorBase基类
该文件的类都是继承至该类,而这个基类也是由作者自定义的一个类。简单解释一下该类的功能
print_banner
方法子类通过继承,可以通过该函数打印出该类使用的一些接口的信息send_req
方法发送请求的方法,该方法中自定义了大量的http头部变量,该方法返回的服务器回复的数据get_response
方法从response对象中获取html内容,并返回check_max_subdomains
方法该方法是用来设置寻找子域名最大个数的,如果得到子域名的数量到达该函数设置数量时,程序就会停止继续寻找子域名check_max_pages
方法比如google搜索引擎是需要指定探索的最大页数,否则会无限制的探索下去。check_response_errors
方法该方法检查对服务器的请求是否成功完成should_sleep
方法在进行子域名收集的时候,为了避免类似于google搜索引擎的机器识别,应该设置休眠时间enumerate
方法通过该方法获取子域名
而以下的子域名检索方法是由enumratorBase类派生出来的,然后根据各自的特点进行修改后形成的类,简单的介绍一下功能
类名称 | 实现的功能 |
---|---|
GoogleEnum | 通过google搜索引擎检索目标子域名 |
YahooEnum | 通过雅虎搜索引擎检索目标子域名 |
AskEnum | 通过http://www.ask.com/web检索子域名 |
BingEnum | 通过bing检索子域名 |
BaiduEnum | 通过百度检索子域名 |
NetcraftEnum | 通过http://searchdns.netcraft.com/?restriction=site+ends+with&host={domain}检测子域名 |
Virustotal | 通过'https://www.virustotal.com/en/domain/{domain}/information/检索子域名 |
ThreatCrowd | https://www.threatcrowd.org/searchApi/v2/domain/report/?domain={domain}检索子域名 |
CrtSearch | 通过https://crt.sh/?q=%25.{domain}检索子域名 |
PassiveDNS | 通过http://ptrarchive.com/tools/search.htm?label={domain}检索子域名 |
此外该脚本还提供端口扫描功能和字子域名字典枚举的功能,虽然该项系统并未使用这些功能
postscan
函数关键代码,使用socket模块去尝试连接目标端口,如果超过2s目标没有回复,则判断目标没有开放该端口
for port in ports:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(2)
result = s.connect_ex((host, int(port)))
if result == 0:
openports.append(port)
对与子域名枚举功能,该脚本调用了subbrute.py中的函数,用于对字典中字符串拼成的子域名进行验证操作。 关键代码
bruteforce_list = subbrute.print_target(parsed_domain.netloc, record_type, subs, resolvers, process_count, output, json_output, search_list, verbose)
"""
print_target函数是subbrute文件中的函数,用于验证子域名是否是可用的
"""
最后通过一个make_domain
函数将所有的以.txt
为后缀名的文件全部复制到all_reqult.log文件中,通过读取后格式化处理,将结果存储到report/result.txt文件中关键代码如下
os.system("copy *.txt all_reqult.log && del *.txt")
print("[+]del *txt\ncopy txt\n")
list_domain = []
f = open("all_reqult.log", "r")
......
f.close()
os.system("del all_reqult.log")
list_domain = list(set((list_domain)))
print list_domain, len(list_domain)
with open('result.txt', 'w') as outfile:
outfile.write('\n'.join(list_domain))
fopen1 = open('report/result.txt', 'w')
fopen1.write('\n'.join(list_domain))
fopen1.close()
3.Web信息获取
对于获取Web网页的信息,这里主要是获取网页使用的是什么框架或者CMS,通过本地提供的一个json规则检测文件,对目标网站进行检测和指纹识别json文件中部分规则如下:
{
"discuz": [
{
"url": "/static/image/admincp/bg_repno.gif",
"md5": "403889213f03534a0651d7cfd6878b2c"
},
{
"url": "/",
"text": "<meta name=\"generator\" content=\"Discuz!",
"regexp": "<script src=\".*?logging\\.js"
}
],
"phpwind": [
{
"url": "/res/js/dev/wind.js",
"md5": "7ad9ac3d647e00e12c615a06762430fe"
},
{
"url": "/",
"regexp": "<meta name=\"generator\" content=\"(phpwind|PHPWind)"
}
],
"phpmyadmin": [
{
"url": "/",
"text": "<link rel=\"stylesheet\" type=\"text/css\" href=\"phpmyadmin.css.php",
"regexp": "<title>phpMyAdmin</title>"
}
],
"wordpress": [
{
"url": "/wp-login.php",
"text": ["wp-core-ui", "click-backtoblog"]
}
],
"zblog": [
{
"url": "/",
"regexp": "<link rel=\"stylesheet\" rev=\"stylesheet\" href=\".*zb_users"
}
],
......
实现的过程是将report/result.txt
中所有的域名根据json文件中的规则进行url拼接,然后对这个特定URL进行访问获取网页内容,再将网页内容与规则进行匹配,最后判断使用cms或者框架名称。关键代码如下
r = requests.get(url1+ rule["url"], timeout=5)
r.encoding = r.apparent_encoding
r.close()
if "md5" in rule and hashlib.md5(r.content).hexdigest() == rule["md5"]:
print (url1, "<font color=red>"+cms+"</font>")
elif "field" in rule and rule["field"] in r.headers and rule["value"] in r.headers[rule["field"]]:
print (url1, "<font color=red>"+cms+"</font>")
elif "text" in rule:
if type(rule["text"]) is list:
for itext in rule["text"]:
if itext in r.text:
print (url1, "<font color=red>"+cms+"</font>")
elif rule["text"] in r.text:
print (url1, "<font color=red>"+cms+"</font>")
elif "regexp" in rule:
if type(rule["regexp"]) is list:
for reg in rule["regexp"]:
if re.search(reg, r.text):
print (url1, "<font color=red>"+cms+"</font>")
elif re.search(rule["regexp"], r.text):
print (url1, "<font color=red>"+cms+"</font>")
else:
print 'no'
注意该脚本还引入了一个第三方模块from thirdlib import threadpool
该threadpool是一个简单的实现线程池的框架,方便使用脚本使用多线程。脚本中调用的关键代码
pool = threadpool.ThreadPool(self.thread_num)
reqs = threadpool.makeRequests(self.identify_cms, self.rules, self.log)
for req in reqs:
pool.putRequest(req)
pool.wait()
在系统的主函数中通过接受该脚本stdout输出的流数据,将数据写入到一个列表中,最后将结果保存到report/whatweb.html
。
0x03.漏洞扫描
该系统的漏洞扫描模块是调用的第三方脚本BBScan.py,该文件代码实现了一个轻量级的web漏洞扫描器。使用的规则库为存放在本地的一个txt文件中rules/commom.txt
部分规则如下:
# Admin Panel
/admin/ {severity=2}
/config/ {severity=2}
/manage/ {severity=2}
/backup/ {severity=2}
/backend/ {severity=2}
/admin.php {severity=2} {status=200}
/admin.jsp {severity=2} {status=200}
/admin.do {severity=2} {status=200}
# Tomcat Examples
/examples {severity=2}
/examples/servlets/servlet/SessionExample {severity=2}
/manager/html {severity=2}
# Database
/db/ {severity=2}
/DB/ {severity=2}
/data/ {severity=2}
/sqlnet.log {status=200} {type_no="html"}
/data/user.txt {status=200} {type_no="html"}
/user.txt {status=200} {type_no="html"}
/phpinfo.php {tag="allow_url_fopen"} {status=200}
/mysql/add_user.php {status=200}
/cachemonitor/statistics.jsp {severity=2} {status=200}
/jmx-console/ {severity=2}
/jmx-console/HtmlAdaptor {status=200}
/cacti/ {severity=2}
/cacti/cacti.sql {status=200} {type_no="html"}
# Directory traversal
/../../../../../../../../../../../../../etc/passwd {tag="root:x:"}
# Discuz!
/config/config_ucenter.php.bak {status=200}
# webshell
/shell.php {status=200}
/shell.jsp {status=200}
/{hostname}.zip {status=200} {type_no="html"}
# Resin
/resin-doc/resource/tutorial/jndi-appconfig/test?inputFile=/etc/passwd {tag="root:x:"}
# Java web
/WEB-INF/web.xml {tag="xml"} {status=200}
/WEB-INF/web.xml.bak {tag="xml"} {status=200}
# SVN and Git
/.svn/
/.svn/entries {tag="dir\n"} {status=200}
# Wordpress
/wp-login.php {tag="user_login"} {status=200}
/config.inc {status=200} {type_no="html"}
/config.ini {status=200} {type_no="html"}
脚本通过从这个文件中读取规则,对目标进行测试,然后根据服务器返回的内容进行判断是否存在该类型的漏洞。代码简介
def __init__(self, url, lock, timeout=600, depth=2):
self.START_TIME = time.time() # 扫描开始时间
#self.lock = lock
self.TIME_OUT = timeout # 默认的网站访问超时时间
self.LINKS_LIMIT = 20 # 最大连接数量
self.final_severity = 0 # 默认的安全等级
self.schema, self.host, self.path = self._parse_url(url) # 对URL进行解析
self.max_depth = self._cal_depth(self.path)[1] + depth # 最大扫描的深度
self.check_404() # 检测目标网页是否是404
if self._status == -1:
return None
if not self.has_404:
print '[Warning] %s has no HTTP 404.' % self.host
self._init_rules() # 初始化规则
self.url_queue = Queue.Queue() # 将所有要扫描的url加入到队列中
self.urls_in_queue = [] # urls already in queue
_path, _depth = self._cal_depth(self.path)
self._enqueue(_path) # 判断是否是已经爬取过的URL路径
self.crawl_index(_path) # 方法,爬取目标主页中的url
self.lock = threading.Lock()
self.results = {}
方法简介
_parse_url
方法:解析URL,如果没有指定通信协议,系统自动添加为http协议。然后判断url中是否存在路径,如果不存在就返回"/",否则返回协议,主机名,和资源路径
return _.scheme, _.netloc, _.path if _.path else '/'
_cal_depth
:该方法是用于计算URL的深度,返回一个元组,url和深度。以//或者javascript开头的URL不做分析,以http开头的URL先对URL进行解析,然后判断hostnane是否为目标的hostname,在判断path路径的深度。关键代码如下
_ = urlparse.urlparse(url, 'http')
if _.netloc == self.host: # same hostname
url = _.path
。。。。
url = url[: url.rfind('/')+1]
depth = len(url.split('/')) - 1
_init_rules
:该方法从文件中加载规则,使用正则表达式从文件中的每一行提取数据,正则表达式如下
p_severity = re.compile('{severity=(\d)}')
p_tag = re.compile('{tag="([^"]+)"}')
p_status = re.compile('{status=(\d{3})}')
p_content_type = re.compile('{type="([^"]+)"}')
p_content_type_no = re.compile('{type_no="([^"]+)"}')
提取数据并进行判断,没有的置为空或者0
_ = p_severity.search(url)
severity = int(_.group(1)) if _ else 3
_ = p_tag.search(url)
tag = _.group(1) if _ else ''
_ = p_status.search(url)
status = int(_.group(1)) if _ else 0
_ = p_content_type.search(url)
content_type = _.group(1) if _ else ''
_ = p_content_type_no.search(url)
content_type_no = _.group(1) if _ else ''
最后将重组的规则存放在一个元组中,最后将这个元组追加到一个列表中
self.url_dict.append((url, severity, tag, status, content_type, content_type_no))
_http_request
方法:通过该方法获取访问目标url的状态码,返回http头部和html内容部分代码如下
conn.request(method='GET', url=url,
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36 BBScan/1.0'}
)
resp = conn.getresponse()
resp_headers = dict(resp.getheaders())
status = resp.status
_decode_response_text
方法:该方法对服务器返回的页面进行解码操作,如果用户没有指定charset类型,那么该方法就会尝试使用'UTF-8', 'GB2312', 'GBK', 'iso-8859-1', 'big5'编码对目标返回的内容进行解码,并将解码后的内容返回部分代码
encodings = ['UTF-8', 'GB2312', 'GBK', 'iso-8859-1', 'big5']
for _ in encodings:
try:
return rtxt.decode(_)
except:
pass
check_404方法
:检查目标返回的页面的状态码是否为404部分代码如下
self._status, headers, html_doc = self._http_request('/A_NOT_EXISTED_URL_TO_CHECK_404_STATUS_EXISTENCE')
if self._status == -1:
print '[ERROR] Fail to connect to %s' % self.host
self.has_404 = (self._status == 404)
return self.has_404
_enqueue
方法:该方法是判断爬取的URL是否是已经爬取的,如果是一个新的链接就传入队列中,该队列用于爬虫部分代码
if url in self.urls_in_queue:
return False
elif len(self.urls_in_queue) >= self.LINKS_LIMIT:
return False
else:
self.urls_in_queue.append(url)
还通过该方法将漏洞检测规则对应到URL上,然后组成一个元组,将这个元组传入一个用于扫描漏洞的队列中,代码如下
for _ in self.url_dict: # 这个列表中包含一个元组[(url, severity, tag, status, content_type, content_type_no)]
full_url = url.rstrip('/') + _[0] # 获取完整的URL,将hostname与path拼接
url_description = {'prefix': url.rstrip('/'), 'full_url': full_url} # 重新裁成一个字典
item = (url_description, _[1], _[2], _[3], _[4], _[5])
self.url_queue.put(item)
crawl_index方法
:该方法使用beautifulSoup4爬取页面中的url链接。部分代码如下
soup = BeautifulSoup(html_doc, "html.parser")
links = soup.find_all('a')
for l in links:
url = l.get('href', '')
url, depth = self._cal_depth(url)
if depth <= self.max_depth:
self._enqueue(url)
_update_severity
方法:该方法用于更新serverity,如果规则中存在serverty字段,那么就将默认的final_serverity进行修改
if severity > self.final_severity:
self.final_severity = severity
_scan_worker
方法:该方法是用于执行漏洞扫描的关键代码如下
try:
# 获取url以及相对应的规则组成的元组
item = self.url_queue.get(timeout=1.0)
except:
return
try:
url_description, severity, tag, code, content_type, content_type_no = item
url = url_description['full_url']
prefix = url_description['prefix']
except Exception, e:
logging.error('[InfoDisScanner._scan_worker][1] Exception: %s' % e)
continue
if not item or not url:
break
。。。。
# 开始进行测试,将URL加上payload之后发送给服务器,本地根据服务器返回的数据与url_description中存放的数据进行对比,如果结果一样,说明目标存在漏洞
status, headers, html_doc = self._http_request(url)
if (status in [200, 301, 302, 303]) and (self.has_404 or status!=self._status):
if code and status != code:
continue
if not tag or html_doc.find(tag) >= 0:
if content_type and headers.get('content-type', '').find(content_type) < 0 or \
content_type_no and headers.get('content-type', '').find(content_type_no) >=0:
continue
self.lock.acquire()
# print '[+] [Prefix:%s] [%s] %s' % (prefix, status, 'http://' + self.host + url)
if not prefix in self.results:
self.results[prefix]= []
# 存放最后结果
self.results[prefix].append({'status':status, 'url': '%s://%s%s' % (self.schema, self.host, url)} )
self._update_severity(severity)
self.lock.release()
scan
方法:这是一个多线程启动器,用来启动_scan_worker方法,最后返回测试的主机名,测试的结果和严重程度代码如下
for i in range(threads):
t = threading.Thread(target=self._scan_worker)
threads_list.append(t)
t.start()
for t in threads_list:
t.join()
for key in self.results.keys():
if len(self.results[key]) > 10:
del self.results[key]
return self.host, self.results, self.final_severity
1.nmap扫描
通过直接调用nmap对目标进行扫描,扫描结果储存在nmap_port_services.xml
中,启动的命令为
nmap -v --script=banner,http-headers,http-title -T4 -iL 'result.txt' -oX nmap_port_services.xml
2.AWVS扫描
使用命令行调用AWVS对目标网站进行扫描,并在系统中启用一个线程去监测该进程的运行情况,代码如下
while not os.path.exists('result.txt'):
time.sleep(20)
print 'listen ......'
print 'starting wvs scan'
popen = subprocess.Popen("cmd.exe /c" + "wvs.bat", stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
while True:
next_line = popen.stdout.readline()
if next_line == '' and popen.poll() != None:
break
sys.stdout.write(next_line)
wvs.bat
的代码如下:
@echo off
set /p wvs_path=please input wvs path,eg: [D:\Program Files (x86)\Web Vulnerability Scanner 10]::
for /f %%i in (result.txt) do (
set pp=%%i
echo running %%i ......
"%wvs_path%\wvs_console.exe" /scan %%i /Profile default /SaveFolder d:\wwwscanresult\%pp%\ /save /Verbose --EnablePortScanning=true --UseCSA=true --RobotsTxt=true --CaseInsensitivePaths=true
)
0x04.报告生成
首先是将nmap生成的XML文件通过import1.php脚本进行格式化后重新输出,核心代码如下:
@file_put_contents("report/result_sentives.html", '<h2><font color=red>'.$ip.'</font></h2>', FILE_APPEND);
print $ip.'<br>';
foreach ($p->port as $pp){
$p_protocol=$pp['protocol'];
$p_port=(int)$pp['portid'];
$p_service_http_title=$pp->script["http-title"]['output'];
$p_banner=$pp->script['output'];
$p_service=$pp->service["name"];
$total++;
$fwritt='<b>'.$p_protocol.'</b> '.'<a href="http://'.$ip.':'.$p_port.'"" target=_blank><b>'.$p_port.'</b></a>'.' <b>'.$p_service.'</b> <pre>'.$p_service_http_title.'</pre> <pre>'.$p_banner.'</pre> '.' '.'<br>';
print $fwritt;
@file_put_contents("report/result_sentives.html", $fwritt, FILE_APPEND);
}
最后将所有的结果通过repoert_all
函数将结果进行整合,将这些文件的路径汇总到left,html中。代码如下:
html_doc = left_html.substitute({'domain_whois_email': 'whois_email_user.html', 'domain_result': 'result.txt',
'domain_port': 'result_sentives.html', 'domain_webscan': 'result_sentivesweb.html',
'domain_whatweb': 'whatweb.html'})
with open('report/left.htm', 'w') as outFile:
outFile.write(html_doc)
0x05.总结
对于该系统而言只能说是能够满足个人的使用,且后期代码维护难度过大,不易进行扩展,使用第三方脚本时,直接是使用命令执行来获取数据,而不是通过导入模块的方式。不过系统的思路是值得借鉴的,尤其是在前期搜集子域名信息的时候是花了大功夫的,调用了很多第三方脚本,为的是尽可能的将目标的域名下的子域名收集完整,值得学习。而对于漏洞扫描模块而言,即使用了第三方脚本,也是用的AWVS对目标进行扫描,对于BBScan这个扫描储程序的规则设计值得学习,自定义化程度很高。总体来说整个扫描器的设计还是不错的,只是将这些第三方脚本进行整合时就显得有点仓促了。