mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-27 16:51:03 +00:00
Discovery asks GitHub for repositories pushed in the last three days, so a proof of concept written for a 2019 CVE in 2021 is unreachable however long the job runs. nomi-sec/PoC-in-GitHub has collected them daily since 1999. It collects without filtering, which is how this index inherited its junk the first time, so nothing arrives trusted: every candidate goes through qualifying_repo_cves, the same gate a repository found by search must pass. Of 6,945 candidates the gate rejected 4,110 and accepted 2,835, adding links to 1,528 entries that had none or had fewer. Vulhub joins the curated archives beside ExploitDB and Metasploit. It is 241 entries, and each one is a docker-compose file and reproduction steps rather than a description of a flaw. Candidates are judged again every week rather than remembered as rejected: an empty shell in March is often a working exploit by June.
211 lines
8.1 KiB
Python
211 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Fold the curated exploit archives into the index.
|
|
|
|
ExploitDB and Metasploit both publish a single machine readable file mapping
|
|
their entries to CVE ids. Neither is a GitHub repository, so their links are
|
|
kept in their own sections rather than being counted and starred as one.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import csv
|
|
import gzip
|
|
import io
|
|
import json
|
|
import re
|
|
import urllib.request
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CVES = ROOT / "cves"
|
|
INDEX = ROOT / "index"
|
|
EDB_CSV = "https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv"
|
|
MSF_JSON = ("https://raw.githubusercontent.com/rapid7/metasploit-framework/"
|
|
"master/db/modules_metadata_base.json")
|
|
EDB_ENTRY = "https://www.exploit-db.com/exploits/"
|
|
MSF_BLOB = "https://github.com/rapid7/metasploit-framework/blob/master"
|
|
CVE = re.compile(r"CVE-\d{4}-\d{4,7}", re.I)
|
|
|
|
|
|
def plausible(cve: str) -> bool:
|
|
"""Reject impossible years. Metasploit ships CVE-1012-5358 for CVE-2012-5358."""
|
|
return 1999 <= int(cve.split("-")[1]) <= 2100
|
|
USER_AGENT = "0xMarcio-cve-exploits"
|
|
# One CVE can carry dozens of ExploitDB rows for the same flaw on different
|
|
# platforms. Enough to be useful, not enough to bury the repositories below.
|
|
PER_CVE = 8
|
|
|
|
SOURCES = (
|
|
("#### ExploitDB", "No ExploitDB entry."),
|
|
("#### Metasploit", "No Metasploit module."),
|
|
("#### Vulhub", "No Vulhub environment."),
|
|
)
|
|
|
|
VULHUB_TARBALL = "https://codeload.github.com/vulhub/vulhub/tar.gz/refs/heads/master"
|
|
VULHUB_TREE = "https://github.com/vulhub/vulhub/tree/master/"
|
|
|
|
|
|
def fetch(url: str) -> bytes:
|
|
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
with urllib.request.urlopen(request, timeout=180) as response:
|
|
raw = response.read()
|
|
return gzip.decompress(raw) if url.endswith(".gz") else raw
|
|
|
|
|
|
def exploitdb() -> dict[str, list[str]]:
|
|
"""CVE to ExploitDB entry links, verified entries first, newest first."""
|
|
text = fetch(EDB_CSV).decode("utf-8", "replace")
|
|
rows: dict[str, list[tuple[int, int]]] = defaultdict(list)
|
|
for row in csv.DictReader(io.StringIO(text)):
|
|
codes = row.get("codes") or ""
|
|
if "CVE-" not in codes.upper():
|
|
continue
|
|
try:
|
|
entry = int(row["id"])
|
|
except (KeyError, TypeError, ValueError):
|
|
continue
|
|
verified = 1 if str(row.get("verified") or "").strip() == "1" else 0
|
|
for cve in {c.upper() for c in CVE.findall(codes)}:
|
|
rows[cve].append((verified, entry))
|
|
return {
|
|
cve: [f"{EDB_ENTRY}{entry}" for _, entry in
|
|
sorted(found, key=lambda item: (-item[0], -item[1]))[:PER_CVE]]
|
|
for cve, found in rows.items()
|
|
}
|
|
|
|
|
|
def metasploit() -> dict[str, list[str]]:
|
|
"""CVE to module source links, the better ranked module first.
|
|
|
|
A module is written to be run, so rank is the useful order here: excellent
|
|
beats manual, whatever order the database happens to list them in.
|
|
"""
|
|
data = json.loads(fetch(MSF_JSON).decode("utf-8", "replace"))
|
|
rows: dict[str, list[tuple[int, str]]] = defaultdict(list)
|
|
for module in data.values():
|
|
path = str(module.get("path") or "")
|
|
if not path.endswith(".rb"):
|
|
continue
|
|
rank = int(module.get("rank") or 0)
|
|
found: set[str] = set()
|
|
for reference in module.get("references") or []:
|
|
found |= {c.upper() for c in CVE.findall(str(reference))}
|
|
found = {cve for cve in found if plausible(cve)}
|
|
# An exploit module is written against the flaws it lists, chains
|
|
# included. A scanner is not: "SSL/TLS Version Detection" cites BEAST,
|
|
# RC4, POODLE and Logjam because it reports on them, and filing it as a
|
|
# proof of concept under each is wrong. Keep auxiliary and post modules
|
|
# only where they name a single CVE, which is the shape of an auxiliary
|
|
# module that is itself the exploit.
|
|
if str(module.get("type") or "") != "exploit" and len(found) > 1:
|
|
continue
|
|
for cve in found:
|
|
rows[cve].append((rank, MSF_BLOB + path))
|
|
return {
|
|
cve: [url for _, url in
|
|
sorted(found, key=lambda item: (-item[0], item[1]))[:PER_CVE]]
|
|
for cve, found in rows.items()
|
|
}
|
|
|
|
|
|
def vulhub() -> dict[str, list[str]]:
|
|
"""CVE to the directory holding a runnable vulnerable environment.
|
|
|
|
Vulhub ships a docker-compose file and reproduction steps per flaw, which
|
|
is a proof of concept anyone can stand up rather than read about. One
|
|
directory per environment, the CVE named in its path.
|
|
"""
|
|
import io
|
|
import tarfile
|
|
|
|
rows: dict[str, set[str]] = defaultdict(set)
|
|
with tarfile.open(fileobj=io.BytesIO(fetch(VULHUB_TARBALL)), mode="r:gz") as tar:
|
|
for member in tar:
|
|
parts = Path(member.name).parts
|
|
# <archive>/<product>/<environment>; anything deeper is its contents
|
|
if len(parts) != 3 or not member.isdir():
|
|
continue
|
|
for cve in {c.upper() for c in CVE.findall(parts[-1])}:
|
|
if plausible(cve):
|
|
rows[cve].add(VULHUB_TREE + "/".join(parts[1:]))
|
|
return {cve: sorted(found)[:PER_CVE] for cve, found in rows.items()}
|
|
|
|
|
|
def section_bounds(text: str, header: str) -> tuple[int, int] | None:
|
|
start = text.find("\n" + header)
|
|
if start < 0:
|
|
return None
|
|
body = start + 1 + len(header)
|
|
nxt = text.find("\n#", body)
|
|
return body, len(text) if nxt < 0 else nxt
|
|
|
|
|
|
def apply_to_markdown(cve: str, header: str, urls: list[str], dry_run: bool) -> str:
|
|
path = CVES / cve.split("-")[1] / f"{cve}.md"
|
|
if not path.exists():
|
|
return "absent"
|
|
original = path.read_text(encoding="utf-8")
|
|
bounds = section_bounds(original, header)
|
|
block = "\n" + "\n".join(f"- {url}" for url in urls) + "\n"
|
|
if bounds:
|
|
start, end = bounds
|
|
if original[start:end] == block:
|
|
return "unchanged"
|
|
updated = original[:start] + block + original[end:]
|
|
else:
|
|
updated = original.rstrip("\n") + f"\n\n{header}\n" + block.lstrip("\n")
|
|
if not dry_run:
|
|
path.write_text(updated, encoding="utf-8")
|
|
return "added"
|
|
|
|
|
|
def drop_section(cve: str, header: str, dry_run: bool) -> bool:
|
|
"""Remove a section for a CVE the source no longer claims."""
|
|
path = CVES / cve.split("-")[1] / f"{cve}.md"
|
|
if not path.exists():
|
|
return False
|
|
original = path.read_text(encoding="utf-8")
|
|
bounds = section_bounds(original, header)
|
|
if not bounds:
|
|
return False
|
|
start, _ = bounds
|
|
head = original.rfind("\n" + header, 0, start + 1)
|
|
end = bounds[1]
|
|
updated = (original[:head] + original[end:]).rstrip("\n") + "\n"
|
|
if not dry_run:
|
|
path.write_text(updated, encoding="utf-8")
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Sync ExploitDB and Metasploit into the index")
|
|
parser.add_argument("--dry-run", action="store_true", help="report without writing")
|
|
args = parser.parse_args()
|
|
|
|
for (header, _), collect in zip(SOURCES, (exploitdb, metasploit, vulhub)):
|
|
found = collect()
|
|
links = sum(len(urls) for urls in found.values())
|
|
tally = {"added": 0, "unchanged": 0, "absent": 0}
|
|
for cve, urls in sorted(found.items()):
|
|
tally[apply_to_markdown(cve, header, urls, args.dry_run)] += 1
|
|
# A CVE the source has stopped claiming is never visited above, so its
|
|
# section would otherwise survive every future run.
|
|
marker = "\n" + header
|
|
stale = 0
|
|
for path in CVES.glob("[12][0-9][0-9][0-9]/CVE-*.md"):
|
|
if path.stem in found:
|
|
continue
|
|
if marker in path.read_text(encoding="utf-8", errors="replace"):
|
|
stale += drop_section(path.stem, header, args.dry_run)
|
|
name = header.replace("#", "").strip()
|
|
print(f"{name}: {len(found):,} CVEs, {links:,} links | "
|
|
f"{tally['added']:,} entries written, {tally['unchanged']:,} unchanged, "
|
|
f"{tally['absent']:,} for CVEs not in the index, {stale:,} stale sections dropped")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|