feat: add acl_abuse LDAP module for ACE chain enumeration#1176
feat: add acl_abuse LDAP module for ACE chain enumeration#1176sup3rDav3 wants to merge 12 commits intoPennyw0rth:mainfrom
Conversation
- Detects WriteDACL, GenericAll, WriteOwner on AD objects - Detects ForceChangePassword via extended rights GUID - Detects WriteProperty on member, servicePrincipalName, msDS-KeyCredentialLink - Detects DS-Replication-Get-Changes and Get-Changes-All (DCSync path) - Includes domain root object scanning for replication rights - Attack suggestions printed inline for each finding - Optional JSON export via OUTPUT_FILE - Tested against Windows Server 2022 domain controller
|
Nice ! Could you replace |
|
Thanks for the PR! Will test it when I have reviewed the rest of the PRs that have piled up, but I'll do a quick code review for now. |
NeffIsBack
left a comment
There was a problem hiding this comment.
Quick code review, without diving too deep into the logic
| @@ -0,0 +1,393 @@ | |||
| from ldap3.protocol.microsoft import security_descriptor_control | |||
There was a problem hiding this comment.
As @azoxlpf said, please replace it with the impacket equivalent
| context.log.fail( | ||
| "Could not resolve target principal — check TARGET_USER or auth credentials" | ||
| ) |
There was a problem hiding this comment.
Please do such logs in one line. That just wastes space.
There was a problem hiding this comment.
Please do such logs in one line. That just wastes space.
Fixed. All multi-line log calls consolidated to single lines.
| context.log.display( | ||
| f"Resolved {len(principal_sids)} principal SID(s), enumerating ACEs..." | ||
| ) |
There was a problem hiding this comment.
I think I got all of the multi-line log calls.
| self.context.log.fail( | ||
| "Could not determine current username — use TARGET_USER option" | ||
| ) |
| user_entries = self._ldap_search( | ||
| f"(&(objectClass=user)(sAMAccountName={username}))", | ||
| ["objectSid", "memberOf", "distinguishedName", "sAMAccountName"], | ||
| ) |
There was a problem hiding this comment.
Fixed. Using conn.search() directly for user and group lookups in _resolve_principal_sids.
| "object_classes": obj_classes | ||
| if isinstance(obj_classes, list) | ||
| else [obj_classes], |
There was a problem hiding this comment.
Logic please in one line, otherwise this is confusing
There was a problem hiding this comment.
Fixed. Consolidated the object_classes ternary onto a single line.
| self.context.log.success( | ||
| f"Found {total} abusable ACE(s) — {critical} CRITICAL, {high} HIGH" | ||
| ) |
| def _ldap_search(self, filter_str, attributes, controls=None): | ||
| try: | ||
| response = self.conn.search( | ||
| searchFilter=filter_str, | ||
| attributes=attributes, | ||
| searchControls=controls, | ||
| ) | ||
| if not response: | ||
| return [] | ||
| return [e for e in response if isinstance(e, SearchResultEntry)] | ||
| except Exception as e: | ||
| self.context.log.debug(f"LDAP search failed ({filter_str}): {e}") | ||
| return [] | ||
|
|
||
| def _parse_attributes(self, entry): | ||
| attrs = {} | ||
| try: | ||
| for attr in entry["attributes"]: | ||
| name = str(attr["type"]) | ||
| vals = attr["vals"] | ||
| parsed_vals = [] | ||
| for v in vals: | ||
| try: | ||
| parsed_vals.append(bytes(v)) | ||
| except Exception: | ||
| parsed_vals.append(str(v)) | ||
| if len(parsed_vals) == 1: | ||
| attrs[name] = parsed_vals[0] | ||
| else: | ||
| attrs[name] = parsed_vals | ||
| except Exception as e: | ||
| self.context.log.debug(f"Failed to parse entry attributes: {e}") | ||
| return attrs |
There was a problem hiding this comment.
Can both be removed after reworking the rest.
There was a problem hiding this comment.
Done. Removed both _ldap_search and _parse_attributes. Now using conn.search() directly and parse_result_attributes from nxc.parsers.ldap_results throughout.
| return ldaptypes.LDAP_SID( | ||
| data=ace["Ace"]["Sid"].getData() | ||
| ).formatCanonical() |
| netexec nfs TARGET_HOST -u "" -p "" --enum-shares | ||
| netexec nfs TARGET_HOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt | ||
| netexec nfs TARGET_HOST -u "" -p "" --put-file ../test.txt /NFStest/test | ||
| nxc ldap {host} -u {user} -p {pass} -M acl_abuse |
There was a problem hiding this comment.
That's at the wrong position. Please sort it into its category
There was a problem hiding this comment.
In general, this has the wrong syntax
There was a problem hiding this comment.
Fixed. Removed the incorrectly placed entry and added it to the LDAP Modules section in alphabetical order with the correct syntax.
Done. Replaced security_descriptor_control from ldap3 with SDFlagsControl from impacket.ldap.ldapasn1. All detections verified working against Windows Server 2022. |
…) and parse_result_attributes directly
|
All feedback has been addressed, thank you for the review.
|
- Adds --shares-depth argument to SMB protocol (default: 0, root only) - When depth > 0, recursively checks subdirectories for write access - Only runs subdir check when root write check fails (no performance impact by default) - Reports writable subdirectories inline e.g. READ,WRITE (ad.local\scripts) - Respects --no-write-check flag - Compatible with --shares write filter - Fixes: write permissions were only checked on top-level share directories - Tested against Windows Server 2022 - detected writable SYSVOL\scripts subdir Closes Pennyw0rth#1186
…on checking" This reverts commit 9f03690.
Description
Adds a new LDAP module
acl_abusethat enumerates abusable ACEs from a targetprincipal to other AD objects, surfacing attack paths with inline attack suggestions.
Detections include:
msDS-KeyCredentialLink (shadow credentials path)
This module was developed with the assistance of Claude (Anthropic) via claude.ai.
Claude assisted with debugging LDAP/impacket attribute access and ACE mask value corrections. All code was reviewed, tested, and verified by the author against a live Windows Server 2022 domain controller.
Type of change
Setup guide for the review
Environment:
Target:
Test setup — run on DC as Domain Admin:
Run the module:
Expected output:
Screenshot:
References