漏洞库:爬取CNVD-国家信息安全漏洞共享平台漏洞库

开源项目 漏洞库 爬虫 开源项目

近期工作中需要爬取整个CNVD的漏洞库,之前写的爬虫是跑ICS.CNVD的库(见之前文章 工控安全:分享自己的工控爬虫项目—PySpider-ICS ),本以为改改就能用,没想到,CNVD主站有几道反爬虫机制,这里记录一下我的解决之法。

初步分析

首先,还是使用了我钟爱的爬虫框架——pyspider,但是写完之后,试了几次都只能获取十几个漏洞信息,然后就报错无法继续下去。看来CNVD是有反爬虫机制的,这种情况以后会越来越常见,那么接下来就分析一下反爬虫机制。
这是CNVD的漏洞查询页:https://www.cnvd.org.cn/flaw/list.htm

先看一下跳转下一页的数据包

POST /flaw/list.htm?flag=true HTTP/1.1
Host: www.cnvd.org.cn
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: https://www.cnvd.org.cn/flaw/list.htm
Content-Type: application/x-www-form-urlencoded
Content-Length: 121
Connection: close
Cookie: xxxx
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache

number=%E8%AF%B7%E8%BE%93%E5%85%A5%E7%B2%BE%E7%A1%AE%E7%BC%96%E5%8F%B7&startDate=&endDate=&field=&order=&max=20&offset=20

根据数据包可得知,CNVD进行下一页查询的操作是通过POST提交数据包的,而且其中数据包关键字段max=20&offset=20 offset表示查询漏洞数的偏移量,max代表显示当前页面的最大条数(经过测试,max设置100页面能显示100条,设置再大的值也是100条),其他字段爬虫用不到,直接删除不影响。

自此,爬虫获取下一页的问题可以解决了,至于爬取页面漏洞URL,并进入漏洞详情页爬取具体数据的操作,无非是数据提取与格式化,也无需过多讨论(基操勿6皆坐)。

深入分析

接下来是解决CNVD对cookie字段的反爬虫限制。

经过测试,猜测:CNVD对相同的cookie值,只允许获取十几个漏洞详情页面,所以要解决cookie动态更新的问题。
这里我使用kali作为运行基础环境(爬虫在linux系统下稳定点,尤其是有中文的时候)
接下来就是要通过chromedirver来自动化访问CNVD页面来更新cookie,
下载地址:   https://npm.taobao.org/mirrors/chromedriver

使用chromedriver要保证有chrome浏览器,且保证下载的版本号基本保持一致

chromedriver安装方法:
将下载的文件移动到系统目录去:sudo mv chromedriver /usr/local/bin/chromedriver
改变用户执行的权限:sudo chmod u+x,o+x /usr/local/bin/chromedriver
检验是否正常使用:chromedriver --version

源码解释

下面先贴上代码

# -*- coding: utf-8 -*-
import requests
from lxml import etree
import csv
import time
import random
from collections import OrderedDict
import codecs
from datetime import date
from multiprocessing.dummy import Pool as Threadpool
# from sqlalchemy.ext.declarative import declarative_base
# from sqlalchemy import Column, Integer, String, ForeignKey, TEXT, Index, DATE
# from sqlalchemy.orm import sessionmaker, relationship
# from sqlalchemy import create_engine
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import ast

#Base = declarative_base()


# class Cnvdtable(Base):
#     __tablename__ = 'cn_table'

#     id = Column(Integer, primary_key=True)
#     cn_url = Column(String(64))
#     cn_title = Column(TEXT)
#     cnvd_id = Column(String(32))
#     pub_date = Column(DATE)
#     hazard_level = Column(String(32))
#     cn_impact = Column(TEXT)
#     cve_id = Column(String(32))
#     cn_describe = Column(TEXT)
#     cn_types = Column(String(64))
#     cn_reference = Column(String(512))
#     cn_solution = Column(String(512))
#     cn_patch = Column(TEXT)

#     __table_args__ = (
#         Index('cn_url', 'cnvd_id'),
#     )


# engine = create_engine(
#     "mysql+pymysql://root:[email protected]/scrapy?charset=utf8", max_overflow=5)

# Base.metadata.create_all(engine)
# Session = sessionmaker(bind=engine)
# session = Session()


class Cnvdspider(object):
    def __init__(self):
        self.headers = {
            "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.16 Safari/537.36"}
        # 如果从某处断线了,可以更改起始的url地址
        #self.start_url = "http://www.cnvd.org.cn/flaw/list.htm"
        self.count = 0
        self.cookies = self.get_cookies()

    def get_cookies(self):
        chrome_options = Options()
        # 加上下面两行,解决报错
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        driver = webdriver.Chrome(chrome_options=chrome_options)
        driver.get("https://www.cnvd.org.cn/flaw/list.htm?max=20&offset=20")#设置每次ChromeDriver访问的初始页面用来更新cookie,以绕过cnvd爬虫限制
        cj = driver.get_cookies()
        cookie = ''
        for c in cj:
            cookie += "'"+c['name'] + "':'" + c['value'] + "',"
        cookie = ast.literal_eval('{'+cookie+'}')
        driver.quit()
        return cookie

    def parse(self, i):
        time.sleep(random.randint(2, 5))
        self.count += 1
        print(self.count)
        if(self.count == 5):
            self.cookies = self.get_cookies()
            self.count = 0
        url='https://www.cnvd.org.cn/flaw/list.htm?%s' % str(i)
        html = requests.post(url, data={'max': 100, 'offset': str(i)}, headers=self.headers,
                            cookies=self.cookies).content.decode()#当前的CNVD设置了只能POST提交数据,这里把max设置成100,这也是cnvd能在一个页面显示的最大漏洞信息条数,默认是20条,这样能减少查询页数。
        html = etree.HTML(html)
        return html

    def parse2(self, url):
        time.sleep(random.randint(2, 5))
        self.count += 1
        print(self.count)
        if(self.count == 5):
            self.cookies = self.get_cookies()
            self.count = 0
        html = requests.get(url, headers=self.headers,
                            cookies=self.cookies).content.decode()
        html = etree.HTML(html)
        return html

    def get_list_url(self, html):
        list_url = html.xpath("//div[@id='flawList']/tbody/tr/td[1]/a/@href")
        if list_url is None:
            list_url = html.xpath(
                "//div[@class='blkContainerPblk']//table[@class='tlist']/tbody/tr/td[1]/a/@href")
        for url in list_url:
            url = "http://www.cnvd.org.cn" + url
            self.parse_detail(url)
        # next_url = html.xpath(
        #     "//a[@class='nextLink']/@href")[0] if html.xpath("//a[@class='nextLink']/@href") else None
        # if next_url:
        #     next_url = "http://www.cnvd.org.cn" + next_url
        # return next_url

    def parse_detail(self, url):
        time.sleep(random.randint(2, 5))
        html = self.parse2(url)
        # item = OrderedDict()  # 如果要存入csv文档,建议用有序字典
        item = {}
        # URL
        item["cn_url"] = url
        # 获取漏洞标题
        item["cn_title"] = html.xpath(
            "//div[@class='blkContainerPblk']/div[@class='blkContainerSblk']/h1/text()")
        if item["cn_title"]:
            item["cn_title"] = html.xpath("//div[@class='blkContainerPblk']/div[@class='blkContainerSblk']/h1/text()")[
                0].strip()
        else:
            item["cn_title"] = 'Null'

        # 获取漏洞公开日期
        # item["date"] = html.xpath("//td[text()='公开日期']/following-sibling::td[1]/text()")
        item["pub_date"] = html.xpath(
            "//div[@class='tableDiv']/table[@class='gg_detail']//tr[2]/td[2]/text()")
        if item["pub_date"]:
            item["pub_date"] = "".join(
                [i.strip() for i in item["pub_date"]])
        #    item["pub_date"] = self.convertstringtodate(item["pub_date"])
        else:
            item["pub_date"] = '2000-01-01'
        #    item["pub_date"] = self.convertstringtodate(item["pub_date"])

        # 获取漏洞危害级别
        item["hazard_level"] = html.xpath(
            "//td[text()='危害级别']/following-sibling::td[1]/text()")
        if item["hazard_level"]:
            item["hazard_level"] = "".join(
                [i.replace("(", "").replace(")", "").strip() for i in item["hazard_level"]])
        else:
            item["hazard_level"] = 'Null'

        # 获取漏洞影响的产品
        item["cn_impact"] = html.xpath(
            "//td[text()='影响产品']/following-sibling::td[1]/text()")
        if item["cn_impact"]:
            item["cn_impact"] = "   ;   ".join(
                [i.strip() for i in item["cn_impact"]])
        else:
            item["cn_impact"] = 'Null'

        # 获取cnvd id
        item["cnvd_id"] = html.xpath(
            "//td[text()='CNVD-ID']/following-sibling::td[1]/text()")
        if item["cnvd_id"]:
            item["cnvd_id"] = "".join(
                [i.strip() for i in item["cnvd_id"]])
        else:
            item["cnvd_id"] = 'Null'

        # 获取cve id
        item["cve_id"] = html.xpath(
            "//td[text()='CVE ID']/following-sibling::td[1]//text()")
        if item["cve_id"]:
            item["cve_id"] = "".join(
                [i.strip() for i in item["cve_id"]])
        else:
            item["cve_id"] = 'Null'

        # 获取漏洞类型
        item["cn_types"] = html.xpath(
            "//td[text()='漏洞类型']/following-sibling::td[1]//text()")
        if item["cn_types"]:
            item["cn_types"] = "".join(
                [i.strip() for i in item["cn_types"]])
        else:
            item["cn_types"] = 'Null'

        # 获取漏洞描述
        item["cn_describe"] = html.xpath(
            "//td[text()='漏洞描述']/following-sibling::td[1]//text()")
        if item["cn_describe"]:
            item["cn_describe"] = "".join(
                [i.strip() for i in item["cn_describe"]]).replace("\u200b", "")
        else:
            item["cn_describe"] = 'Null'

        # 获取漏洞的参考链接
        item["cn_reference"] = html.xpath(
            "//td[text()='参考链接']/following-sibling::td[1]/a/@href")
        if item["cn_reference"]:
            item["cn_reference"] = item["cn_reference"][0].replace('\r', '')
        else:
            item["cn_reference"] = 'Null'

        # 获取漏洞的解决方案
        item["cn_solution"] = html.xpath(
            "//td[text()='漏洞解决方案']/following-sibling::td[1]//text()")
        if item["cn_solution"]:
            item["cn_solution"] = "".join(
                [i.strip() for i in item["cn_solution"]])
        else:
            item["cn_solution"] = 'Null'

        # 获取漏洞厂商补丁
        item["cn_patch"] = html.xpath(
            "//td[text()='厂商补丁']/following-sibling::td[1]/a")
        if item["cn_patch"]:
            for i in item["cn_patch"]:
                list = []
                try:
                    list.append(i.xpath("./text()")[0])
                    list.append("http://www.cnvd.org.cn" + i.xpath("./@href")[0])
                    item["cn_patch"] = list[0] + ':' + list[1]
                except IndexError:
                    pass            
        else:
            item["cn_patch"] = 'Null'

        print(item)
        # 保存数据到csv
        self.save_data(item)

    def convertstringtodate(self, stringtime):
        "把字符串类型转换为date类型"
        #  把数据里的时间格式替换成数据库需要的格式。日期格式,便于后期提取数据,
        if stringtime[0:2] == "20":
            year = stringtime[0:4]
            month = stringtime[4:6]
            day = stringtime[6:8]
            if day == "":
                day = "01"
            begintime = date(int(year), int(month), int(day))
            return begintime
        else:
            year = "20" + stringtime[0:2]
            month = stringtime[2:4]
            day = stringtime[4:6]

            begintime = date(int(year), int(month), int(day))
            return begintime

    def save_data(self, item):
        # 数据保存进csv,此处可以打开,存txt类似
        with open("./cnvd-1290ye.csv", "a") as f:
            writer = csv.writer(f, codecs.BOM_UTF8)
            c = []
            for i in item.values():
                c.append(i)
            writer.writerow(c)

        # dic = dict(item)
        # obc = Cnvdtable(
        #     cn_url=dic['cn_url'],
        #     cn_title=dic['cn_title'],
        #     cnvd_id=dic['cnvd_id'],
        #     cve_id=dic['cve_id'],
        #     pub_date=dic['pub_date'],
        #     cn_types=dic['cn_types'],
        #     hazard_level=dic['hazard_level'],
        #     cn_impact=dic['cn_impact'],
        #     cn_describe=dic['cn_describe'],
        #     cn_reference=dic['cn_reference'],
        #     cn_solution=dic['cn_solution'],
        #     cn_patch=dic['cn_patch'],
        # )
        # session.add(obc)
        # session.commit()
        #print("\n"+dic['cn_title']+" ==============存储成功\n")

    def run(self):

        #每次 i 递增100位
        for i in range(128900,129900,100): #如果因为一些原因导致脚本报错中断,重新运行时需要重新设置范围,根据post页面信息来定位报错的页数
            html = self.parse(i)
            print(i) #页数
            next_url = self.get_list_url(html)
            print(next_url)



if __name__ == "__main__":
    a = Cnvdspider()
    pool = Threadpool(1)  # 单线程跑的慢,但是基本很稳定没有被cnvd封杀。本次跑完整个cnvd库大概十天左右,期间网络波动中断需要维护代码,及爬虫重新爬的页面范围。
    a.run()
    # pool.map(a.run(),self.parse())
    pool.close()
    pool.join()

上述代码,是根据在GitHub上搜索出的几个cnvd爬虫,再根据实际运行情况进行修改出的:
注释了录入mysql数据库的代码
修改爬虫获取下一页的方式(也就是文章开头内容)
再修改了提取漏洞详情页的数据提取方法
等等......具体看源码中的注释吧
下面放一下最后录入到数据库的情况

新评论

称呼不能为空
邮箱格式不合法
网站格式不合法
内容不能为空