设计要求
设计并制作出一种漏洞扫描平台,其要求如下:
( 1 )熟悉爬虫,爬取漏洞、设备详细数据,构建漏洞库、设备指纹库。
( 2 )使用 nmap 工具扫描网络,使用 zgrab2 工具辅助扫描网络。
( 3 )完成设备指纹识别,漏洞匹配过程。
( 4 )使用 nessus 工具验证漏洞。
( 5 )搭建可视化平台。
( 6 )完成设计报告
爬虫:
本项目中使用 python 语言对 CVE_Details、CNVD、 CNNVD、securityfocus、 ics_cnvd 等漏洞网站爬取不少于 100000 条漏洞详细信息,爬取“CVE 编号”、
“危害等级”、“漏洞类型”、“供应商”、“型号”、“设备类型”、“固件版本号”等
信息,构建 CVE 漏洞-设备信息映射库。同时,使用 python 语言对京东、亚马逊、淘宝等电商网站爬取不少于 100000
条设备详细信息,爬取“设备类型”、“设备品牌”、“设备型号”等信息,并将其
构建一个设备指纹库。
本节小目录 (本文为作者踩坑记录,先看别直接上手,代码在最后)
scrapy框架
Scrapy是一个快速的高级Web爬网和Web爬网框架,用于对网站进行爬网并从其页面中提取结构化数据。
Scrapy Engine(引擎): 负责Spider、ItemPipeline、 Downloader、Scheduler中间的通讯, 信号、数据传递等,
Scheduler(调度器): 负责接受引擎发送过来的Request请求,并按照一定的方式进行整理排列, 入队,当引擎需要时,交还给引擎,
Downloader (下载器): 负责下载Scrapy Engine(引擎发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spider来处理,
Spider (爬虫) : 它负责处理所有Responses,从中分析提取数据,获取ltem字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),
Item Pipeline(管道): 负责处理Spider中获取到的Item,并进行进行后期处理(详细分析、过滤存储等)的地方.
Downloader Middlewares (下载中间件) : 你可以当作是一个可以自定义扩展下载功能的组件。
Spider Middlewares (Spider中间件) : 你可以理解为是一个可以自定扩展和操作引擎和Spider中间通信的功能组件(比如进入Spider的Responses;和从Spider出去的Requests)
运作流程:
1、从spider中获取到初始url给引擎,告诉引擎帮我给调度器;
2、引擎将初始url给调度器,调度器安排入队列;
3、调度器告诉引擎已经安排好,并把url给引擎,告诉引擎,给下载器进行下载;
4、引擎将url给下载器,下载器下载页面源码;
5、下载器告诉引擎已经下载好了,并把页面源码response给到引擎;
6、引擎拿着response给到spider,spider解析数据、提取数据;
7、spider将提取到的数据给到引擎,告诉引擎,帮我把新的url给到调度器入队列,把信息给到Item Pipelines进行保存;
8、Item Pipelines将提取到的数据保存,保存好后告诉引擎,可以进行下一个url的提取了;
9、循环3-8步,直到调度器中没有url,关闭网站(若url下载失败了,会返回重新下载)。
安装scrapy
直接用anaconda或者pip命令安装,推荐装入venv虚拟环境
conda install scrapy 或 pip install scrapy
可以在命令行直接输入scrapy
验证是否安装成功
创建项目
scrapy startproject 项目名
就会创建以下目录结构的项目文件夹
tutorial/
scrapy.cfg # deploy configuration file
tutorial/ # project's Python module, you'll import your code from here
__init__.py
items.py # project items definition file
middlewares.py # project middlewares file
pipelines.py # project pipelines file
settings.py # project settings file
spiders/ # a directory where you'll later put your spiders
__init__.py
这些文件分别是:
scrapy.cfg: 项目的配置文件,现在可以先忽略。
tutorial/: 该项目的python模块。
tutorial/items.py: 项目中的item文件。
Item 是保存爬取到的数据的容器;其使用方法和python字典类似, 并且提供了额外保护机制来避免拼写错误导致的未定义字段错误。
tutorial/pipelines.py: 项目中的pipelines文件。
Scrapy提供了pipeline模块来执行保存数据的操作。在创建的 Scrapy 项目中自动创建了一个 pipeline.py 文件,同时创建了一个默认的 Pipeline 类。比如我们可以在里面写把item提取的数据保存到mysql数据库的方法。
tutorial/settings.py: 项目的设置文件。
settings.py是Scrapy中比较重要的配置文件,里面可以设置的内容非常之多。
tutorial/spiders/: 放置spider代码的目录。爬虫文件就放在里面
创建爬虫
scrapy genspider 爬虫名 要爬取的网站域名 # 注意爬虫名不要和项目名冲突,网站域名指一级域名,如:baidu.com
生成的爬虫文件会放在文件夹项目名/spider/
下为爬虫名.py
启动爬虫
scrapy crawl 爬虫名字 # 注意是名字,不是爬虫py文件
CVE_Details爬虫
先从setting讲起还是从网页讲起呢
不说话那就网页吧!
访问https://www.cvedetails.com
可以进入CVE_Details的主页面,这就是我们的重要漏洞库来源,看左边Browse–>vulnerabilities by data 点进去会发现自有记录以来所有的漏洞数量信息,按照年->月->页数来爬取太麻烦,跳过月,直接按照每年的爬取
随便点进一个年份,发现url为https://www.cvedetails.com/vulnerability-list/year-1999/vulnerabilities.html
感觉可以替换里面的年份,试试果然可以成功访问,由于每年漏洞数量和页数不一样,页数的最大值不好搞,,直接选择前面这个简单的漏洞数量,然后进行除以50的向上取整运算,就能得到页数
再观察观察每一页的url,发现第2页开始url就变复杂了
https://www.cvedetails.com/vulnerability-list.php?vendor_id=0&product_id=0&version_id=0&page=3&hasexp=0&opdos=0&opec=0&opov=0&opcsrf=0&opgpriv=0&opsqli=0&opxss=0&opdirt=0&opmemc=0&ophttprs=0&opbyp=0&opfileinc=0&opginf=0&cvssscoremin=0&cvssscoremax=0&year=1999&month=0&cweid=0&order=1&trc=894&sha=8fdcb89732c98600636042e1eff8c1b2ff5cb25d
尝试过后发现几个重要的参数,page(页数), year(年份), trc(这个就是数量,去掉也没事)
问题解决,通过 year构造的url --> 得到页数并构造页数的url --> 得到每一条cve链接 -->访问cve页面爬取信息返回管道
最重要的是xpath选择器,推荐看官方文档的介绍,然后推荐一个很方便尝试的方式,比如第一步,我们访问用year构造的url,然后用选择器得到页数,可以像下面一样在cmd中输入
scrapy shell https://www.cvedetails.com/vulnerability-list/year-1999/vulnerabilities.html
可以很方便的进行选择器的调试,我通常在这里试验选择器效果
回到这个网页,只需要获取这个div下的b标签
在cmd里试验
>>> response.selector.xpath('//div[@class="paging"]')
[<Selector xpath='//div[@class="paging"]' data='<div class="paging" id="pagingt" styl...'>, <Selector xpath='//div[@class="paging"]' data='<div class="paging" id="pagingb">\n\tTo...'>]
>>> response.selector.xpath('//div[@class="paging"]').get()
'<div class="paging" id="pagingt" style="display:none; clear:both;"></div>'
>>> response.selector.xpath('//div[@class="paging"]/b').get()
'<b>894</b>'
>>> response.selector.xpath('//div[@class="paging"]/b/text()').get()
'894'
轻松到手!于是可以开始写了,在生成的py文件里应该有默认代码,稍微改改就能自己用
class CveDetailSpider(scrapy.Spider):
name = 'cve_detail'
allowed_domains = ['https://www.cvedetails.com']
start_urls = [
"https://www.cvedetails.com/vulnerability-list/year-" + str(i) + "/vulnerabilities.html" for i in range(1999, 2021)
] # 建议大家这里改为range(2020, 1998, -1)倒序爬取
def parse(self, response):
# 得到页数,生成url
# 获取cve的数量
nums = response.selector.xpath('//div[@id="pagingb"]/b/text()').get()
# 向上取整算出页数
pages = ceil(int(nums)/50)
# 遍历年份1999-2020年
for year in range(1999, 2021):
# 遍历页数
for page in range(1, pages+1):
# 通过page,year,nums生成页面的url
newurl = self.get_url(str(page), str(year), str(nums))
yield scrapy.Request(url=newurl, callback=self.parse1, dont_filter=True)
scrapy默认从start_urls[] 寻找可爬取的url,然后默认调用parse()进行访问,注意ceil()方法需要导入math库函数,from math import ceil
get_url()是我写的生成url的函数,如下代码
yield 两句话搞定,首先它相当于return,同时它还是一个生成器
def get_url(self, page, year, trc):
return "https://www.cvedetails.com/vulnerability-list.php?vendor_id=0&product_id=0&version_id=0&page={}&hasexp=0&opdos=0&opec=0&opov=0&opcsrf=0&opgpriv=0&opsqli=0&opxss=0&opdirt=0&opmemc=0&ophttprs=0&opbyp=0&opfileinc=0&opginf=0&cvssscoremin=0&cvssscoremax=0&year={}&month=0&cweid=0&order=1&trc={}&sha=ef7bb39664f094781e7b403da0e482830f5837d6".format(page, year, trc)
yield scrapy.Request()
有两个参数,url和回调函数,我写了另外一个回调函数parse1()来处理下阶段的页面解析
继续使用cmd进行分析(上一次的退出命令是exit()
)
scrapy shell https://www.cvedetails.com/vulnerability-list.php?vendor_id=0&product_id=0&version_id=0&page=3&hasexp=0&opdos=0&opec=0&opov=0&opcsrf=0&opgpriv=0&opsqli=0&opxss=0&opdirt=0&opmemc=0&ophttprs=0&opbyp=0&opfileinc=0&opginf=0&cvssscoremin=0&cvssscoremax=0&year=1999&month=0&cweid=0&order=1&trc=894&sha=8fdcb89732c98600636042e1eff8c1b2ff5cb25d
这页面简单,我们只需要获取一个一个的链接,就在这些字的a标签下
尝试一下,就能用response.selector.xpath('//div[@id="searchresults"]/table/tr[@class="srrowns"]/td[@nowrap]/a/@href').get()
定位到了,但是get()只能匹配到一个,可以用getall()将页面上的全部拿到手
>>> response.selector.xpath('//div[@id="searchresults"]/table/tr[@class="srrowns"]/td[@nowrap]/a/@href').getall()
['/cve/CVE-2019-1020019/', '/cve/CVE-2019-1020018/', '/cve/CVE-2019-1020017/', '/cve/CVE-2019-1020016/', '/cve/CVE-2019-1020015/', '/cve/CVE-2019-1020014/', '/cve/CVE-2019-1020013/', '/cve/CVE-2019-1020012/', '/cve/CVE-2019-1020011/', '/cve/CVE-2019-1020010/', '/cve/CVE-2019-1020009/', '/cve/CVE-2019-1020008/', '/cve/CVE-2019-1020007/', '/cve/CVE-2019-1020006/', '/cve/CVE-2019-1020005/', '/cve/CVE-2019-1020004/', '/cve/CVE-2019-1020003/', '/cve/CVE-2019-1020002/', '/cve/CVE-2019-1020001/', '/cve/CVE-2019-1010319/', '/cve/CVE-2019-1010318/', '/cve/CVE-2019-1010317/', '/cve/CVE-2019-1010316/', '/cve/CVE-2019-1010315/', '/cve/CVE-2019-1010314/', '/cve/CVE-2019-1010312/', '/cve/CVE-2019-1010311/', '/cve/CVE-2019-1010310/', '/cve/CVE-2019-1010309/', '/cve/CVE-2019-1010308/', '/cve/CVE-2019-1010307/', '/cve/CVE-2019-1010306/', '/cve/CVE-2019-1010305/', '/cve/CVE-2019-1010304/', '/cve/CVE-2019-1010302/', '/cve/CVE-2019-1010301/', '/cve/CVE-2019-1010300/', '/cve/CVE-2019-1010299/', '/cve/CVE-2019-1010298/', '/cve/CVE-2019-1010297/', '/cve/CVE-2019-1010296/', '/cve/CVE-2019-1010295/', '/cve/CVE-2019-1010294/', '/cve/CVE-2019-1010293/', '/cve/CVE-2019-1010292/', '/cve/CVE-2019-1010290/', '/cve/CVE-2019-1010287/', '/cve/CVE-2019-1010283/', '/cve/CVE-2019-1010279/', '/cve/CVE-2019-1010275/']
随便点进一个知道下一次跳转是"https://www.cvedetails.com"+爬取的url
,所以parse1()也会写了
def parse1(self, response):
# xpath爬取url列表
detailurls = response.selector.xpath('//div[@id="searchresults"]/table/tr[@class="srrowns"]/td[@nowrap]/a/@href').getall()
for detailurl in detailurls:
# for循环构造每个子页面url
durl = "https://www.cvedetails.com" + detailurl
yield scrapy.Request(url=durl, callback=self.parse2, dont_filter=True)
这里又用yield回调了parse2(),没错我就是命名天才,略略略
老规矩,cmd
scrapy shell https://www.cvedetails.com/cve/CVE-1999-1567/
找到需要爬取的目标点,CVE编号,危害等级,漏洞类型,供应商,型号,设备类型,固件版本号
# CVE编号
cveid = response.selector.xpath('//h1/a/text()').get()
# 危害等级
score = response.selector.xpath('//div[@class="cvssbox"]/text()').get()
注意有的页面危害等级为0.0有可能是信息丢失也有可能是保密信息,反正页面没显示,写个判断直接跳过
if score == '0.0':
return None
然后是漏洞类型,这个有点麻烦,为空的时候很难定位,有字的时候直接锁定就行了,所以我获取了整个表格倒数第二个标签信息,再用re正则表达式匹配标签的信息,如果findall()匹配不到就会返回空列表,这我很喜欢,两句代码搞定
vulntype = re.findall(r'">(.*?)</span>', response.selector.xpath('//table[@id="cvssscorestable"]/tr').getall()[-2])
vulntype = '' if vulntype == [] else vulntype[0]
接下来的设备就比较麻烦了,因为很有可能一个漏洞对应很多个设备同时很多个版本,总之,直接把设备列表的每一行都获取到就行了
>>> response.selector.xpath('//table[@id="vulnprodstable"]/tr').getall()[1:]
['<tr>\n\t\t\t\t\t\t\t<td class="num">\n\t\t\t\t\t\t\t\t1\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\tApplication\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<a href="//www.cvedetails.com/vendor/216/Seapine-Software.html" title="Details for Seapine Software">Seapine Software</a>\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t<a href="//www.cvedetails.com/product/380/Seapine-Software-Testtrack.html?vendor_id=216" title="Product Details Seapine Software Testtrack">Testtrack</a>\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td>\n\t\t\t\t\t\t\t\t <a href="/version/1106/Seapine-Software-Testtrack-.html" title="Seapine Software Testtrack ">Version Details</a>&nbsp<a href="/vulnerability-list/vendor_id-216/product_id-380/version_id-1106/Seapine-Software-Testtrack-.html" title="Vulnerabilities of Seapine Software Testtrack ">Vulnerabilities</a>\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>']
很恶心,不过没关系,两个规则直接匹配
rule1 = re.compile(r'<a .*>(.*)</a>')
rule2 = re.compile(r'<td>\s+(.*?)\s+</td>')
vendor,product,_ = rule1.findall(make)
producttype,_,_,version,_,_,_,_ = rule2.findall(make)
emmmm,正则有问题的可以隔壁去看看正则表达式,反正我也是用一次查一次
最后实例化管道,然后每一行的设备都和cveid他们存入管道,通过管道存入数据库,下面是parse2()代码
def parse2(self, response):
# CVE编号,危害等级,漏洞类型,供应商,型号,设备类型,固件版本号
cveid = response.selector.xpath('//h1/a/text()').get()
score = response.selector.xpath('//div[@class="cvssbox"]/text()').get()
if score == '0.0':
return None
vulntype = re.findall(r'">(.*?)</span>', response.selector.xpath('//table[@id="cvssscorestable"]/tr').getall()[-2])
vulntype = '' if vulntype == [] else vulntype[0]
makes = response.selector.xpath('//table[@id="vulnprodstable"]/tr').getall()[1:]
rule1 = re.compile(r'<a .*>(.*)</a>')
rule2 = re.compile(r'<td>\s+(.*?)\s+</td>')
for make in makes:
vendor,product,_ = rule1.findall(make)
producttype,_,_,version,_,_,_,_ = rule2.findall(make)
item = CveDetailsItem()
item['cveid'],item['score'],item['vulntype'],item['vendor'],item['product'],i