0sec-cli 0.15.0 → 0.18.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 +327 -0
- package/README.md +68 -48
- 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-SMH6V4PD.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-HQ7HLP6V.js +34 -0
- package/chunks/assumption-mining-5SNIVVDN.js +79 -0
- package/chunks/chunk-242TMK6G.js +110 -0
- package/chunks/chunk-2JCCA2JL.js +381 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-3KPWWJCI.js +1422 -0
- package/chunks/chunk-3MOLBTLS.js +953 -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-5JI7L7KV.js +74240 -0
- package/chunks/chunk-6L26OBWI.js +1094 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-72CISA2D.js +2597 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-AD7WXDH6.js +3250 -0
- package/chunks/chunk-ADOLI6DT.js +102 -0
- package/chunks/chunk-AHTBZC3E.js +596 -0
- package/chunks/chunk-ASUR522M.js +518 -0
- package/chunks/chunk-ATATQACO.js +471 -0
- package/chunks/chunk-BATRQBOR.js +1732 -0
- package/chunks/chunk-BFR2CDV5.js +5735 -0
- package/chunks/chunk-BFRB6HB2.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-BVAZO4WA.js +193 -0
- package/chunks/chunk-BZNEY2ZS.js +33225 -0
- package/chunks/chunk-C2WXWFBB.js +1070 -0
- package/chunks/chunk-CMKXP5RR.js +917 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-GMT2AZKM.js +2439 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-JQHOLLTD.js +3729 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-N7BOUYEV.js +852 -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-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-SVJEDVYK.js +808 -0
- package/chunks/chunk-TSK2AG6J.js +5108 -0
- package/chunks/chunk-U5FLXGHT.js +2010 -0
- package/chunks/chunk-UCYRA73C.js +662 -0
- package/chunks/chunk-UR4ELX2Y.js +4784 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WEBMRWEG.js +9337 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-WXQ6BJ6A.js +596 -0
- package/chunks/chunk-XM5NLHYU.js +600 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/chunk-YYRBQVFE.js +2572 -0
- package/chunks/chunk-Z5FA2XOX.js +2798 -0
- package/chunks/commands-TBC2F5CE.js +16999 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-NU63MYZM.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-KFWOAUUB.js +16 -0
- package/chunks/disclose-EKTWSCGW.js +132 -0
- package/chunks/dist-4MAW5X6L.js +74 -0
- package/chunks/dist-4YICS2J2.js +2776 -0
- package/chunks/dist-FMGSR3DW.js +159 -0
- package/chunks/eval-runner-7ZF472KV.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-45YQOHPJ.js +98 -0
- package/chunks/exploit-autoclimb-UZGEMGYT.js +81 -0
- package/chunks/exploit-climb-SG23KB2G.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-7ODOLOWU.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-MMKUOETU.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-7U6CWHXD.js +44 -0
- package/chunks/kernel-vm-runner-3JAU4O6J.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-RJCJPOUS.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-OC2VZN2X.js +62 -0
- package/chunks/pipeline-BZK5BLJR.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-LQRCS5OY.js +13 -0
- package/chunks/replay-runner-45G3VU7X.js +39 -0
- package/chunks/run-D56MPDKY.js +32716 -0
- package/chunks/runtime-G7YWIO3B.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-GZ4A4KYT.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-YERCYMPD.js +13 -0
- package/chunks/web-recon-prepass-JJGPEOLE.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,685 @@
|
|
|
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/adgraph/paths.js
|
|
6
|
+
var AD_EDGE_TECHNIQUES = {
|
|
7
|
+
MemberOf: "inherits the group's rights through membership",
|
|
8
|
+
Contains: "controls the child object via the containing OU/container",
|
|
9
|
+
GPLink: "applies policy to every object under the linked container",
|
|
10
|
+
TrustedBy: "crosses the domain trust into the trusting domain",
|
|
11
|
+
HasSIDHistory: "carries the target's SID in SID history, inheriting its access",
|
|
12
|
+
AdminTo: "has local administrator rights on the host",
|
|
13
|
+
HasSession: "can dump the logged-on user's credentials from LSASS",
|
|
14
|
+
CanRDP: "can log on interactively over RDP",
|
|
15
|
+
CanPSRemote: "can execute code over PowerShell Remoting (WinRM)",
|
|
16
|
+
ExecuteDCOM: "can execute code through DCOM activation",
|
|
17
|
+
SQLAdmin: "is a SQL sysadmin and can execute OS commands via xp_cmdshell",
|
|
18
|
+
GenericAll: "holds full control and can reset the password or add itself",
|
|
19
|
+
GenericWrite: "can write attributes, e.g. set an SPN or a logon script",
|
|
20
|
+
WriteDacl: "can rewrite the DACL and grant itself full control",
|
|
21
|
+
WriteOwner: "can take ownership and then rewrite the DACL",
|
|
22
|
+
Owns: "owns the object and can grant itself full control",
|
|
23
|
+
WriteSPN: "can set an SPN and then kerberoast the account",
|
|
24
|
+
AllExtendedRights: "holds all extended rights, including password reset",
|
|
25
|
+
ForceChangePassword: "can reset the account password without knowing the old one",
|
|
26
|
+
AddMember: "can add any principal to the group",
|
|
27
|
+
AddSelf: "can add itself to the group",
|
|
28
|
+
AddKeyCredentialLink: "can shadow-credential the account and request a TGT",
|
|
29
|
+
WriteAccountRestrictions: "can set delegation attributes on the account",
|
|
30
|
+
WriteGPLink: "can link a malicious GPO to the container",
|
|
31
|
+
ReadLAPSPassword: "can read the LAPS-managed local administrator password",
|
|
32
|
+
ReadGMSAPassword: "can read the gMSA password blob and derive its keys",
|
|
33
|
+
SyncLAPSPassword: "can sync the LAPS password via directory replication",
|
|
34
|
+
DumpSMSAPassword: "can dump the sMSA password from the host",
|
|
35
|
+
AllowedToDelegate: "can impersonate any user to the target service (constrained delegation)",
|
|
36
|
+
AllowedToAct: "is trusted for resource-based constrained delegation to the host",
|
|
37
|
+
AddAllowedToAct: "can configure RBCD on the target and then impersonate any user",
|
|
38
|
+
DCSync: "can replicate directory secrets, including the krbtgt hash",
|
|
39
|
+
GetChanges: "holds one half of the replication rights needed for DCSync",
|
|
40
|
+
GetChangesAll: "holds the second half of the replication rights needed for DCSync",
|
|
41
|
+
GetChangesInFilteredSet: "can replicate the filtered attribute set (LAPS)",
|
|
42
|
+
ADCSESC1: "enrolls in a template allowing a caller-supplied SAN (ESC1)",
|
|
43
|
+
ADCSESC3: "abuses an enrollment-agent template to enrol on behalf of others (ESC3)",
|
|
44
|
+
ADCSESC4: "has write access over the certificate template itself (ESC4)",
|
|
45
|
+
ADCSESC5: "controls a PKI object the CA depends on (ESC5)",
|
|
46
|
+
ADCSESC6a: "abuses EDITF_ATTRIBUTESUBJECTALTNAME2 on the CA (ESC6)",
|
|
47
|
+
ADCSESC6b: "abuses EDITF_ATTRIBUTESUBJECTALTNAME2 with weak binding (ESC6)",
|
|
48
|
+
ADCSESC7: "holds ManageCA/ManageCertificates on the CA (ESC7)",
|
|
49
|
+
ADCSESC9a: "abuses a template with CT_FLAG_NO_SECURITY_EXTENSION (ESC9)",
|
|
50
|
+
ADCSESC9b: "abuses no-security-extension against a machine account (ESC9)",
|
|
51
|
+
ADCSESC10a: "abuses weak certificate mapping on the DC (ESC10)",
|
|
52
|
+
ADCSESC10b: "abuses UPN certificate mapping on the DC (ESC10)",
|
|
53
|
+
ADCSESC13: "abuses an issuance policy linked to a privileged group (ESC13)",
|
|
54
|
+
GoldenCert: "can steal the CA private key and forge certificates for any principal",
|
|
55
|
+
ManageCA: "is a CA administrator and can enable a vulnerable configuration",
|
|
56
|
+
ManageCertificates: "is a certificate manager and can approve pending requests",
|
|
57
|
+
Enroll: "may request a certificate from the template",
|
|
58
|
+
WritePKIEnrollmentFlag: "can set the enrollment flags on the template",
|
|
59
|
+
WritePKINameFlag: "can enable caller-supplied subject names on the template",
|
|
60
|
+
CoerceToTGT: "can coerce the target into authenticating and capture its TGT",
|
|
61
|
+
CoerceAndRelayNTLMToADCS: "can coerce authentication and relay it to ADCS web enrollment",
|
|
62
|
+
CoerceAndRelayNTLMToLDAP: "can coerce authentication and relay it to LDAP",
|
|
63
|
+
CoerceAndRelayNTLMToLDAPS: "can coerce authentication and relay it to LDAPS",
|
|
64
|
+
CoerceAndRelayNTLMToSMB: "can coerce authentication and relay it to SMB"
|
|
65
|
+
};
|
|
66
|
+
function describeEdgeTechnique(kind) {
|
|
67
|
+
return AD_EDGE_TECHNIQUES[kind] ?? `holds the ${kind} relationship over the target`;
|
|
68
|
+
}
|
|
69
|
+
var DEFAULT_MAX_DEPTH = 8;
|
|
70
|
+
var DEFAULT_MAX_RESULTS = 25;
|
|
71
|
+
var DEFAULT_MAX_EXPANSIONS = 5e5;
|
|
72
|
+
function resolveEdgeFilter(opts) {
|
|
73
|
+
const allowed = opts.allowedEdgeKinds ? new Set(opts.allowedEdgeKinds) : void 0;
|
|
74
|
+
const denied = opts.deniedEdgeKinds ? new Set(opts.deniedEdgeKinds) : void 0;
|
|
75
|
+
if (!allowed && !denied)
|
|
76
|
+
return () => true;
|
|
77
|
+
return (edge) => {
|
|
78
|
+
if (allowed && !allowed.has(edge.kind))
|
|
79
|
+
return false;
|
|
80
|
+
if (denied && denied.has(edge.kind))
|
|
81
|
+
return false;
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function resolveEdgeCost(opts) {
|
|
86
|
+
const fn = opts.edgeCost;
|
|
87
|
+
if (!fn)
|
|
88
|
+
return () => 1;
|
|
89
|
+
return (edge) => {
|
|
90
|
+
const cost = fn(edge);
|
|
91
|
+
return Number.isFinite(cost) && cost > 0 ? cost : 0;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
var LabelHeap = class {
|
|
95
|
+
cost;
|
|
96
|
+
depth;
|
|
97
|
+
items = [];
|
|
98
|
+
constructor(cost, depth) {
|
|
99
|
+
this.cost = cost;
|
|
100
|
+
this.depth = depth;
|
|
101
|
+
}
|
|
102
|
+
get size() {
|
|
103
|
+
return this.items.length;
|
|
104
|
+
}
|
|
105
|
+
before(a, b) {
|
|
106
|
+
if (this.cost[a] !== this.cost[b])
|
|
107
|
+
return this.cost[a] < this.cost[b];
|
|
108
|
+
return this.depth[a] < this.depth[b];
|
|
109
|
+
}
|
|
110
|
+
push(label) {
|
|
111
|
+
const items = this.items;
|
|
112
|
+
items.push(label);
|
|
113
|
+
let i = items.length - 1;
|
|
114
|
+
while (i > 0) {
|
|
115
|
+
const parent = i - 1 >> 1;
|
|
116
|
+
if (!this.before(items[i], items[parent]))
|
|
117
|
+
break;
|
|
118
|
+
[items[i], items[parent]] = [items[parent], items[i]];
|
|
119
|
+
i = parent;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
pop() {
|
|
123
|
+
const items = this.items;
|
|
124
|
+
if (items.length === 0)
|
|
125
|
+
return void 0;
|
|
126
|
+
const top = items[0];
|
|
127
|
+
const last = items.pop();
|
|
128
|
+
if (items.length === 0)
|
|
129
|
+
return top;
|
|
130
|
+
items[0] = last;
|
|
131
|
+
let i = 0;
|
|
132
|
+
for (; ; ) {
|
|
133
|
+
const left = 2 * i + 1;
|
|
134
|
+
const right = left + 1;
|
|
135
|
+
let smallest = i;
|
|
136
|
+
if (left < items.length && this.before(items[left], items[smallest]))
|
|
137
|
+
smallest = left;
|
|
138
|
+
if (right < items.length && this.before(items[right], items[smallest]))
|
|
139
|
+
smallest = right;
|
|
140
|
+
if (smallest === i)
|
|
141
|
+
break;
|
|
142
|
+
[items[i], items[smallest]] = [items[smallest], items[i]];
|
|
143
|
+
i = smallest;
|
|
144
|
+
}
|
|
145
|
+
return top;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
function buildAttackPath(graph, edgeIndices, edgeCost = () => 1, describeEdge = (edge) => describeEdgeTechnique(edge.kind)) {
|
|
149
|
+
if (edgeIndices.length === 0)
|
|
150
|
+
return void 0;
|
|
151
|
+
const steps = [];
|
|
152
|
+
let cost = 0;
|
|
153
|
+
for (const edgeIndex of edgeIndices) {
|
|
154
|
+
const edge = graph.edges[edgeIndex];
|
|
155
|
+
if (!edge)
|
|
156
|
+
return void 0;
|
|
157
|
+
const from = graph.nodes.get(edge.source);
|
|
158
|
+
const to = graph.nodes.get(edge.target);
|
|
159
|
+
if (!from || !to)
|
|
160
|
+
return void 0;
|
|
161
|
+
cost += edgeCost(edge);
|
|
162
|
+
steps.push({ from, edge, to, technique: `${from.label} ${describeEdge(edge)}` });
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
sourceId: steps[0].from.objectId,
|
|
166
|
+
targetId: steps[steps.length - 1].to.objectId,
|
|
167
|
+
steps,
|
|
168
|
+
length: steps.length,
|
|
169
|
+
cost,
|
|
170
|
+
technique: steps.map((step) => step.edge.kind).join(" -> ")
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function shortestPaths(graph, sourceIds, targetIds, opts = {}) {
|
|
174
|
+
const maxDepth = Math.max(1, opts.maxDepth ?? DEFAULT_MAX_DEPTH);
|
|
175
|
+
const maxResults = Math.max(1, opts.maxResults ?? DEFAULT_MAX_RESULTS);
|
|
176
|
+
const maxExpansions = Math.max(1, opts.maxExpansions ?? DEFAULT_MAX_EXPANSIONS);
|
|
177
|
+
const passesFilter = resolveEdgeFilter(opts);
|
|
178
|
+
const edgeCost = resolveEdgeCost(opts);
|
|
179
|
+
const describeEdge = opts.describeEdge ?? ((edge) => describeEdgeTechnique(edge.kind));
|
|
180
|
+
const targets = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const id of targetIds)
|
|
182
|
+
if (graph.nodes.has(id))
|
|
183
|
+
targets.add(id);
|
|
184
|
+
if (targets.size === 0)
|
|
185
|
+
return [];
|
|
186
|
+
const labelNode = [];
|
|
187
|
+
const labelCost = [];
|
|
188
|
+
const labelDepth = [];
|
|
189
|
+
const labelEdge = [];
|
|
190
|
+
const labelPrev = [];
|
|
191
|
+
const heap = new LabelHeap(labelCost, labelDepth);
|
|
192
|
+
const bestCostByDepth = /* @__PURE__ */ new Map();
|
|
193
|
+
const admit = (node, cost, depth, edgeIndex, prev) => {
|
|
194
|
+
let frontier = bestCostByDepth.get(node);
|
|
195
|
+
if (!frontier) {
|
|
196
|
+
frontier = new Array(maxDepth + 1).fill(Number.POSITIVE_INFINITY);
|
|
197
|
+
bestCostByDepth.set(node, frontier);
|
|
198
|
+
}
|
|
199
|
+
if (cost >= frontier[depth])
|
|
200
|
+
return false;
|
|
201
|
+
for (let d = depth; d <= maxDepth; d += 1) {
|
|
202
|
+
if (frontier[d] <= cost)
|
|
203
|
+
break;
|
|
204
|
+
frontier[d] = cost;
|
|
205
|
+
}
|
|
206
|
+
const label = labelNode.length;
|
|
207
|
+
labelNode.push(node);
|
|
208
|
+
labelCost.push(cost);
|
|
209
|
+
labelDepth.push(depth);
|
|
210
|
+
labelEdge.push(edgeIndex);
|
|
211
|
+
labelPrev.push(prev);
|
|
212
|
+
heap.push(label);
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
215
|
+
let seeded = false;
|
|
216
|
+
for (const id of sourceIds) {
|
|
217
|
+
if (!graph.nodes.has(id))
|
|
218
|
+
continue;
|
|
219
|
+
if (admit(id, 0, 0, -1, -1))
|
|
220
|
+
seeded = true;
|
|
221
|
+
}
|
|
222
|
+
if (!seeded)
|
|
223
|
+
return [];
|
|
224
|
+
const bestLabelByTarget = /* @__PURE__ */ new Map();
|
|
225
|
+
let expansions = 0;
|
|
226
|
+
while (heap.size > 0 && bestLabelByTarget.size < targets.size && expansions < maxExpansions) {
|
|
227
|
+
const label = heap.pop();
|
|
228
|
+
const node = labelNode[label];
|
|
229
|
+
const cost = labelCost[label];
|
|
230
|
+
const depth = labelDepth[label];
|
|
231
|
+
if (cost > bestCostByDepth.get(node)[depth])
|
|
232
|
+
continue;
|
|
233
|
+
if (depth > 0 && targets.has(node) && !bestLabelByTarget.has(node)) {
|
|
234
|
+
bestLabelByTarget.set(node, label);
|
|
235
|
+
}
|
|
236
|
+
if (depth >= maxDepth)
|
|
237
|
+
continue;
|
|
238
|
+
for (const edgeIndex of graph.outbound.get(node) ?? []) {
|
|
239
|
+
const edge = graph.edges[edgeIndex];
|
|
240
|
+
if (!passesFilter(edge))
|
|
241
|
+
continue;
|
|
242
|
+
expansions += 1;
|
|
243
|
+
if (expansions >= maxExpansions)
|
|
244
|
+
break;
|
|
245
|
+
admit(edge.target, cost + edgeCost(edge), depth + 1, edgeIndex, label);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const paths = [];
|
|
249
|
+
for (const label of bestLabelByTarget.values()) {
|
|
250
|
+
const edgeIndices = [];
|
|
251
|
+
for (let cursor = label; cursor !== -1; cursor = labelPrev[cursor]) {
|
|
252
|
+
const edgeIndex = labelEdge[cursor];
|
|
253
|
+
if (edgeIndex !== -1)
|
|
254
|
+
edgeIndices.push(edgeIndex);
|
|
255
|
+
}
|
|
256
|
+
edgeIndices.reverse();
|
|
257
|
+
const path = buildAttackPath(graph, edgeIndices, edgeCost, describeEdge);
|
|
258
|
+
if (path)
|
|
259
|
+
paths.push(path);
|
|
260
|
+
}
|
|
261
|
+
paths.sort((a, b) => a.cost - b.cost || a.length - b.length || a.targetId.localeCompare(b.targetId));
|
|
262
|
+
return paths.slice(0, maxResults);
|
|
263
|
+
}
|
|
264
|
+
function bfs(graph, startIds, maxDepth, index, step, opts) {
|
|
265
|
+
const passesFilter = resolveEdgeFilter(opts);
|
|
266
|
+
const maxExpansions = Math.max(1, opts.maxExpansions ?? DEFAULT_MAX_EXPANSIONS);
|
|
267
|
+
const depths = /* @__PURE__ */ new Map();
|
|
268
|
+
let frontier = [];
|
|
269
|
+
for (const id of startIds) {
|
|
270
|
+
if (!graph.nodes.has(id) || depths.has(id))
|
|
271
|
+
continue;
|
|
272
|
+
depths.set(id, 0);
|
|
273
|
+
frontier.push(id);
|
|
274
|
+
}
|
|
275
|
+
let expansions = 0;
|
|
276
|
+
for (let depth = 0; depth < Math.max(0, maxDepth) && frontier.length > 0; depth += 1) {
|
|
277
|
+
const next = [];
|
|
278
|
+
for (const node of frontier) {
|
|
279
|
+
for (const edgeIndex of index.get(node) ?? []) {
|
|
280
|
+
const edge = graph.edges[edgeIndex];
|
|
281
|
+
if (!passesFilter(edge))
|
|
282
|
+
continue;
|
|
283
|
+
if (++expansions >= maxExpansions)
|
|
284
|
+
return depths;
|
|
285
|
+
const neighbour = step(edge);
|
|
286
|
+
if (depths.has(neighbour))
|
|
287
|
+
continue;
|
|
288
|
+
depths.set(neighbour, depth + 1);
|
|
289
|
+
next.push(neighbour);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
frontier = next;
|
|
293
|
+
}
|
|
294
|
+
return depths;
|
|
295
|
+
}
|
|
296
|
+
function reachableFrom(graph, sourceIds, maxDepth = DEFAULT_MAX_DEPTH, opts = {}) {
|
|
297
|
+
return bfs(graph, sourceIds, maxDepth, graph.outbound, (edge) => edge.target, opts);
|
|
298
|
+
}
|
|
299
|
+
function reachableTo(graph, targetIds, maxDepth = DEFAULT_MAX_DEPTH, opts = {}) {
|
|
300
|
+
return bfs(graph, targetIds, maxDepth, graph.inbound, (edge) => edge.source, opts);
|
|
301
|
+
}
|
|
302
|
+
function nodeOrStub(graph, objectId) {
|
|
303
|
+
return graph.nodes.get(objectId) ?? { objectId, label: objectId, kind: "Base", properties: {}, stub: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// packages/core/dist/adgraph/analyzers.js
|
|
307
|
+
var TIER0_DOMAIN_RIDS = ["512", "516", "518", "519", "521", "526", "527"];
|
|
308
|
+
var DOMAIN_ADMIN_RIDS = ["512", "519"];
|
|
309
|
+
var BUILTIN_ADMINISTRATORS = "S-1-5-32-544";
|
|
310
|
+
var TIER0_BUILTIN_SIDS = [
|
|
311
|
+
BUILTIN_ADMINISTRATORS,
|
|
312
|
+
"S-1-5-32-548",
|
|
313
|
+
// Account Operators
|
|
314
|
+
"S-1-5-32-549",
|
|
315
|
+
// Server Operators
|
|
316
|
+
"S-1-5-32-550",
|
|
317
|
+
// Print Operators
|
|
318
|
+
"S-1-5-32-551"
|
|
319
|
+
// Backup Operators
|
|
320
|
+
];
|
|
321
|
+
var DEFAULT_REPLICATION_HOLDER_SIDS = /* @__PURE__ */ new Set(["S-1-5-9", "S-1-5-32-544"]);
|
|
322
|
+
var DEFAULT_REPLICATION_HOLDER_RIDS = ["512", "516", "518", "519", "498", "521"];
|
|
323
|
+
var DOMAIN_CONTROLLER_RIDS = ["516", "521"];
|
|
324
|
+
var DANGEROUS_ACL_EDGES = [
|
|
325
|
+
"GenericAll",
|
|
326
|
+
"GenericWrite",
|
|
327
|
+
"WriteDacl",
|
|
328
|
+
"WriteOwner",
|
|
329
|
+
"Owns",
|
|
330
|
+
"AllExtendedRights",
|
|
331
|
+
"ForceChangePassword",
|
|
332
|
+
"AddMember",
|
|
333
|
+
"AddSelf",
|
|
334
|
+
"AddKeyCredentialLink",
|
|
335
|
+
"WriteSPN",
|
|
336
|
+
"WriteAccountRestrictions",
|
|
337
|
+
"WriteGPLink"
|
|
338
|
+
];
|
|
339
|
+
var ADCS_ABUSE_EDGES = [
|
|
340
|
+
"GoldenCert",
|
|
341
|
+
"ManageCA",
|
|
342
|
+
"ManageCertificates",
|
|
343
|
+
"WritePKIEnrollmentFlag",
|
|
344
|
+
"WritePKINameFlag",
|
|
345
|
+
"CanAbuseUPNCertMapping",
|
|
346
|
+
"CanAbuseWeakCertBinding"
|
|
347
|
+
];
|
|
348
|
+
function resolveOptions(opts) {
|
|
349
|
+
const maxDepth = Math.max(1, opts.maxDepth ?? 6);
|
|
350
|
+
return {
|
|
351
|
+
maxDepth,
|
|
352
|
+
maxPathsPerFinding: Math.max(1, opts.maxPathsPerFinding ?? 5),
|
|
353
|
+
maxFindingsPerAnalyzer: Math.max(1, opts.maxFindingsPerAnalyzer ?? 25),
|
|
354
|
+
includeDefaultPrivileged: opts.includeDefaultPrivileged ?? false,
|
|
355
|
+
traversal: { maxDepth, ...opts.traversal }
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function slug(...parts) {
|
|
359
|
+
return parts.map((part) => part.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "x").join(":");
|
|
360
|
+
}
|
|
361
|
+
function hasRid(objectId, rids) {
|
|
362
|
+
return rids.some((rid) => objectId.endsWith(`-${rid}`));
|
|
363
|
+
}
|
|
364
|
+
function isTrue(value) {
|
|
365
|
+
return value === true || value === "true" || value === "True";
|
|
366
|
+
}
|
|
367
|
+
function isEnabled(node) {
|
|
368
|
+
return node.properties.enabled !== false;
|
|
369
|
+
}
|
|
370
|
+
function nodesOfKind(graph, kind) {
|
|
371
|
+
const ids = graph.nodesByKind.get(kind) ?? [];
|
|
372
|
+
const out = [];
|
|
373
|
+
for (const id of ids) {
|
|
374
|
+
const node = graph.nodes.get(id);
|
|
375
|
+
if (node)
|
|
376
|
+
out.push(node);
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
function singleEdgePaths(graph, edgeIndices, cap) {
|
|
381
|
+
const paths = [];
|
|
382
|
+
for (const edgeIndex of edgeIndices.slice(0, cap)) {
|
|
383
|
+
const path = buildAttackPath(graph, [edgeIndex]);
|
|
384
|
+
if (path)
|
|
385
|
+
paths.push(path);
|
|
386
|
+
}
|
|
387
|
+
return paths;
|
|
388
|
+
}
|
|
389
|
+
function finding(analyzer, id, severity, title, description, remediation, paths, affectedPrincipals, evidence) {
|
|
390
|
+
return {
|
|
391
|
+
id: `adgraph:${analyzer}:${id}`,
|
|
392
|
+
analyzer,
|
|
393
|
+
title,
|
|
394
|
+
severity,
|
|
395
|
+
description,
|
|
396
|
+
paths,
|
|
397
|
+
affectedPrincipals: [...new Set(affectedPrincipals)],
|
|
398
|
+
remediation,
|
|
399
|
+
...evidence ? { evidence } : {}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
function domainAdminGroupIds(graph) {
|
|
403
|
+
const out = [];
|
|
404
|
+
for (const node of nodesOfKind(graph, "Group")) {
|
|
405
|
+
if (hasRid(node.objectId, DOMAIN_ADMIN_RIDS) || node.objectId === BUILTIN_ADMINISTRATORS) {
|
|
406
|
+
out.push(node.objectId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
function highValueTargetIds(graph, extra) {
|
|
412
|
+
const out = new Set(extra ?? []);
|
|
413
|
+
for (const node of nodesOfKind(graph, "Group")) {
|
|
414
|
+
if (hasRid(node.objectId, TIER0_DOMAIN_RIDS) || TIER0_BUILTIN_SIDS.includes(node.objectId)) {
|
|
415
|
+
out.add(node.objectId);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const node of nodesOfKind(graph, "Domain"))
|
|
419
|
+
out.add(node.objectId);
|
|
420
|
+
for (const node of graph.nodes.values()) {
|
|
421
|
+
if (isTrue(node.properties.highvalue))
|
|
422
|
+
out.add(node.objectId);
|
|
423
|
+
}
|
|
424
|
+
return out;
|
|
425
|
+
}
|
|
426
|
+
function isDomainController(graph, node) {
|
|
427
|
+
if (isTrue(node.properties.isdc))
|
|
428
|
+
return true;
|
|
429
|
+
const dn = String(node.properties.distinguishedname ?? "").toUpperCase();
|
|
430
|
+
if (dn.includes("OU=DOMAIN CONTROLLERS"))
|
|
431
|
+
return true;
|
|
432
|
+
for (const edgeIndex of graph.outbound.get(node.objectId) ?? []) {
|
|
433
|
+
const edge = graph.edges[edgeIndex];
|
|
434
|
+
if (edge.kind === "MemberOf" && hasRid(edge.target, DOMAIN_CONTROLLER_RIDS))
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
function candidateSources(graph, opts, highValue) {
|
|
440
|
+
if (opts.ownedPrincipalIds) {
|
|
441
|
+
return [...opts.ownedPrincipalIds].filter((id) => graph.nodes.has(id));
|
|
442
|
+
}
|
|
443
|
+
return nodesOfKind(graph, "User").filter((node) => isEnabled(node) && !highValue.has(node.objectId) && !isTrue(node.properties.admincount)).map((node) => node.objectId);
|
|
444
|
+
}
|
|
445
|
+
function findPathsToDomainAdmin(graph, opts = {}) {
|
|
446
|
+
const resolved = resolveOptions(opts);
|
|
447
|
+
const targets = domainAdminGroupIds(graph);
|
|
448
|
+
if (targets.length === 0)
|
|
449
|
+
return [];
|
|
450
|
+
const highValue = highValueTargetIds(graph, opts.highValueIds);
|
|
451
|
+
const sources = candidateSources(graph, opts, highValue);
|
|
452
|
+
if (sources.length === 0)
|
|
453
|
+
return [];
|
|
454
|
+
const findings = [];
|
|
455
|
+
for (const targetId of targets) {
|
|
456
|
+
if (findings.length >= resolved.maxFindingsPerAnalyzer)
|
|
457
|
+
break;
|
|
458
|
+
const target = graph.nodes.get(targetId);
|
|
459
|
+
const distances = reachableTo(graph, [targetId], resolved.maxDepth, resolved.traversal);
|
|
460
|
+
const reaching = sources.filter((id) => id !== targetId && distances.has(id)).sort((a, b) => distances.get(a) - distances.get(b) || a.localeCompare(b));
|
|
461
|
+
if (reaching.length === 0)
|
|
462
|
+
continue;
|
|
463
|
+
const paths = [];
|
|
464
|
+
for (const sourceId of reaching.slice(0, resolved.maxPathsPerFinding)) {
|
|
465
|
+
const [path] = shortestPaths(graph, [sourceId], [targetId], {
|
|
466
|
+
...resolved.traversal,
|
|
467
|
+
maxResults: 1
|
|
468
|
+
});
|
|
469
|
+
if (path)
|
|
470
|
+
paths.push(path);
|
|
471
|
+
}
|
|
472
|
+
if (paths.length === 0)
|
|
473
|
+
continue;
|
|
474
|
+
findings.push(finding("paths-to-domain-admin", slug(targetId), "critical", `Attack path from unprivileged principals to ${target.label}`, `${reaching.length} unprivileged principal(s) can reach ${target.label} within ${resolved.maxDepth} hops. The shortest path takes ${paths[0].length} hop(s): ${paths[0].technique}.`, `Break the shortest hop first \u2014 it is usually a single over-broad ACL or a stale local-admin membership. Move ${target.label} into a Tier-0 admin tier, remove standing membership in favour of just-in-time elevation, and re-run collection to confirm the path is gone.`, paths, reaching.slice(0, 100), {
|
|
475
|
+
targetId,
|
|
476
|
+
reachingPrincipalCount: reaching.length,
|
|
477
|
+
shortestPathLength: paths[0].length
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
return findings;
|
|
481
|
+
}
|
|
482
|
+
function findKerberoastablePaths(graph, opts = {}) {
|
|
483
|
+
const resolved = resolveOptions(opts);
|
|
484
|
+
const highValue = highValueTargetIds(graph, opts.highValueIds);
|
|
485
|
+
const roastable = nodesOfKind(graph, "User").filter((node) => isTrue(node.properties.hasspn) && isEnabled(node));
|
|
486
|
+
if (roastable.length === 0)
|
|
487
|
+
return [];
|
|
488
|
+
const distances = reachableTo(graph, highValue, resolved.maxDepth, resolved.traversal);
|
|
489
|
+
const findings = [];
|
|
490
|
+
for (const node of roastable) {
|
|
491
|
+
if (findings.length >= resolved.maxFindingsPerAnalyzer)
|
|
492
|
+
break;
|
|
493
|
+
const alreadyPrivileged = highValue.has(node.objectId) || isTrue(node.properties.admincount);
|
|
494
|
+
if (!alreadyPrivileged && !distances.has(node.objectId))
|
|
495
|
+
continue;
|
|
496
|
+
const paths = alreadyPrivileged ? [] : shortestPaths(graph, [node.objectId], highValue, {
|
|
497
|
+
...resolved.traversal,
|
|
498
|
+
maxResults: resolved.maxPathsPerFinding
|
|
499
|
+
});
|
|
500
|
+
if (!alreadyPrivileged && paths.length === 0)
|
|
501
|
+
continue;
|
|
502
|
+
const description = alreadyPrivileged ? `${node.label} has an SPN and is itself a Tier-0 principal. Any domain user can request a service ticket for it and crack the hash offline, yielding privileged access directly.` : `${node.label} has an SPN and reaches ${paths.length} high-value target(s) within ${resolved.maxDepth} hops. Cracking its password gives an attacker the whole path: ${paths[0].technique}.`;
|
|
503
|
+
findings.push(finding("kerberoastable-paths", slug(node.objectId), alreadyPrivileged ? "critical" : "high", `Kerberoastable account with a path to privilege: ${node.label}`, description, `Move ${node.label} to a group Managed Service Account so the password is machine-generated and rotated automatically. If that is not possible, set a 25+ character random password and remove the account's privileged group memberships.`, paths, [node.objectId], {
|
|
504
|
+
hasSpn: true,
|
|
505
|
+
alreadyPrivileged,
|
|
506
|
+
servicePrincipalNames: node.properties.serviceprincipalnames,
|
|
507
|
+
pwdLastSet: node.properties.pwdlastset
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
return findings;
|
|
511
|
+
}
|
|
512
|
+
function findUnconstrainedDelegation(graph, opts = {}) {
|
|
513
|
+
const resolved = resolveOptions(opts);
|
|
514
|
+
const findings = [];
|
|
515
|
+
const candidates = [...nodesOfKind(graph, "Computer"), ...nodesOfKind(graph, "User")];
|
|
516
|
+
for (const node of candidates) {
|
|
517
|
+
if (findings.length >= resolved.maxFindingsPerAnalyzer)
|
|
518
|
+
break;
|
|
519
|
+
if (!isTrue(node.properties.unconstraineddelegation))
|
|
520
|
+
continue;
|
|
521
|
+
if (!isEnabled(node))
|
|
522
|
+
continue;
|
|
523
|
+
if (node.kind === "Computer" && isDomainController(graph, node))
|
|
524
|
+
continue;
|
|
525
|
+
const controlEdges = [];
|
|
526
|
+
const controllers = [];
|
|
527
|
+
for (const edgeIndex of graph.inbound.get(node.objectId) ?? []) {
|
|
528
|
+
const edge = graph.edges[edgeIndex];
|
|
529
|
+
if (edge.kind === "MemberOf" || edge.kind === "Contains")
|
|
530
|
+
continue;
|
|
531
|
+
controlEdges.push(edgeIndex);
|
|
532
|
+
controllers.push(edge.source);
|
|
533
|
+
}
|
|
534
|
+
findings.push(finding("unconstrained-delegation", slug(node.objectId), "critical", `Unconstrained delegation on ${node.label}`, `${node.label} is trusted for unconstrained delegation and is not a domain controller. An attacker controlling this host can coerce a DC into authenticating to it, capture the DC's TGT from memory, and replay it to replicate domain secrets. ${controllers.length} principal(s) already hold a direct relationship over it.`, `Clear the TRUSTED_FOR_DELEGATION flag on ${node.label} and switch the service to resource-based constrained delegation. Add Tier-0 accounts to the Protected Users group and mark them "sensitive and cannot be delegated" so their tickets are never cacheable here.`, singleEdgePaths(graph, controlEdges, resolved.maxPathsPerFinding), [node.objectId, ...controllers], {
|
|
535
|
+
objectId: node.objectId,
|
|
536
|
+
kind: node.kind,
|
|
537
|
+
operatingSystem: node.properties.operatingsystem,
|
|
538
|
+
directControllerCount: controllers.length
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
return findings;
|
|
542
|
+
}
|
|
543
|
+
function findDcsyncPrincipals(graph, opts = {}) {
|
|
544
|
+
const resolved = resolveOptions(opts);
|
|
545
|
+
const findings = [];
|
|
546
|
+
for (const domain of nodesOfKind(graph, "Domain")) {
|
|
547
|
+
const rights = /* @__PURE__ */ new Map();
|
|
548
|
+
for (const edgeIndex of graph.inbound.get(domain.objectId) ?? []) {
|
|
549
|
+
const edge = graph.edges[edgeIndex];
|
|
550
|
+
if (edge.kind !== "DCSync" && edge.kind !== "GetChanges" && edge.kind !== "GetChangesAll")
|
|
551
|
+
continue;
|
|
552
|
+
let entry = rights.get(edge.source);
|
|
553
|
+
if (!entry) {
|
|
554
|
+
entry = { kinds: /* @__PURE__ */ new Set(), edges: [] };
|
|
555
|
+
rights.set(edge.source, entry);
|
|
556
|
+
}
|
|
557
|
+
entry.kinds.add(edge.kind);
|
|
558
|
+
entry.edges.push(edgeIndex);
|
|
559
|
+
}
|
|
560
|
+
for (const [principalId, entry] of rights) {
|
|
561
|
+
if (findings.length >= resolved.maxFindingsPerAnalyzer)
|
|
562
|
+
break;
|
|
563
|
+
const viaDcsyncEdge = entry.kinds.has("DCSync");
|
|
564
|
+
const viaReplicationPair = entry.kinds.has("GetChanges") && entry.kinds.has("GetChangesAll");
|
|
565
|
+
if (!viaDcsyncEdge && !viaReplicationPair)
|
|
566
|
+
continue;
|
|
567
|
+
const isDefault = DEFAULT_REPLICATION_HOLDER_SIDS.has(principalId) || hasRid(principalId, DEFAULT_REPLICATION_HOLDER_RIDS);
|
|
568
|
+
if (isDefault && !resolved.includeDefaultPrivileged)
|
|
569
|
+
continue;
|
|
570
|
+
const principal = graph.nodes.get(principalId);
|
|
571
|
+
if (!principal)
|
|
572
|
+
continue;
|
|
573
|
+
if (principal.kind === "Computer" && isDomainController(graph, principal) && !resolved.includeDefaultPrivileged) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const inherited = [...reachableTo(graph, [principalId], resolved.maxDepth, {
|
|
577
|
+
...resolved.traversal,
|
|
578
|
+
allowedEdgeKinds: ["MemberOf"]
|
|
579
|
+
}).keys()].filter((id) => id !== principalId);
|
|
580
|
+
findings.push(finding("dcsync-principals", slug(domain.objectId, principalId), "critical", `${principal.label} can DCSync ${domain.label}`, `${principal.label} holds ${viaDcsyncEdge ? "a DCSync right" : "both GetChanges and GetChangesAll"} on ${domain.label} and is not a domain controller or a default replication holder. That is enough to replicate every credential in the domain, including the krbtgt hash, which enables Golden Ticket forgery. ${inherited.length} principal(s) inherit this through group membership.`, `Remove the DS-Replication-Get-Changes and DS-Replication-Get-Changes-All ACEs for ${principal.label} from the domain head. Replication rights belong to domain controllers and to directory-sync service accounts that are managed as Tier 0.`, singleEdgePaths(graph, entry.edges, resolved.maxPathsPerFinding), [principalId, ...inherited.slice(0, 100)], {
|
|
581
|
+
domainId: domain.objectId,
|
|
582
|
+
grantedBy: [...entry.kinds],
|
|
583
|
+
inheritedByCount: inherited.length
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return findings;
|
|
588
|
+
}
|
|
589
|
+
function findAclAbuseChains(graph, opts = {}) {
|
|
590
|
+
const resolved = resolveOptions(opts);
|
|
591
|
+
const highValue = highValueTargetIds(graph, opts.highValueIds);
|
|
592
|
+
if (highValue.size === 0)
|
|
593
|
+
return [];
|
|
594
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const kind of DANGEROUS_ACL_EDGES) {
|
|
596
|
+
for (const edgeIndex of graph.edgesByKind.get(kind) ?? []) {
|
|
597
|
+
const edge = graph.edges[edgeIndex];
|
|
598
|
+
if (!highValue.has(edge.target))
|
|
599
|
+
continue;
|
|
600
|
+
if (highValue.has(edge.source))
|
|
601
|
+
continue;
|
|
602
|
+
const source = graph.nodes.get(edge.source);
|
|
603
|
+
if (!source)
|
|
604
|
+
continue;
|
|
605
|
+
if (!resolved.includeDefaultPrivileged && hasRid(edge.source, TIER0_DOMAIN_RIDS))
|
|
606
|
+
continue;
|
|
607
|
+
const bucket = bySource.get(edge.source);
|
|
608
|
+
if (bucket)
|
|
609
|
+
bucket.push(edgeIndex);
|
|
610
|
+
else
|
|
611
|
+
bySource.set(edge.source, [edgeIndex]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const findings = [];
|
|
615
|
+
for (const [sourceId, edgeIndices] of bySource) {
|
|
616
|
+
if (findings.length >= resolved.maxFindingsPerAnalyzer)
|
|
617
|
+
break;
|
|
618
|
+
const source = graph.nodes.get(sourceId);
|
|
619
|
+
const kinds = [...new Set(edgeIndices.map((index) => graph.edges[index].kind))];
|
|
620
|
+
const targets = [...new Set(edgeIndices.map((index) => graph.edges[index].target))];
|
|
621
|
+
const inherited = [...reachableTo(graph, [sourceId], resolved.maxDepth, {
|
|
622
|
+
...resolved.traversal,
|
|
623
|
+
allowedEdgeKinds: ["MemberOf"]
|
|
624
|
+
}).keys()].filter((id) => id !== sourceId);
|
|
625
|
+
findings.push(finding("acl-abuse-chains", slug(sourceId), "high", `${source.label} holds ${kinds.join("/")} over ${targets.length} Tier-0 object(s)`, `${source.label} is not a Tier-0 principal yet holds ${kinds.join(", ")} over ${targets.length} high-value object(s). Any of these rights can be converted into membership or control of the target in a single step, so compromising ${source.label} is equivalent to compromising Tier 0. ${inherited.length} principal(s) inherit these rights through group membership.`, `Remove the ACEs granting ${source.label} control over Tier-0 objects, or promote and manage it as a Tier-0 principal itself. Enable AdminSDHolder protection on the affected objects so the ACL is restored automatically if it is loosened again.`, singleEdgePaths(graph, edgeIndices, resolved.maxPathsPerFinding), [sourceId, ...inherited.slice(0, 100)], {
|
|
626
|
+
rightKinds: kinds,
|
|
627
|
+
targetIds: targets.slice(0, 50),
|
|
628
|
+
inheritedByCount: inherited.length
|
|
629
|
+
}));
|
|
630
|
+
}
|
|
631
|
+
return findings;
|
|
632
|
+
}
|
|
633
|
+
function findAdcsEscalation(graph, opts = {}) {
|
|
634
|
+
const resolved = resolveOptions(opts);
|
|
635
|
+
const findings = [];
|
|
636
|
+
const relevantKinds = [];
|
|
637
|
+
for (const kind of graph.edgesByKind.keys()) {
|
|
638
|
+
if (/^ADCSESC/i.test(kind) || ADCS_ABUSE_EDGES.includes(kind))
|
|
639
|
+
relevantKinds.push(kind);
|
|
640
|
+
}
|
|
641
|
+
relevantKinds.sort();
|
|
642
|
+
for (const kind of relevantKinds) {
|
|
643
|
+
if (findings.length >= resolved.maxFindingsPerAnalyzer)
|
|
644
|
+
break;
|
|
645
|
+
const edgeIndices = graph.edgesByKind.get(kind) ?? [];
|
|
646
|
+
if (edgeIndices.length === 0)
|
|
647
|
+
continue;
|
|
648
|
+
const principals = [...new Set(edgeIndices.map((index) => graph.edges[index].source))];
|
|
649
|
+
const targets = [...new Set(edgeIndices.map((index) => graph.edges[index].target))];
|
|
650
|
+
const isEsc = /^ADCSESC/i.test(kind);
|
|
651
|
+
findings.push(finding("adcs-escalation", slug(kind), isEsc || kind === "GoldenCert" ? "critical" : "high", `ADCS escalation available: ${kind}`, `${principals.length} principal(s) hold the ${kind} relationship over ${targets.length} certificate-services object(s). This lets an attacker obtain a certificate that authenticates as a higher-privileged principal, which survives a password reset and is usable until the certificate expires or is revoked.`, `Audit the affected certificate templates and CAs. Remove caller-supplied subject names (CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT), require manager approval or authorised signatures for client-authentication templates, restrict enrollment rights to the principals that need them, and clear EDITF_ATTRIBUTESUBJECTALTNAME2 on the CA. Revoke certificates already issued under the vulnerable configuration.`, singleEdgePaths(graph, edgeIndices, resolved.maxPathsPerFinding), principals.slice(0, 100), {
|
|
652
|
+
edgeKind: kind,
|
|
653
|
+
edgeCount: edgeIndices.length,
|
|
654
|
+
targetIds: targets.slice(0, 50)
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
return findings;
|
|
658
|
+
}
|
|
659
|
+
var AD_ANALYZERS = [
|
|
660
|
+
findPathsToDomainAdmin,
|
|
661
|
+
findDcsyncPrincipals,
|
|
662
|
+
findUnconstrainedDelegation,
|
|
663
|
+
findAdcsEscalation,
|
|
664
|
+
findAclAbuseChains,
|
|
665
|
+
findKerberoastablePaths
|
|
666
|
+
];
|
|
667
|
+
|
|
668
|
+
export {
|
|
669
|
+
AD_EDGE_TECHNIQUES,
|
|
670
|
+
describeEdgeTechnique,
|
|
671
|
+
buildAttackPath,
|
|
672
|
+
shortestPaths,
|
|
673
|
+
reachableFrom,
|
|
674
|
+
reachableTo,
|
|
675
|
+
nodeOrStub,
|
|
676
|
+
domainAdminGroupIds,
|
|
677
|
+
highValueTargetIds,
|
|
678
|
+
findPathsToDomainAdmin,
|
|
679
|
+
findKerberoastablePaths,
|
|
680
|
+
findUnconstrainedDelegation,
|
|
681
|
+
findDcsyncPrincipals,
|
|
682
|
+
findAclAbuseChains,
|
|
683
|
+
findAdcsEscalation,
|
|
684
|
+
AD_ANALYZERS
|
|
685
|
+
};
|