伪全栈式安全研发:CVE监控
evil77 企业安全 10682浏览 · 2017-12-01 02:32

Author: bipabo1l@京东安全

主题

在网络安全圈,攻防是核心,在攻防中漏洞的重要性不言而喻,而CVE是全世界通用漏洞的集合,对于安全人员来说及时知晓刚爆出的通用型漏洞对于企业来讲是十分必要的。本文讲解上周本人在完成CVE监控研发的过程中的一些技术探讨,为什么叫伪全栈呢,因为全栈远不止前端+后端。本例主要使用的技术为Golang、Vuejs、Mongodb、Beego等。

需求

具体需求为,实时爬取与【公司内部使用的开源框架/组件】相关的业内最新的CVE漏洞,进行网页展示以及报警。

数据库设计

由于Mongodb的灵活性与类Json形式的语法,将其作为我们的数据库。根据需求我们需要两个表,一个表存储CVE关键字和其重要程度;另一个表存储具体爬下来的每一个条CVE详情。
关键字表数据格式如下(以两条信息为例):

{
"_id" : ObjectId("599d2c1ca9218e4e8ec4e6xx"),
"keyword" : [ 
    {
        "wordname" : "spring",
        "wordcount" : 1
    }, 
    {
        "wordname" : "java",
        "wordcount" : 2
    }
}

其中wordcount为1表示高危,wordcount为2表示中危。
CVE详情表数据格式如下(以一条信息为例):

{
"_id" : ObjectId("59a69bf70988ac81605b76xx"),
"cve" : "CVE-2017-13758",
"keyword" : "ImageMagick",
"note" : "In ImageMagick 7.0.6-10, there is a heap-based buffer overflow in theTracePoint() function in MagickCore/draw.c.",
"time" : "2017-08-30 19:05:28",
"references" : "CONFIRM:https://www.imagemagick.org/discourse-server/viewtopic.php?f=3&t=32583",
"isignored" : true
}

需要注意的是isignored字段表示是否需要忽略,在安全人员查看信息时,如果认为当前条目CVE不存在严重影响,甚至可以忽略时可以将其置为忽略,而本字段记录其状态。

爬虫的研发

我们首先需要找到需要爬取的信息源,一方面需要一个接口能告知我们每日的更新,另一方面我们需要知晓每个CVE编号对应的漏洞详情。通过CVE官网http://cve.mitre.org/,我们很快找到了两个需要的接口:

https://cassandra.cerias.purdue.edu/CVE_changes/today.html

以及

http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-xxxx-xxxx

姑且称他们为接口1和接口2.爬虫的整体逻辑为向接口1发送请求,正则匹配出我们需要的【New entries】信息,如下图所示

然后分别爬取每个cve对应的接口2的url,继续正则匹配出我们想要的漏洞详情信息、相关文档信息等,我们需要的信息如下。

随后查keyword库判断是否为我们想要的漏洞,这里比较的是keyword与Description信息,如果是则存到库中。
回看整个过程,略微存在技术难点的地方在于正则表达式的编写与golang对Mongodb数据库的操作。
请求到的页面源码中,CVE编号的存在形式如下:

<A HREF = 'http://cve.mitre.org/cgi-bin/cvename.cgi?name=2017-3898'>2017-3898</A><br />

发送请求利用的是grequests库,爬取每日CVE更新信息代码如下:

func monitorCVEimpl() ([]string) {
op := grequests.RequestOptions{
    RequestTimeout:     10 * time.Second,
    InsecureSkipVerify: true,
    RedirectLimit:      5,
}
urlStr := "https://cassandra.cerias.purdue.edu/CVE_changes/today.html"
res, _ := grequests.Get(urlStr, &op)
newEntriesStr := Between(res.String(), "New entries:<br />", "Graduations (CAN to CVE)")
digitsRegexp := regexp.MustCompile("<A HREF = '(.*?)'>(.*?)</A>")
data := digitsRegexp.FindAllStringSubmatch(newEntriesStr, -1)
cveStrList := []string{}
for _, v := range data {
    //fmt.Println(v[2])
    if v[2] != "" {
        cveStrList = append(cveStrList, v[2])
    }
}
return cveStrList
}

其中Between函数为一个工具函数,获取一个字符串中在字符串2和字符串3中间的子字符串。

func Between(str, starting, ending string) string {
s := strings.Index(str, starting)
if s < 0 {
    return ""
}
s += len(starting)
e := strings.Index(str[s:], ending)
if e < 0 {
    return ""
}
return str[s: s+e]
}

最终返回类似[‘2017-10848’,’2017-10849’]的字符串切片。
随后构造为接口2模式的url,同样的方式进行请求,通过工具函数和regexp包,我们能够获取到页面存在的我们需要的信息。随后进行入库操作,Golang操作Mongodb用的是mgo包,基本的增删改查语法可以在https://studygolang.com/articles/1737查阅到,需要注意的是我们经常会遇到模糊查询的情况,遇到模糊查询时,可以用下面的解决办法:

err := mCVEdb.Find(bson.M{"cve": bson.M{"$regex": "CVE-2017", "$options": "$i"}}).Distinct("cve",&res)

查询数据库对象mCVEdb对应的数据库中,cve字段模糊匹配CVE-2017字样的文档,并且结果只返回cve集合。
根据bson.go官方包中的规定,options参数设置为i是不分大小写的匹配,正好符合我们的需求。

至此,我们成功的写完了爬虫脚本并且执行后能把数据存入我们的库中。

界面的展现

我们需要一个可视化平台,能够看到我们爬取到的CVE数据。搭建这个平台需要后端接口研发以及前端页面的展现。我们使用的golang http框架为beego,支持RESTful API和MVC模型。
后端接口的研发:
后端接口,主要是用来查询数据库中的数据并且以json格式返回。
在Controller文件中写入

// @router /cve/ [get]
func (c *CveController) GetAll() {
var mResultCve = new(models.CveResult)
result, err := mResultCve.Cve_search_today()
if err != nil {
    c.Data["json"] = utils.AjaxReturn(result, "get message success", 1)
}else {
    c.Data["json"] = utils.AjaxReturn("", "Error", -1)
}
c.ServeJSON()
}

models层中定义Cve_search_today()函数,将来我们可以在http://localhost:8080/cve接口获取到返回的json数据以供前端调用。
Cve_search_today()函数模糊查询time字段为当天的数据即可,将[]CVEinfo和err一同返回。
前端页面的研发:
首先我们需要一个页面url,在Controller中指定:

//@router /Cve [get]
func (this *CveController) GetPage() {
this.Data["title"] = "CVE信息 - "
//导航的ID
this.Data["navCode"] = "opinionNavCode"
this.TplName = "cveInfo/index.html"
}

随后编写cveInfo/index.html文件,利用layui+Vuejs进行开发
js主要代码

var vm = new Vue({
        delimiters: ['[[', ']]'],
        el: "#body_id",
        data: {
            url: "",
            cveList: [],
            cveNum: 0,
            lasttime: "",
            cveTasktime: "",
            date_range_list: ""
            //testList: ["test1", "test2", "test3", "test4"]
        },
        methods: {
            loadData: function (ev) {
                var _cveurl = "/cve"
                this.cveInfoUrl(_cveurl);
                this.cveTaskInfo();
            },
            initLoad: function () {
                var e = {"keyCode": 13};
                this.loadData(e);
            },
            cveInfoUrl: function (url) {
                var me = this;
                $.ajax({
                    async: true,
                    url: url,
                    type: 'get',
                    datatype: 'json',
                    success: function (data) {
                        me.buildData(data, me)
                    }
                });
            },
            cveTaskInfo: function () {
                var me = this;
                $.ajax({
                    async: true,
                    url: "/CveTaskTime",
                    type: 'get',
                    datatype: 'json',
                    success: function (data) {
                        me.lasttime = data.data;
                    }
                });
            },
            buildData: function (data, me) {
                $('#cve_id').html("<center style='margin-top:100px'><a  class='layui-btn layui-btn-disabled'>数据加载中...</a></center>");

                if (data.status == -1 || data.status == -5) {
                    $('#cve_id').html("<center style='margin-top:100px'><a  class='layui-btn layui-btn-disabled'>暂无cve信息</a></center>");
                    return false;
                } else {
                    $('#cve_id').html("");
                }
                var _tmpList = data.data.CveList;
                console.log(data.data.CveNum);
                this.cveList = []
                this.cveNum = data.data.CveNum;
                for (var d in _tmpList) {
                    if (_tmpList[d].Isignored == false) {
                        me.cveList.push({
                            "cve": _tmpList[d].Cve,
                            "keyword": _tmpList[d].Keyword,
                            "note": _tmpList[d].Note,
                            "time": _tmpList[d].Time,
                            "reference": _tmpList[d].References,
                            "isignored": _tmpList[d].Isignored
                        });
                    }
                }
                for (var d in _tmpList) {
                    if (_tmpList[d].Isignored == true) {
                        me.cveList.push({
                            "cve": _tmpList[d].Cve,
                            "keyword": _tmpList[d].Keyword,
                            "note": _tmpList[d].Note,
                            "time": _tmpList[d].Time,
                            "reference": _tmpList[d].References,
                            "isignored": _tmpList[d].Isignored
                        });
                    }
                }
                console.log(me.cveList);
            },

            ignore: function (message) {
                $.get('/cveIgnore/' + message, '', function (data, status) {
                    vm.buildData(data, vm)
                })
            },

            unignore: function (message) {
                $.get('/cveUnIgnore/' + message, '', function (data, status) {
                    vm.buildData(data, vm)
                })
            }
        }
    });

    //加载事件
    window.onload = function () {
        vm.initLoad();
    };

html部分主要利用v-for循环和v-if判断来读取ajax请求返回的内容,[[]]双括号可以将vuejs处理后的data数据返回到html页面。
页面效果如图:

因为让页面更加简洁优美所以未将CVE漏洞详情信息放入页面。

定时与实时

要能够定时地完成爬取,我们就需要利用beego的Task任务模块。将开始我们编写的爬虫脚本挂载到beego框架中,然后在脚本最后加入

func CveRun() {
cveSpiderRun := toolbox.NewTask("cve_spider_run", "0 0 7 * * *", CveSpider)
toolbox.AddTask("cve_spider_run", cveSpiderRun)
toolbox.StartTask()
defer toolbox.StopTask()
}

需要注意的是toolbox.AddTask第二个参数是脚本启动的入口函数,0 0 7 ***为定时任务的时间设定,再此为每日的7点,我们也可以自定义设置,比如每隔10分钟等等,语法与Linux中的Crontab类似。
随后在beego Controller层的DefaultController中加入

func init() {
//初始化CVE任务
cveTask.CveRun()
}

即可。
再此启动项目,访问8088端口(默认),在Task中可以管理任务。

邮件预警

在爬虫文件入库后加入邮件预警函数,内容也较为简单,调用github.com/go-gomail/gomail 库发送html邮件,主要代码为先gomail.NewMessage()创建新的Message对象,然后设置邮箱正文、头信息等,使用gomail.NewPlainDialer()配置本端邮箱的账号密码stmp信息,最后DialAndSend()发送即可,注意对异常的处理。邮件正文如下

总结与展望

本项目仍可提高的点我认为有如下:
1.利用Golang并发编程机制加快爬虫速度
2.Web界面与邮件界面的UI更加优雅
3.多维度漏洞爬虫
感谢@Dean、@Mr.Hao、@tanglion在研发过程中对我的帮助与启发。
如有错误还请帮指出,感谢阅读。

5 条评论
某人
表情
可输入 255