ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VulDB漏洞数据爬虫实战:绕过JS渲染与Cloudflare的Python方案

VulDB漏洞数据爬虫实战:绕过JS渲染与Cloudflare的Python方案 简介本资源是一款面向网络安全研究人员与Python开发者的漏洞数据采集工具聚焦NVD、CNVD、CNNVD等权威漏洞数据库的自动化爬取与结构化存储助力漏洞分析、威胁情报构建及安全态势研究。压缩包共267个文件主体为250个XML格式的原始漏洞数据文件含CVE/CNNVD/CNVD编号、CVSS评分、影响组件等字段辅以5个核心Python爬虫脚本支持CNVD独立版与PySpider适配版、NVD主爬取逻辑、3张可视化图表PNG、2个预处理完成的7z漏洞数据集如VulDB_Spider_NVD-20200423.7z以及xlsx/xls统计表、git配置文件等整体体积达191.8MB结构清晰、开箱即用。目前已有105人学习下载读者可直接复用完整爬虫框架、获取近十年高质量漏洞原始数据集、参考多源异构数据清洗与归一化实践并基于现有XML结构快速拓展至其他漏洞平台。1. 为什么你查不到 VulDB 的最新漏洞数据——一个 Python 爬虫能解决的真实缺口VulDB 是全球少数持续更新、带 CVSS 评分、含厂商响应状态的第三方漏洞数据库但它不提供公开 API也不开放 RSS 或 JSON 接口。安全团队做资产风险评估、红队情报收集、自动化漏洞匹配时常卡在「手动翻页→复制 CVE→粘贴进 Excel」这一步。有人用浏览器插件导出表格但漏字段、缺时间戳、无法回溯历史变更有人试过 Selenium 模拟点击结果被 Cloudflare 验证墙拦在第 3 页还有人直接调requests.get()却发现返回 HTML 里全是空 div——因为 VulDB 前端用 React 渲染关键漏洞列表由 JS 动态注入。这个标题里的「基于 Python 的 VulDB_Spider 漏洞数据库爬虫」不是玩具项目而是为解决上述真实断点设计的轻量级数据管道它绕过前端渲染陷阱直取 VulDB 后端接口非公开但可逆向支持按 CVE 编号、厂商名、CVSS 分数区间、发布时间范围精准拉取输出结构化 JSON/CSV并内置去重、断点续爬、HTTP 状态码分级重试机制。适合渗透测试工程师、SOC 分析师、漏洞管理平台开发者——只要你需要把 VulDB 变成可编程的数据源而不是浏览器里的一页页网页。2. 不碰前端 DOM从 Network 面板逆向 VulDB 的真实数据接口VulDB 的页面看似是静态 HTML实则所有漏洞卡片都由一个隐藏的/api/v1/search接口驱动。这个接口不写在文档里但通过 Chrome DevTools 的 Network 面板抓包可稳定复现。关键在于识别它的请求模式、参数签名和反爬水印。下面分三步拆解。2.1 抓包定位核心接口与请求特征打开 VulDB 主页如https://vuldb.com/在搜索框输入apache并回车。切换到 Network 面板过滤 XHR 请求找到一条search?开头的请求。观察其 Request URLhttps://vuldb.com/?searchapachelimit20offset0sortdateorderdesc注意两点它不是/api/v1/search而是根路径/?search说明 VulDB 用的是服务端渲染 查询参数透传而非纯 AJAXlimit和offset控制分页sort和order控制排序没有 token 或 timestamp 参数说明无强签名验证。提示不要用https://vuldb.com/api/...这类路径——VulDB 官方 API需申请 Key只对商业客户开放本爬虫走的是面向公众的 Web 接口合法合规用于个人安全研究。2.2 构建最小可行请求绕过 User-Agent 封禁VulDB 对非常规 UA 会返回 403。实测发现仅设置User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36仍可能被拒必须补全Accept和Accept-Language头。以下是最小有效请求模板import requests headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8,application/signed-exchange;vb3;q0.7, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, } params { search: apache, limit: 20, offset: 0, sort: date, order: desc } response requests.get(https://vuldb.com/, paramsparams, headersheaders, timeout10) print(response.status_code) # 应为 200 print(len(response.text)) # 应 10000含完整 HTML这段代码的关键不在requests.get本身而在于header 的完整性Accept-Encoding: gzip让服务器返回压缩内容大幅降低传输体积Connection: keep-alive复用 TCP 连接避免频繁建连触发风控。若省略任一 header大概率返回 403 或空 HTML。2.3 解析 HTML 中的结构化数据避开 JavaScript 渲染陷阱VulDB 页面虽由 JS 渲染但其 HTML 源码中已嵌入全部漏洞数据——藏在script标签的window.__INITIAL_STATE__变量里。这是 SSR服务端渲染的典型特征React 组件在服务端生成初始 HTML再由客户端 JS 接管交互。我们只需提取该变量的 JSON 字符串即可无需 Selenium。import re import json # 从 response.text 中提取 window.__INITIAL_STATE__ match re.search(rwindow\.__INITIAL_STATE__\s*\s*({.*?});, response.text, re.DOTALL) if match: try: state_data json.loads(match.group(1)) # state_data[search][results] 即漏洞列表 vulnerabilities state_data.get(search, {}).get(results, []) print(f成功提取 {len(vulnerabilities)} 条漏洞记录) except json.JSONDecodeError as e: print(JSON 解析失败检查正则是否匹配到完整对象) else: print(未找到 __INITIAL_STATE__ 变量请确认页面结构是否变更)这段正则rwindow\.__INITIAL_STATE__\s*\s*({.*?});的设计要点\s*匹配任意空白符换行、空格、制表符适应不同压缩格式({.*?})使用非贪婪匹配防止跨多个}错误截断re.DOTALL让.匹配换行符确保多行 JSON 被捕获。这是本爬虫最稳的解析方式——比 BeautifulSoup 解析div classvuln-item可靠 10 倍因为后者极易因前端改版 class 名而失效而__INITIAL_STATE__是 SSR 的核心变量VulDB 不会轻易删改。3. 从单页到全量分页控制、去重与断点续爬的工程化实现单页抓取只是起点。真实需求是按关键词批量拉取 5000 漏洞、每天增量同步新条目、中断后能从上次 offset 继续。这要求爬虫具备状态管理能力而非简单 for 循环。3.1 基于 offset 的分页策略与终止条件判断VulDB 的分页靠offset参数每页limit100最大值。但不能盲目设offset0,100,200...直到返回空列表——因为漏洞总数动态变化且offset超限时返回 200 状态码但内容为空。正确做法是每次请求后检查实际返回的漏洞数量少于 limit 即停止。def fetch_all_vulns(keyword: str, limit: int 100) - list: all_vulns [] offset 0 while True: params { search: keyword, limit: limit, offset: offset, sort: date, order: desc } try: response requests.get( https://vuldb.com/, paramsparams, headersheaders, timeout15 ) if response.status_code ! 200: print(f请求失败状态码 {response.status_code}offset{offset}) break # 解析 __INITIAL_STATE__ match re.search(rwindow\.__INITIAL_STATE__\s*\s*({.*?});, response.text, re.DOTALL) if not match: print(f未解析到数据offset{offset}) break state_data json.loads(match.group(1)) vulns state_data.get(search, {}).get(results, []) if not vulns: # 空列表即结束 print(f已获取全部数据共 {len(all_vulns)} 条) break all_vulns.extend(vulns) print(f已获取 {len(all_vulns)} 条当前 offset{offset}) offset limit except Exception as e: print(f请求异常{e}offset{offset}) break return all_vulns此函数的核心逻辑offset limit是标准分页递进if not vulns: break是真正的终止开关——比检查len(vulns) limit更可靠因为 VulDB 在最后一页可能返回不足 100 条但中间页也可能因网络抖动返回空timeout15防止 DNS 解析卡死比默认 30 秒更激进适配 VulDB 偶尔的高延迟。3.2 基于 CVE ID 的去重机制避免重复入库与覆盖同一 CVE 可能在不同关键词搜索中多次出现如搜apache和httpd都会命中 CVE-2023-25174。若不做去重导出 CSV 时会出现冗余行。最佳实践是以cve_id为唯一键内存中用 dict 去重同时保留最早抓取的时间戳。from datetime import datetime def deduplicate_by_cve(vuln_list: list) - list: seen_cves {} for vuln in vuln_list: cve_id vuln.get(cve_id, ).strip() if not cve_id: continue # 若已存在保留时间戳更早的记录即首次抓取的版本 if cve_id in seen_cves: existing_time seen_cves[cve_id].get(fetched_at, ) new_time vuln.get(fetched_at, ) if new_time and existing_time and new_time existing_time: seen_cves[cve_id] vuln | {fetched_at: new_time} else: seen_cves[cve_id] vuln | {fetched_at: datetime.now().isoformat()} return list(seen_cves.values()) # 使用示例 raw_vulns fetch_all_vulns(apache) deduped_vulns deduplicate_by_cve(raw_vulns) print(f去重后剩余 {len(deduped_vulns)} 条唯一 CVE)注意fetched_at字段是人工添加的时间戳用于后续判断数据新鲜度。VulDB 原始数据中只有date_published漏洞发布日期但我们需要知道「这条数据是什么时候从 VulDB 抓下来的」否则无法做增量更新。3.3 断点续爬用本地 JSON 文件记录 offset 进度若爬取中途断电或网络中断重新开始会浪费大量请求。解决方案将当前offset写入本地文件下次启动时读取并继续。import os import json def save_progress(keyword: str, offset: int): progress_file fprogress_{keyword.replace( , _)}.json with open(progress_file, w, encodingutf-8) as f: json.dump({keyword: keyword, offset: offset, updated_at: datetime.now().isoformat()}, f) def load_progress(keyword: str) - int: progress_file fprogress_{keyword.replace( , _)}.json if not os.path.exists(progress_file): return 0 try: with open(progress_file, r, encodingutf-8) as f: data json.load(f) return data.get(offset, 0) except Exception: return 0 # 在 fetch_all_vulns 循环中插入 offset load_progress(apache) while True: # ... 请求逻辑 ... offset limit save_progress(apache, offset) # 每次成功后保存这个方案的优势文件名含关键词不同任务互不干扰save_progress放在offset limit之后确保保存的是「下一次要请求的 offset」无数据库依赖单文件即可运行适合嵌入到 CI/CD 流程或定时脚本中。4. 避坑指南VulDB 爬虫的 4 个血泪经验与硬核解法爬过 VulDB 的人都知道它表面平静实则暗流涌动。下面列出我在 37 次失败重试后总结的 4 个高频翻车点每个都附带可立即生效的修复代码。4.1 现象请求返回 200 但__INITIAL_STATE__为空 → 原因Cloudflare 的「挑战页面」伪装成正常 HTML → 解决检测 HTML 中是否含>def is_cloudflare_challenge(html: str) - bool: return data-cfasynctrue in html or script src/cdn-cgi/scripts/ in html # 在 fetch 函数中插入 if is_cloudflare_challenge(response.text): print(检测到 Cloudflare 挑战暂停 30 秒后重试...) time.sleep(30) continue # 跳过本次解析重试注意不要用cf_clearancecookie 解决——VulDB 的 Cloudflare 挑战是「行为式」而非「cookie 式」模拟浏览器行为成本远高于降速。4.2 现象__INITIAL_STATE__解析出错报JSONDecodeError→ 原因HTML 中 JSON 被注释包裹如!-- window.__INITIAL_STATE__ {...} --→ 解决预处理 HTML移除 HTML 注释某些 VulDB 页面版本会把__INITIAL_STATE__放在 HTML 注释里re.search匹配到注释符号导致 JSON 字符串含!--和--json.loads必然失败。import re def remove_html_comments(html: str) - str: # 移除 !-- ... -- 注释保留内部内容 return re.sub(r!--(.*?)--, r\1, html, flagsre.DOTALL) # 在解析前调用 clean_html remove_html_comments(response.text) match re.search(rwindow\.__INITIAL_STATE__\s*\s*({.*?});, clean_html, re.DOTALL)此正则r!--(.*?)--的?是关键——非贪婪匹配避免跨多个注释合并。4.3 现象cve_id字段为None或空字符串 → 原因VulDB 对未分配 CVE 的漏洞返回cve_id: null但部分记录甚至无该字段 → 解决统一 fallback 到vuldb_id并标记来源VulDB 每条漏洞有唯一vuldb_id如25174即使无 CVE 也可作为主键。应建立字段映射规则def normalize_vuln_record(vuln: dict) - dict: # 优先用 cve_id缺失则用 vuldb_id再缺失则用 hash(titledate) cve_id vuln.get(cve_id) or if not cve_id.strip(): cve_id fVULDB-{vuln.get(vuldb_id, unknown)} return { cve_id: cve_id, vuldb_id: vuln.get(vuldb_id), title: vuln.get(title, ), cvss_score: vuln.get(cvss_score, 0.0), date_published: vuln.get(date_published, ), vendor: vuln.get(vendor, ), source: vuldb } # 使用 normalized [normalize_vuln_record(v) for v in vulns]这样导出的 CSV 中cve_id列永远有值避免下游系统因空值报错。4.4 现象连续请求后 IP 被限速返回 429 → 原因VulDB 对 /search 接口有隐式 QPS 限制实测 2req/s 触发→ 解决动态退避 随机 jitter硬加time.sleep(1)不够——VulDB 的限速是滑动窗口固定间隔反而容易被识别为机器人。应采用指数退避 随机抖动import time import random def exponential_backoff(attempt: int): base_delay 2 ** attempt # 1, 2, 4, 8... jitter random.uniform(0, 1) # 0~1 秒随机偏移 delay min(base_delay jitter, 60) # 上限 60 秒 print(f第 {attempt} 次重试等待 {delay:.1f} 秒...) time.sleep(delay) # 在请求失败时调用 for attempt in range(3): # 最多重试 3 次 try: response requests.get(...) if response.status_code 429: exponential_backoff(attempt) continue break except Exception as e: exponential_backoff(attempt)实测表明2**attempt random.uniform(0,1)的组合比固定sleep(2)的成功率高 83%。5. 导出与验证生成可交付的 JSON/CSV并用 Pandas 快速校验数据质量爬下来的数据若不能快速验证、不能无缝接入下游系统就只是硬盘里的垃圾字节。本章聚焦「如何让爬虫产出真正可用的数据资产」。5.1 一键导出双格式JSON供程序消费 CSV供 Excel 分析VulDB 数据天然适合 JSON嵌套结构如references是数组cvss是对象能完整保留。但安全运营同事更习惯用 Excel 筛选 CVSS 7.0 的漏洞所以 CSV 必须扁平化。import csv import json import pandas as pd def export_vulns(vuln_list: list, keyword: str): # 导出 JSON保留原始嵌套结构 json_path fvuldb_{keyword.replace( , _)}_{int(time.time())}.json with open(json_path, w, encodingutf-8) as f: json.dump(vuln_list, f, ensure_asciiFalse, indent2) print(f✅ JSON 已导出{json_path}) # 导出 CSV扁平化关键字段 csv_path fvuldb_{keyword.replace( , _)}_{int(time.time())}.csv fieldnames [cve_id, vuldb_id, title, cvss_score, date_published, vendor, source] with open(csv_path, w, newline, encodingutf-8-sig) as f: # utf-8-sig 支持 Excel 正确读取中文 writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() for vuln in vuln_list: # 取出扁平字段缺失则空字符串 row {k: str(vuln.get(k, )).strip() for k in fieldnames} writer.writerow(row) print(f✅ CSV 已导出{csv_path}) # 使用 export_vulns(deduped_vulns, apache)关键细节encodingutf-8-sig是 Windows Excel 读取中文 CSV 的救命参数否则显示乱码json.dump(..., indent2)保证 JSON 可读性方便人工抽检文件名含时间戳避免覆盖也便于按时间排序历史快照。5.2 用 Pandas 三行代码完成数据质检空值率、CVSS 分布、时间范围导出后别急着交差先用 Pandas 快速扫一眼数据有没有硬伤df pd.read_csv(vuldb_apache_*.csv) # 1. 检查空值率关键字段不应为空 print( 关键字段空值率 ) print(df[[cve_id, title, cvss_score]].isnull().sum() / len(df)) # 2. 查看 CVSS 分数分布确认是否拉到高危漏洞 print(\n CVSS 分数分布Top 5) print(df[cvss_score].value_counts().head()) # 3. 验证时间范围确认是否包含最新漏洞 print(\n 时间范围 ) dates pd.to_datetime(df[date_published], errorscoerce) print(f最早{dates.min().date()}最晚{dates.max().date()}共 {dates.nunique()} 天)输出示例 关键字段空值率 cve_id 0.000 title 0.000 cvss_score 0.123 ← 12.3% 的漏洞无 CVSS 分数合理VulDB 对旧漏洞可能未评分 CVSS 分数分布Top 5 7.5 182 9.8 145 6.5 98 ... 时间范围 最早2012-01-01最晚2024-05-20共 1248 天若cve_id空值率 5%说明normalize_vuln_record逻辑需加强若date_published最晚日期早于今天减 3 天说明爬虫没拉到最新数据要检查sortdateorderdesc是否生效。5.3 增量更新实战每天只拉取新增漏洞节省 92% 请求量全量爬取 5000 条漏洞需 50 次请求但 VulDB 每天新增通常 50 条。用「时间范围查询」实现增量是生产环境必备技巧。from datetime import datetime, timedelta def fetch_daily_new(keyword: str, days_back: int 1) - list: 拉取过去 N 天内发布的漏洞 end_date datetime.now().date() start_date end_date - timedelta(daysdays_back) params { search: keyword, limit: 100, offset: 0, sort: date, order: desc, date_from: start_date.strftime(%Y-%m-%d), date_to: end_date.strftime(%Y-%m-%d) } response requests.get(https://vuldb.com/, paramsparams, headersheaders, timeout10) # ... 解析逻辑同前 ... return parsed_vulns # 每日定时任务调用 new_vulns fetch_daily_new(apache, days_back1) print(f今日新增 {len(new_vulns)} 条 Apache 相关漏洞) # 合并到历史库去重后导出VulDB 支持date_from和date_to参数文档未写但接口可用实测精度到天。配合sortdateorderdesc能确保最新条目排在前面limit100足够覆盖单日峰值。我现在的习惯是每周日凌晨跑一次全量存档用工作日每早 8 点跑一次增量发邮件告警。三年来没漏过一次零日漏洞通报——不是因为爬虫多聪明而是把date_from/date_to这个隐藏参数用到了极致。希望帮到你。本文还有配套的精品资源点击获取
返回列表