Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .tickets/_TICKETS-BY-STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Regenerate after any status change rather than editing by hand.
`.gitattributes` marks this file `merge=ours`, which needs a one-time
`git config merge.ours.driver true` per clone.

**150 tickets** — 6 in progress · 29 open · 115 closed
**151 tickets** — 6 in progress · 30 open · 115 closed

## In progress (6)

Expand All @@ -18,7 +18,7 @@ Regenerate after any status change rather than editing by hand.
| [`con-907c`](con-907c.md) | 2 | task | Rebaseline fixture expected values on the full lab + add host-level SEC assertions | testing, integration, fixtures, lab |
| [`ope-1f0f`](ope-1f0f.md) | 2 | task | Code-Quality Pass: Conditional Logging, Exception Handling, Variable Scope, Linting & Cleanup -type chore -priority 2 -assignee Mayyhem -tags sccm,... | — |

## Open (29)
## Open (30)

| Ticket | P | Type | Title | Tags |
|---|---|---|---|---|
Expand All @@ -31,6 +31,7 @@ Regenerate after any status change rather than editing by hand.
| [`con-0289`](con-0289.md) | 2 | bug | Integration fixtures cannot be gated on possible-edges, so low-priv + --disable-possible-edges reports 4 false failures | sccm, integration, fixtures, possible-edges |
| [`con-0394`](con-0394.md) | 2 | task | Positively identify a secondary site or site server as a plain domain user | sccm, lowpriv, secondary, discovery |
| [`con-2ca2`](con-2ca2.md) | 2 | bug | _safe silently swallows BinderException, turning transform bugs into no-ops | preproc, diagnostics, transforms, testing |
| [`con-7b9e`](con-7b9e.md) | 2 | bug | Fix System Management ACL Full Control filtering in deprecated PowerShell collector | powershell, sccm, ldap, acl |
| [`con-c542`](con-c542.md) | 2 | task | Tag fixture cases from measured privileged/unprivileged A/B | testing, integration, fixtures, lowpriv |
| [`con-d857`](con-d857.md) | 2 | task | Add TAKEOVER from parent CAS to child primary as possible/assumed edge | — |
| [`ope-1172`](ope-1172.md) | 2 | task | Test different port / named instance | — |
Expand Down
20 changes: 20 additions & 0 deletions .tickets/con-7b9e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
id: con-7b9e
status: open
deps: []
links: []
created: 2026-10-06T01:06:21Z
type: bug
priority: 2
tags: [powershell, sccm, ldap, acl]
---

# Fix System Management ACL Full Control filtering in deprecated PowerShell collector

`powershell_deprecated/ConfigManBearPig.ps1` incorrectly treats some System Management container ACEs as Full Control. In the AD-module path near line 3459, the `Where-Object` filter is missing `-and` before the `NT AUTHORITY` exclusion, so its last expression can admit Deny and non-GenericAll ACEs. Both the AD-module and DirectoryServices paths also need to exclude inherit-only ACEs and rights scoped by `ObjectType`; Exchange ACEs of these forms can otherwise make Exchange servers appear to be SCCM collection targets. See ConfigManBearPig PR #14 for the Python collector's related fix.

## Acceptance Criteria

- Both PowerShell ACL paths retain effective Allow Full Control on the container, including a container-inherit ACE and an object ACE whose only GUID is `InheritedObjectType` and that is not inherit-only. `InheritedObjectType` limits child inheritance; by itself it does not remove rights on the current container.
- Deny, non-Full-Control, inherit-only, and `ObjectType`-scoped ACEs do not produce Full Control principals or targets. Exclude LOCAL SYSTEM, BUILTIN\Administrators, and CREATOR OWNER from SCCM target inference.
- A regression test covers the AD-module filter's missing `-and` and the Exchange/SCCM mixed-ACL case. A lab check confirms Exchange servers are no longer added solely through this ACL while genuine SCCM site servers still are.
6 changes: 6 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ Two more pieces wire discovery into this loop:
while the snapshot returns the target only once. If a RemoteRegistry worker has no registered target,
it warns and emits the available rows without a host AD SID.

The System Management DACL parser supplies another gate before target registration: inherit-only and
`ObjectType`-scoped ACEs cannot establish Full Control on the container. `InheritedObjectType` alone only
limits which children inherit an ACE, so an otherwise effective Full Control ACE still feeds group
expansion and target discovery.

One discovery path recurses on its own, one level below `register_target`:
[`_expand_group_targets`](src/openhound_sccm/collectors/ldap.py#L618-L661) (ope-e191). When
`ldap_system_management_dacl` finds a **group** holding GenericAll on the System Management container,
Expand Down Expand Up @@ -2112,6 +2117,7 @@ took a separate, manual step outside `openhound collect sccm`.

| Date | Change |
|---|---|
| 2026-10-05 | **System Management ACL scope preserves effective inheritance-type ACEs** (section 3). The DACL parser skips inherit-only and ObjectType-scoped grants, but an InheritedObjectType GUID alone limits child inheritance and does not remove Full Control on the container. A regression test distinguishes that case from inherit-only; a DC-only lab run with disposable Exchange-like and SCCM principals confirmed DACL rows, GenericAll edges, and target registration. |
| 2026-09-30 | **Added opt-in `--http-proxy` on the PR #11 branch.** Direct HTTP remains the default. An explicit HTTP proxy or `auto` routes AdminService and HTTP role probes only; `auto` reads environment and static Windows registry proxy settings and honors bypasses while the Requests session keeps `trust_env=False` to avoid ambient `.netrc` credentials. The option cannot be combined with the process-wide SOCKS5 `--proxy`. Proxy 407 responses produce a visible warning, and a failed direct HTTP connection suggests `auto` when environment or static Windows proxy settings apply to that target. |
| 2026-09-30 | **Bounded verbose output from local client logs without losing host discovery** (§7). The scraper keeps reading CCM/ccmsetup logs on Windows even when `root\CCM` is absent. It logs one full URL and UNC sample per host, counts suppressed matches in one summary, and leaves the target set unchanged. A test covers repeated references across both log directories and discovery from logs left by a former client. |
| 2026-09-30 | **Preserved target identity across hostname upgrades and incomplete RemoteRegistry discovery** (§3). A queued short hostname remains an alias when its target is upgraded to an FQDN; snapshots deduplicate the aliases. Registry lookups use normalized hostnames and emit partial rows with a warning if a target is genuinely absent, instead of failing the phase. Regression tests cover mixed-case names, the queued-name upgrade, and missing targets. |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1685,6 +1685,8 @@ Example (`SCCM_AdminsReplicatedTo`, abbreviated):

Links each AD principal that holds Full Control over the System Management container to the [`Container`](#container) node for it. Wires up `ldap_system_management_dacl`, which the collector always parsed for its DACL but which fed no edge at all before the low-privilege work — this is "who can control SCCM via AD", the attack surface the container's ACL actually represents.

The LDAP parser excludes inherit-only and `ObjectType`-scoped ACEs. An `InheritedObjectType` GUID alone limits child inheritance, so an otherwise effective Full Control ACE still counts on this container.

- **Start:** the Full-Control principal (any AD SID — `Computer`, `User`, or `Group`)
- **End:** [`Container`](#container) (the System Management container)
- **Traversable:** yes — standard BloodHound base kind, in `TRAVERSABLE_EDGE_KINDS` alongside `GenericAll`'s usual SharpHound meaning
Expand Down
54 changes: 47 additions & 7 deletions powershell_deprecated/ConfigManBearPig.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2960,6 +2960,50 @@ function Get-MssqlEpaSettingsViaRemoteRegistry {
}
}

function Test-SystemManagementFullControlRule {
param([System.DirectoryServices.ActiveDirectoryAccessRule]$Rule)

if ($Rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow) {
Write-LogMessage Verbose "Skipping non-Allow System Management ACE for $($Rule.IdentityReference)"
return $false
}

$rights = [int]$Rule.ActiveDirectoryRights
$genericAll = [int][System.DirectoryServices.ActiveDirectoryRights]::GenericAll
if ((($rights -band $genericAll) -ne $genericAll) -and (($rights -band 0x10000000) -eq 0)) {
Write-LogMessage Verbose "Skipping System Management ACE without Full Control for $($Rule.IdentityReference)"
return $false
}

if (([int]$Rule.PropagationFlags -band [int][System.Security.AccessControl.PropagationFlags]::InheritOnly) -ne 0) {
Write-LogMessage Verbose "Skipping inherit-only System Management ACE for $($Rule.IdentityReference)"
return $false
}

if (([int]$Rule.ObjectFlags -band [int][System.Security.AccessControl.ObjectAceFlags]::ObjectAceTypePresent) -ne 0) {
Write-LogMessage Verbose "Skipping object-type-scoped System Management ACE for $($Rule.IdentityReference)"
return $false
}

$identity = $Rule.IdentityReference.ToString()
if ($identity -like 'NT AUTHORITY\*') {
Write-LogMessage Verbose "Skipping built-in System Management ACE for $identity"
return $false
}

try {
$sid = $Rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
if ($sid -in @('S-1-5-18', 'S-1-5-32-544', 'S-1-3-0')) {
Write-LogMessage Verbose "Skipping built-in System Management ACE for $identity"
return $false
}
} catch {
Write-LogMessage Verbose "Could not translate System Management ACE principal $identity to a SID: $_"
}

return $true
}

function Invoke-LDAPCollection {
Write-LogMessage Info "Starting LDAP collection..."

Expand Down Expand Up @@ -3457,9 +3501,7 @@ function Invoke-LDAPCollection {
if ($script:ADModuleAvailable) {
$acl = Get-Acl -Path "AD:\$systemManagementDN"
$genericAllAccounts = $acl.Access | Where-Object {
$_.AccessControlType -eq "Allow" -and
$_.ActiveDirectoryRights -eq "GenericAll"
$_.IdentityReference -notlike "NT AUTHORITY\*"
Test-SystemManagementFullControlRule -Rule $_
}

foreach ($account in $genericAllAccounts) {
Expand Down Expand Up @@ -3531,9 +3573,7 @@ function Invoke-LDAPCollection {
$accessRules = $ntSecurityDescriptor.GetAccessRules($true, $true, [System.Security.Principal.NTAccount])

foreach ($rule in $accessRules) {
if ($rule.AccessControlType -eq "Allow" -and
$rule.ActiveDirectoryRights -match "GenericAll" -and
$rule.IdentityReference -notlike "NT AUTHORITY\*") {
if (Test-SystemManagementFullControlRule -Rule $rule) {

Write-LogMessage Success "Found principal with GenericAll on System Management container: $($rule.IdentityReference)"

Expand Down Expand Up @@ -9947,7 +9987,7 @@ try {
$script:Edges = @()

# Script version information
$script:ScriptVersion = "1.2"
$script:ScriptVersion = "1.3"
$script:ScriptName = "ConfigManBearPig"

if ($Version) {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
name = "configmanbearpig"
# Version comes from the git tag via hatch-vcs (see [tool.hatch.version] below).
dynamic = ["version"]
description = "ConfigManBearPig 2.0 — OpenHound/BloodHound OpenGraph collector for Microsoft Configuration Manager (SCCM/MECM)"
description = "ConfigManBearPig 2.0.2 — OpenHound/BloodHound OpenGraph collector for Microsoft Configuration Manager (SCCM/MECM)"
readme = "README.md"
requires-python = "<3.15,>=3.13"
authors = [
Expand Down
Loading