0x00 前言
漏洞作为攻击的重要组成部分,在威胁情报中占据着重要地位。安全研究员可以监控历史漏洞来对系统进行攻击或者及时加固。可漏洞数据过于庞大,若进行人工检测,会出现成本过高,数据不全等问题。所以一般来说,需要开发一系列漏洞工具来解决此问题。
本系列文章主要以Go语言作为示例,分享如何爬取监控alma、alpine、ubuntu、debian、nvd、cnnvd和kubernetes等漏洞数据。对于漏洞工具的研发而言,若将漏洞情报收集这一块相对而言比较独立且复用性较高的工作单独拎出来讨论,将会减少大家的工作量。
0x01 简介
AlmaLinux OS 是一个开源的、由社区拥有并治理且永久免费的企业 Linux 发行版,专注于长期稳定性,提供一个强大的生产级平台。AlmaLinux OS 与 RHEL® 和 CentOS Stream 是 1:1 的二进制兼容。漏洞情报如下所示:
本文结构如下所示:
0x02 数据结构
如图所示,数据结构erratum就是我们需要爬取的漏洞的数据结构,这里不展开说明。数据结构options是最底层的数据结构,在他的上面还封装了结构体Config和匿名函数option。options主要是用来配置需要爬取的urls、数据存放的目录dir和若请求不成功需要重试的次数retry。默认的值如下所示:
const (
almaLinuxDir = "alma" // 目录名
urlFormat = "https://errata.almalinux.org/%s/errata.json" // 爬取的URL格式
retry = 3 // 重试次数
)
var (
AlmaReleaseVersion = []string{"8", "9"} //alma的版本
)
结构体options的定义如下所示。这里的urls是一个键值都为string的map,键存放的是alma系统的版本(共有8和9两个版本),值存放的是url的地址。同种漏洞库根据操作系统的发行版可能会有多个url。
type options struct {
urls map[string]string
dir string
retry int
}
基于结构体options定义一个名为option的匿名函数。后续会利用该匿名函数依次给alma爬虫对象配置参数。然后定义WithURLs、WithDir和WithRetry三个函数,用来给options的各个数据成员赋值(为了增加程序的安全性,options的数据成员都为私有的)。三个函数都返回一个option的匿名函数。
type option func(*options)
func WithURLs(urls map[string]string) option {
return func(opts *options) { opts.urls = urls }
}
func WithDir(dir string) option {
return func(opts *options) { opts.dir = dir }
}
func WithRetry(retry int) option {
return func(opts *options) { opts.retry = retry }
}
基于结构体options还定义一个结构体Config。其构造函数如下所示:
type Config struct {
*options
}
func NewConfig(opts ...option) Config {
//根据alma的版本构造各自的爬虫url
urls := map[string]string{}
for _, version := range AlmaReleaseVersion {
urls[version] = fmt.Sprintf(urlFormat, version)
}
//设置默认的config
o := &options{
urls: urls,
dir: utils.VulnListDir(),
retry: retry,
}
//再根据传递进来的参数,给o赋值
for _, opt := range opts {
opt(o)
}
//返回最终的结果
return Config{
options: o,
}
}
综上所述,如果我们需要爬取alma的漏洞数据,就需要声明一个Config对象,然后调用Config的方法Update和update。代码示例如下所示:
ac := alma.NewConfig()
if err := ac.Update(); err != nil {
return xerrors.Errorf("AlmaLinux update error: %w", err)
}
commitMsg = "AlmaLinux Security Advisory"
ac := alma.NewConfig(alma.WithURLs(map[string]string{tt.version: ts.URL}), alma.WithDir(t.TempDir()), alma.WithRetry(0))
if err := ac.Update(); tt.expectedError != nil {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError.Error())
return
}
0x03 关键方法
alma爬虫对象的关键方法和主要调用的方法是Update方法。在该方法中,主要根据config中的url调用update方法来爬取漏洞数据。
func (c Config) Update() error {
for version, url := range c.urls {
log.Printf("Fetching security advisories of AlmaLinux %s ...\n", version)
if err := c.update(version, url); err != nil {
return xerrors.Errorf("failed to update security advisories of AlmaLinux %s: %w", version, err)
}
}
return nil
}
所以,主要的逻辑还是在update方法中,该方法只能被Update方法调用。在update方法中对数据进行了以下预处理:
- 根据不同的url调用工具包中的
FetchURL
函数爬取漏洞数据,然后对数据进行一次清洗,即只保留UpdateinfoID
前缀为"ALSA-"的漏洞数据。 - 将漏洞数据分类存放到文件中,并以
UpdateinfoID
为文件名。- 第一次分类是根据漏洞库的名称。在这里,以操作系统的名称来命名漏洞库名称,即为alma。
- 第二次分类是根据操作系统的发型版本。在这里,alma有8和9两个不同分发行版本。
- 第三次分类是根据漏洞签发的年份。
func (c Config) update(version, url string) error {
//构建数据存放的目录,其中c.dir为vuln-list目录,在vuln-list上海有alma目录,然后根据发行版本的不同,指定不同的目录。
//详情看:utils.VulnListDir()
dirPath := filepath.Join(c.dir, almaLinuxDir, version)
// 重新创建一个目录(若目录存在,则情况)
log.Printf("Remove AlmaLinux %s directory %s\n", version, dirPath)
if err := os.RemoveAll(dirPath); err != nil {
return xerrors.Errorf("failed to remove AlmaLinux %s directory: %w", version, err)
}
if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {
return xerrors.Errorf("failed to mkdir: %w", err)
}
//根据url来爬取漏洞数据
body, err := utils.FetchURL(url, "", c.retry)
if err != nil {
return xerrors.Errorf("failed to fetch security advisories from AlmaLinux: %w", err)
}
//将数据反序列化
var errata []erratum
if err := json.Unmarshal(body, &errata); err != nil {
return xerrors.Errorf("failed to unmarshal json: %w", err)
}
// 定义一个名为secErrata的map,其key的数据类型为string,主要为漏洞数据签发的年份。value为数据类型为erratum的切片,主要是存放漏洞数据
secErrata := map[string][]erratum{}
for _, erratum := range errata {
// 只保存UpdateinfoID有ALSA-前缀的数据
if !strings.HasPrefix(erratum.UpdateinfoID, "ALSA-") {
continue
}
// 将漏洞数据按照签发年份进行分类
y := strconv.Itoa(time.UnixMilli(erratum.IssuedDate.Date).Year())
secErrata[y] = append(secErrata[y], erratum)
}
// 根据漏洞数据签发的年份再在dirPath之上创建目录
for year, errata := range secErrata {
log.Printf("Write Errata for AlmaLinux %s %s\n", version, year)
if err := os.MkdirAll(filepath.Join(dirPath, year), os.ModePerm); err != nil {
return xerrors.Errorf("failed to mkdir: %w", err)
}
// 开启进度条
bar := pb.StartNew(len(errata))
// 以UpdateinfoID为文件名,将漏洞数据写入文件中
for _, erratum := range errata {
filepath := filepath.Join(dirPath, year, fmt.Sprintf("%s.json", erratum.UpdateinfoID))
if err := utils.Write(filepath, erratum); err != nil {
return xerrors.Errorf("failed to write AlmaLinux CVE details: %w", err)
}
bar.Increment()
}
bar.Finish()
}
return nil
}
0x04 工具包总结
若需要对其他的漏洞情报进行收集,总结一些公用的方法是必不可少的。我们可以将这些方法统称为工具,放在utils文件夹中。
4.1 数据存放路径
程序会根据操作系统来选择用户的缓存目录作为程序的缓存目录,再在其之上创建一个名为vuln-list-update的目录作为程序的缓存目录。若无法根据操作系统来获取用户的缓冲目录,则创建一个临时文件夹,再执行上述操作。最后在程序的缓存目录上再指定一个数据存放目录vuln-list。
// CacheDir 设置缓存目录(vuln-list-update的目录)
func CacheDir() string {
//更具用户的操作系统获取缓存目录,若无法获取缓存目录,则获取临时目录
cacheDir, err := os.UserCacheDir()
if err != nil {
cacheDir = os.TempDir()
}
dir := filepath.Join(cacheDir, "vuln-list-update")
return dir
}
// VulnListDir 获取vuln-list目录
func VulnListDir() string {
return filepath.Join(CacheDir(), "vuln-list")
}
4.2 爬取URL
// FetchURL returns HTTP response body with retry
// 拉取URL(可设置重试次数),并返回响应体
func FetchURL(url, apikey string, retry int) (res []byte, err error) {
for i := 0; i <= retry; i++ {
// 首次访问为立即访问,若访问失败,则计算出一个随机值作为间隔,再次访问
if i > 0 {
// wait = i^2+[0-9]的一个随机数
// 其中RandInt为[0,MaxInt64)的整数
wait := math.Pow(float64(i), 2) + float64(RandInt()%10)
log.Printf("retry after %f seconds\n", wait)
time.Sleep(time.Duration(time.Duration(wait) * time.Second))
}
res, err = fetchURL(url, map[string]string{"api-key": apikey})
if err == nil {
return res, nil
}
}
return nil, xerrors.Errorf("failed to fetch URL: %w", err)
}
// RandInt 返回一个[0,MaxInt64)的一个随机整数
func RandInt() int {
seed, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
return int(seed.Int64())
}
func fetchURL(url string, headers map[string]string) ([]byte, error) {
req := gorequest.New().Get(url)
for key, value := range headers {
req.Header.Add(key, value)
}
resp, body, errs := req.Type("text").EndBytes()
if len(errs) > 0 {
return nil, xerrors.Errorf("HTTP error. url: %s, err: %w", url, errs[0])
}
if resp.StatusCode != 200 {
return nil, xerrors.Errorf("HTTP error. status code: %d, url: %s", resp.StatusCode, url)
}
return body, nil
}
4.3 写入文件
// Write 写入漏洞数据
func Write(filePath string, data interface{}) error {
// 返回filePath的路径
dir := filepath.Dir(filePath)
// 创建路径
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return xerrors.Errorf("failed to create %s: %w", dir, err)
}
// 创建文件
f, err := os.Create(filePath)
if err != nil {
return xerrors.Errorf("file create error: %w", err)
}
defer f.Close()
// 将data 序列化
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return xerrors.Errorf("JSON marshal error: %w", err)
}
// 写入文件
_, err = f.Write(b)
if err != nil {
return xerrors.Errorf("file write error: %w", err)
}
return nil
}
0x05 使用到的第三方库
Go 的终端进度条pb可以帮助我们丰富Golang编写的终端工具
https://github.com/cheggaaa/pb
0x06 运行结果
程序运行结束后,终端上会有类似的打印:
此时我们收集到的数据如下所示:
以AlmaLinux 9最新的漏洞ALSA-2023:0383 为例,内容如下:
{
"_id": {},
"bs_repo_id": {},
"updateinfo_id": "ALSA-2023:0383",
"description": "X.Org X11 libXpm runtime library.\n\nSecurity Fix(es):\n\n* libXpm: compression commands depend on $PATH (CVE-2022-4883)\n* libXpm: Runaway loop on width of 0 and enormous height (CVE-2022-44617)\n* libXpm: Infinite loop on unclosed comments (CVE-2022-46285)\n\nFor more details about the security issue(s), including the impact, a CVSS score, acknowledgments, and other related information, refer to the CVE page(s) listed in the References section.",
"fromstr": "packager@almalinux.org",
"issued_date": {
"$date": 1674432000000
},
"pkglist": {
"name": "almalinux-9-for-x86_64-appstream-rpms__9_1_default",
"shortname": "almalinux-9-for-x86_64-appstream-rpms__9_1_default",
"packages": [
{
"name": "libXpm",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "i686",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-3.5.13-8.el9_1.i686.rpm",
"sum": "a480fde4a4e7588afd28669f37c5d99d6fce30ca4b2242339d9f3291e10b2007",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm-devel",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "i686",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-devel-3.5.13-8.el9_1.i686.rpm",
"sum": "fb0049df50b019cd939de8765800b76f9fa151263f27e32a1be97e5a27bc2401",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "ppc64le",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-3.5.13-8.el9_1.ppc64le.rpm",
"sum": "420f3c80dc8fd2ee8eebe1fd89c3d9a2375d92456fb55349674f2ea694690752",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm-devel",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "ppc64le",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-devel-3.5.13-8.el9_1.ppc64le.rpm",
"sum": "7c6243d1a4556d1daf028449f8a90d74b22c726a1cf951eeb8153d0ee77329b4",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm-devel",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "aarch64",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-devel-3.5.13-8.el9_1.aarch64.rpm",
"sum": "30b4c77de55f0362a65b80cdecefe1e3a441c40b70339280893bf6816e399e47",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "aarch64",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-3.5.13-8.el9_1.aarch64.rpm",
"sum": "b61a699c5c664d0262b279e12a2c32690fdc8428856480b18b91611bb6611c55",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm-devel",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "s390x",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-devel-3.5.13-8.el9_1.s390x.rpm",
"sum": "0cca6f2f73e7ec44b03f451dd95fab137fad7a494aaecb57b19a423fcb28f797",
"sum_type": 5,
"reboot_suggested": 0
},
{
"name": "libXpm",
"version": "3.5.13",
"release": "8.el9_1",
"epoch": "0",
"arch": "s390x",
"src": "libXpm-3.5.13-8.el9_1.src.rpm",
"filename": "libXpm-3.5.13-8.el9_1.s390x.rpm",
"sum": "16796c5d0b7d3e6586db4730349bd9c3f34ba872cfeebe0450628a1f08517a51",
"sum_type": 5,
"reboot_suggested": 0
}
],
"module": {}
},
"pushcount": "1",
"references": [
{
"href": "https://access.redhat.com/errata/RHSA-2023:0383",
"type": "rhsa",
"id": "RHSA-2023:0383",
"title": "RHSA-2023:0383"
},
{
"href": "https://access.redhat.com/security/cve/CVE-2022-44617",
"type": "cve",
"id": "CVE-2022-44617",
"title": "CVE-2022-44617"
},
{
"href": "https://access.redhat.com/security/cve/CVE-2022-46285",
"type": "cve",
"id": "CVE-2022-46285",
"title": "CVE-2022-46285"
},
{
"href": "https://access.redhat.com/security/cve/CVE-2022-4883",
"type": "cve",
"id": "CVE-2022-4883",
"title": "CVE-2022-4883"
},
{
"href": "https://bugzilla.redhat.com/2160092",
"type": "bugzilla",
"id": "2160092",
"title": ""
},
{
"href": "https://bugzilla.redhat.com/2160193",
"type": "bugzilla",
"id": "2160193",
"title": ""
},
{
"href": "https://bugzilla.redhat.com/2160213",
"type": "bugzilla",
"id": "2160213",
"title": ""
},
{
"href": "https://errata.almalinux.org/9/ALSA-2023-0383.html",
"type": "self",
"id": "ALSA-2023:0383",
"title": "ALSA-2023:0383"
}
],
"release": "0",
"rights": "Copyright 2023 AlmaLinux OS",
"severity": "Important",
"solution": "For details on how to apply this update, which includes the changes described in this advisory, refer to:\n\nhttps://access.redhat.com/articles/11258",
"status": "final",
"summary": "libXpm security update",
"title": "Important: libXpm security update",
"type": "security",
"updated_date": {
"$date": 1674572086000
},
"version": "1"
}
0x07 总结
本文主要是从数据源、数据结构、关键方法、工具包总结、使用到的第三方库和运行结果六个方面讨论了AlmaLinux操作系统漏洞情报收集的方法。若有不足的地方,欢迎讨论纠正。后续会讨论如何对alpine操作系统漏洞情报进行收集,本文已经详细叙述的部分将不会赘述。