漏洞库:爬取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数据库的代码
修改爬虫获取下一页的方式(也就是文章开头内容)
再修改了提取漏洞详情页的数据提取方法
等等......具体看源码中的注释吧
下面放一下最后录入到数据库的情况

# -*- coding:utf-8 -*- import sys #print (u'系统默认编码为',sys.getdefaultencoding()) default_encoding = 'utf-8' #重新设置编码方式为uft-8 if sys.getdefaultencoding() != default_encoding: reload(sys) sys.setdefaultencoding(default_encoding) #print (u'系统默认编码为',sys.getdefaultencoding()) import requests from bs4 import BeautifulSoup import traceback import re import xlwt def getURLDATA(url): #url = 'http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-201901-1014' header={ 'user-agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36', 'Connection': 'keep-alive',} r=requests.get(url,headers=header,timeout=30) #r.raise_for_status()抛出异常 html = BeautifulSoup(r.content.decode(),'html.parser') link=html.find(class_='detail_xq w770')#漏洞信息详情 link_introduce=html.find(class_='d_ldjj')#漏洞简介 link_others=html.find_all(class_='d_ldjj m_t_20')#其他 #print(len(link_introduce)) try: #print ("危害等级:"+link.contents[3].contents[3].find('a').text.lstrip().rstrip())#危害等级 list4.append(str(link.contents[3].contents[3].find('a').text.lstrip().rstrip())) except: #print("危害等级:is empty") list4.append("") try: #print ("CVE编号:"+link.contents[3].contents[5].find('a').text.lstrip().rstrip())#CVE编号 list5.append(str(link.contents[3].contents[5].find('a').text.lstrip().rstrip())) except: #print("CVE编号:is empty") list5.append("") try: #print ("漏洞类型:"+link.contents[3].contents[7].find('a').text.lstrip().rstrip())#漏洞类型 list6.append(str(link.contents[3].contents[7].find('a').text.lstrip().rstrip())) except : #print("漏洞类型:is empty") list6.append("") try: #print ("发布时间:"+link.contents[3].contents[9].find('a').text.lstrip().rstrip())#发布时间 list7.append(str(link.contents[3].contents[9].find('a').text.lstrip().rstrip())) except : #print("发布时间:is empty") list7.append("") try: #print ("威胁类型:"+link.contents[3].contents[11].find('a').text.lstrip().rstrip())#威胁类型 list8.append(str(link.contents[3].contents[11].find('a').text.lstrip().rstrip())) except : #print("威胁类型:is empty") list8.append("") try: #print ("更新时间:"+link.contents[3].contents[13].find('a').text.lstrip().rstrip())#更新时间 list9.append(str(link.contents[3].contents[13].find('a').text.lstrip().rstrip())) except : #print("更新时间:is empty") list9.append("") try: #print ("厂商:"+link.contents[3].contents[15].find('a').text.lstrip().rstrip())#厂商 list10.append(str(link.contents[3].contents[15].find('a').text.lstrip().rstrip())) except: #print("厂商:is empty") list10.append("") #link_introduce=html.find(class_='d_ldjj')#漏洞简介 try: link_introduce_data=BeautifulSoup(link_introduce.decode(),'html.parser').find_all(name='p') s="" for i in range(0,len(link_introduce_data)): ##print (link_introduce_data[i].text.lstrip().rstrip()) s=s+str(link_introduce_data[i].text.lstrip().rstrip()) #print(s) list11.append(s) except : list11.append("") if(len(link_others)!=0): #link_others=html.find_all(class_='d_ldjj m_t_20') #print(len(link_others)) try: #漏洞公告 link_others_data1=BeautifulSoup(link_others[0].decode(),'html.parser').find_all(name='p') s="" for i in range(0,len(link_others_data1)): ##print (link_others_data1[i].text.lstrip().rstrip()) s=s+str(link_others_data1[i].text.lstrip().rstrip()) #print(s) list12.append(s) except: list12.append("") try: #参考网址 link_others_data2=BeautifulSoup(link_others[1].decode(),'html.parser').find_all(name='p') s="" for i in range(0,len(link_others_data2)): ##print (link_others_data2[i].text.lstrip().rstrip()) s=s+str(link_others_data2[i].text.lstrip().rstrip()) #print(s) list13.append(s) except: list13.append("") try: #受影响实体 link_others_data3=BeautifulSoup(link_others[2].decode(),'html.parser').find_all('a',attrs={'class':'a_title2'}) s="" for i in range(0,len(link_others_data3)): ##print (link_others_data3[i].text.lstrip().rstrip()) s=s+str(link_others_data3[i].text.lstrip().rstrip()) #print(s) list14.append(s) except: list14.append("") try: #补丁 link_others_data3=BeautifulSoup(link_others[3].decode(),'html.parser').find_all('a',attrs={'class':'a_title2'}) s="" for i in range(0,len(link_others_data3)): ##print (link_others_data3[i].t
爬取CNVD漏洞信息,可以使用Java中的HttpClient和Jsoup来实现。具体步骤如下: 1. 使用HttpClient发送请求,获取CNVD漏洞列表页面的HTML源码。 ```java CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("http://www.cnvd.org.cn/flaw/list.htm"); CloseableHttpResponse response = httpClient.execute(httpGet); String html = EntityUtils.toString(response.getEntity(), "UTF-8"); ``` 2. 使用Jsoup解析HTML源码,获取漏洞列表。 ```java Document doc = Jsoup.parse(html); Elements flawList = doc.select(".flaw_list tr:gt(0)"); for (Element flaw : flawList) { // 获取漏洞信息,如漏洞名称、CNVD-ID、公开日期、危害级别等 String name = flaw.select(".flaw_tit a").text(); String cnvdId = flaw.select(".flaw_list_c td:eq(1)").text(); String publicDate = flaw.select(".flaw_list_c td:eq(2)").text(); String severity = flaw.select(".flaw_list_c td:eq(3)").text(); // TODO: 处理漏洞信息 } ``` 3. 可以将获取到的漏洞信息存储到数据库或者文件中。 完整代码示例: ```java import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.IOException; public class CnvdCrawler { public static void main(String[] args) throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("http://www.cnvd.org.cn/flaw/list.htm"); CloseableHttpResponse response = httpClient.execute(httpGet); String html = EntityUtils.toString(response.getEntity(), "UTF-8"); Document doc = Jsoup.parse(html); Elements flawList = doc.select(".flaw_list tr:gt(0)"); for (Element flaw : flawList) { String name = flaw.select(".flaw_tit a").text(); String cnvdId = flaw.select(".flaw_list_c td:eq(1)").text(); String publicDate = flaw.select(".flaw_list_c td:eq(2)").text(); String severity = flaw.select(".flaw_list_c td:eq(3)").text(); System.out.println(name + " " + cnvdId + " " + publicDate + " " + severity); } } } ```
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值