1use adhammer_core::sid::Sid;
9use adhammer_core::snapshot::Snapshot;
10use adhammer_core::AdObject;
11use petgraph::graph::{DiGraph, NodeIndex};
12use petgraph::visit::EdgeRef;
13use std::collections::HashMap;
14use windows_sddl::{rights, AccessMask, AceType};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum EdgeKind {
18 MemberOf,
19 Owns,
20 WriteDacl,
21 WriteOwner,
22 GenericAll,
23 GenericWrite,
24 ForceChangePassword,
25 AddMember,
26 AddKeyCredential, WriteRbcd,
28 DcsyncRight,
29}
30
31impl EdgeKind {
32 pub fn weight(self) -> u32 {
34 match self {
35 EdgeKind::MemberOf => 0,
36 EdgeKind::GenericAll | EdgeKind::Owns => 1,
37 EdgeKind::WriteDacl | EdgeKind::WriteOwner => 1,
38 EdgeKind::ForceChangePassword | EdgeKind::AddKeyCredential => 1,
39 EdgeKind::AddMember => 1,
40 EdgeKind::WriteRbcd => 2,
41 EdgeKind::GenericWrite => 2,
42 EdgeKind::DcsyncRight => 0,
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
48pub struct Node {
49 pub sid: Sid,
50 pub label: String,
51 pub tier0: bool,
52}
53
54pub struct ControlGraph {
55 g: DiGraph<Node, EdgeKind>,
56 by_sid: HashMap<String, NodeIndex>,
57}
58
59impl ControlGraph {
60 pub fn build(snap: &Snapshot) -> Self {
61 let mut cg = ControlGraph {
62 g: DiGraph::new(),
63 by_sid: HashMap::new(),
64 };
65
66 for o in &snap.objects {
68 if let Some(sid) = o.bin1("objectSid").and_then(Sid::from_bytes) {
69 let tier0 = is_tier0(snap, &sid);
70 cg.ensure(sid, label_of(o), tier0);
71 }
72 }
73
74 for o in &snap.objects {
76 let Some(dst) = o.bin1("objectSid").and_then(Sid::from_bytes) else {
77 continue;
78 };
79 cg.add_membership_edges(snap, o, &dst);
80 cg.add_acl_edges(snap, o, &dst);
81 cg.add_rbcd_edge(o, &dst);
82 }
83 cg
84 }
85
86 fn ensure(&mut self, sid: Sid, label: String, tier0: bool) -> NodeIndex {
87 if let Some(&ix) = self.by_sid.get(&sid.to_string()) {
88 if tier0 {
89 self.g[ix].tier0 = true;
90 }
91 return ix;
92 }
93 let key = sid.to_string();
94 let ix = self.g.add_node(Node { sid, label, tier0 });
95 self.by_sid.insert(key, ix);
96 ix
97 }
98
99 fn node_for(&mut self, sid: Sid) -> NodeIndex {
100 self.ensure(sid.clone(), sid.to_string(), false)
101 }
102
103 fn add_membership_edges(&mut self, snap: &Snapshot, o: &AdObject, self_sid: &Sid) {
104 let me = self.node_for(self_sid.clone());
106 for group_dn in o.all("memberOf") {
107 if let Some(g) = snap
108 .by_dn(group_dn)
109 .and_then(|g| g.bin1("objectSid"))
110 .and_then(Sid::from_bytes)
111 {
112 let gx = self.node_for(g);
113 self.g.add_edge(me, gx, EdgeKind::MemberOf);
114 }
115 }
116 }
117
118 fn add_rbcd_edge(&mut self, o: &AdObject, self_sid: &Sid) {
119 if let Some(raw) = o.bin1("msDS-AllowedToActOnBehalfOfOtherIdentity") {
121 if let Ok(sd) = windows_sddl::parse(raw) {
122 let target = self.node_for(self_sid.clone());
123 for ace in sd
124 .dacl
125 .iter()
126 .flat_map(|d| &d.aces)
127 .filter(|a| a.is_allow())
128 {
129 let src = self.node_for(ace.trustee.clone());
130 self.g.add_edge(src, target, EdgeKind::WriteRbcd);
131 }
132 }
133 }
134 }
135
136 fn add_acl_edges(&mut self, _snap: &Snapshot, o: &AdObject, self_sid: &Sid) {
137 let Some(raw) = o.bin1("nTSecurityDescriptor") else {
138 return;
139 };
140 let Ok(sd) = windows_sddl::parse(raw) else {
141 return;
142 };
143 let target = self.node_for(self_sid.clone());
144
145 if let Some(owner) = sd.owner {
146 if !owner.is_well_known() {
147 let src = self.node_for(owner);
148 self.g.add_edge(src, target, EdgeKind::Owns);
149 }
150 }
151
152 for ace in sd.dacl.iter().flat_map(|d| &d.aces) {
153 if ace.ace_type == AceType::AccessDenied || ace.ace_type == AceType::AccessDeniedObject
154 {
155 continue; }
157 if ace.trustee.is_well_known() {
158 continue;
159 }
160 let kinds = classify(ace);
161 if kinds.is_empty() {
162 continue;
163 }
164 let src = self.node_for(ace.trustee.clone());
165 for k in kinds {
166 self.g.add_edge(src, target, k);
167 }
168 }
169 }
170
171 pub fn paths_to_tier0(&self) -> Vec<AttackPath> {
173 use petgraph::algo::dijkstra;
174 let rev = petgraph::visit::Reversed(&self.g);
176 let mut out = Vec::new();
177 for tix in self.g.node_indices().filter(|&i| self.g[i].tier0) {
178 let dist = dijkstra(rev, tix, None, |e| *edge_weight(e.weight()));
179 for (src, cost) in dist {
180 if src == tix || self.g[src].tier0 {
181 continue;
182 }
183 out.push(AttackPath {
184 principal: self.g[src].label.clone(),
185 principal_sid: self.g[src].sid.to_string(),
186 target: self.g[tix].label.clone(),
187 cost,
188 });
189 }
190 }
191 out.sort_by_key(|p| p.cost);
192 out
193 }
194
195 pub fn stats(&self) -> (usize, usize) {
196 (self.g.node_count(), self.g.edge_count())
197 }
198
199 pub fn direct_edges_to_tier0(&self, kind: EdgeKind) -> Vec<(String, String)> {
201 let mut out = Vec::new();
202 for e in self.g.edge_indices() {
203 if self.g[e] != kind {
204 continue;
205 }
206 let (src, dst) = self.g.edge_endpoints(e).unwrap();
207 if self.g[dst].tier0 && !self.g[src].tier0 {
208 out.push((self.g[src].label.clone(), self.g[dst].label.clone()));
209 }
210 }
211 out.sort();
212 out.dedup();
213 out
214 }
215}
216
217fn edge_weight(k: &EdgeKind) -> &u32 {
218 match k {
220 EdgeKind::MemberOf | EdgeKind::DcsyncRight => &0,
221 EdgeKind::WriteRbcd | EdgeKind::GenericWrite => &2,
222 _ => &1,
223 }
224}
225
226#[derive(Clone, Debug, serde::Serialize)]
227pub struct AttackPath {
228 pub principal: String,
229 pub principal_sid: String,
230 pub target: String,
231 pub cost: u32,
232}
233
234fn classify(ace: &windows_sddl::Ace) -> Vec<EdgeKind> {
236 let m = ace.mask;
237 let mut v = Vec::new();
238 if m.contains(AccessMask::GENERIC_ALL) {
239 v.push(EdgeKind::GenericAll);
240 }
241 if m.contains(AccessMask::WRITE_DAC) {
242 v.push(EdgeKind::WriteDacl);
243 }
244 if m.contains(AccessMask::WRITE_OWNER) {
245 v.push(EdgeKind::WriteOwner);
246 }
247 if m.contains(AccessMask::GENERIC_WRITE) {
248 v.push(EdgeKind::GenericWrite);
249 }
250 let broad = ace.object_type.is_none();
253 if m.contains(AccessMask::CONTROL_ACCESS) {
254 match &ace.object_type {
255 Some(g) if rights::FORCE_CHANGE_PASSWORD.matches(g) => {
256 v.push(EdgeKind::ForceChangePassword)
257 }
258 Some(g) if rights::is_dcsync_right(g) => v.push(EdgeKind::DcsyncRight),
259 None => v.push(EdgeKind::GenericAll), _ => {}
261 }
262 }
263 if m.contains(AccessMask::WRITE_PROP) {
264 match &ace.object_type {
265 Some(g) if rights::MEMBER_ATTR.matches(g) => v.push(EdgeKind::AddMember),
266 Some(g) if rights::KEY_CREDENTIAL_LINK.matches(g) => v.push(EdgeKind::AddKeyCredential),
267 Some(g) if rights::RBCD_ATTR.matches(g) => v.push(EdgeKind::WriteRbcd),
268 None if broad => v.push(EdgeKind::GenericWrite),
269 _ => {}
270 }
271 }
272 v
273}
274
275fn is_tier0(snap: &Snapshot, sid: &Sid) -> bool {
276 use adhammer_core::sid::rid;
277 let Some(rid) = sid.rid() else { return false };
278 if matches!(
280 rid,
281 rid::DOMAIN_ADMINS
282 | rid::ENTERPRISE_ADMINS
283 | rid::SCHEMA_ADMINS
284 | rid::ADMINISTRATOR
285 | rid::KRBTGT
286 | rid::DOMAIN_CONTROLLERS
287 ) {
288 if let Some(dsid) = &snap.domain.domain_sid {
290 let prefix = &sid.sub_authorities[..sid.sub_authorities.len().saturating_sub(1)];
291 if prefix == &dsid.sub_authorities[..] {
292 return true;
293 }
294 }
295 }
296 sid.identifier_authority == 5
298 && sid.sub_authorities.first() == Some(&32)
299 && sid.rid() == Some(rid::ADMINISTRATORS_BUILTIN)
300}
301
302fn label_of(o: &AdObject) -> String {
303 o.one("sAMAccountName")
304 .map(String::from)
305 .unwrap_or_else(|| o.dn.clone())
306}