├── LICENSE ├── README.md ├── ShadowHound-ADM.ps1 ├── ShadowHound-DS.ps1 ├── logo.png └── split_output.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Friends & Security 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](./logo.png) 2 | 3 | # ShadowHound 4 | 5 | ShadowHound is a set of PowerShell scripts for Active Directory enumeration without the need for introducing known-malicious binaries like SharpHound. It leverages native PowerShell capabilities to minimize detection risks and offers two methods for data collection: 6 | 7 | - **ShadowHound-ADM.ps1**: Uses the Active Directory module (ADWS). 8 | - **ShadowHound-DS.ps1**: Utilizes direct LDAP queries via `DirectorySearcher`. 9 | 10 | ## Blog Post 11 | 12 | For more details and context, check out the [blog post](https://blog.fndsec.net/2024/11/25/shadowhound/). 13 | 14 | Huge thanks to [Itay Yashar](https://www.linkedin.com/in/itay-yashar-55586a163/) for the assistance with the research & development. 15 | 16 | ## Scripts Overview 17 | 18 | ### ShadowHound-ADM.ps1 19 | 20 | - **Method**: Active Directory module (`Get-ADObject` via ADWS). 21 | - **Usage Scenario**: When the AD module is available and ADWS is accessible. 22 | - **Features**: 23 | - Handles large domains with `-SplitSearch`, `-Recurse`, and `-LetterSplitSearch` options. 24 | - Enumerates certificates with the `-Certificates` flag. 25 | 26 | ### ShadowHound-DS.ps1 27 | 28 | - **Method**: Direct LDAP queries using `DirectorySearcher`. 29 | - **Usage Scenario**: Environments where the AD module isn't available or LDAP is preferred. 30 | - **Features**: 31 | - Enumerates certificates with the `-Certificates` flag. 32 | - Supports alternate credentials with the `-Credential` parameter. 33 | 34 | ## Usage Examples 35 | 36 | ### Basic Enumeration 37 | 38 | #### ShadowHound-ADM.ps1 39 | 40 | ```powershell 41 | # Basic usage 42 | ShadowHound-ADM -OutputFilePath "C:\Results\ldap_output.txt" 43 | 44 | # Specify a domain controller and custom LDAP filter 45 | ShadowHound-ADM -Server "dc.domain.local" -OutputFilePath "C:\Results\ldap_output.txt" -LdapFilter "(objectClass=user)" 46 | 47 | # Use alternate credentials 48 | $cred = Get-Credential 49 | ShadowHound-ADM -OutputFilePath "C:\Results\ldap_output.txt" -Credential $cred -SearchBase "DC=domain,DC=local" 50 | ``` 51 | 52 | #### ShadowHound-DS.ps1 53 | 54 | ```powershell 55 | # Basic usage 56 | ShadowHound-DS -OutputFile "C:\Results\ldap_output.txt" 57 | 58 | # Specify a domain controller 59 | ShadowHound-DS -Server "dc.domain.local" -OutputFile "C:\Results\ldap_output.txt" 60 | 61 | # Use a custom LDAP filter 62 | ShadowHound-DS -OutputFile "C:\Results\ldap_output.txt" -LdapFilter "(objectClass=computer)" 63 | ``` 64 | 65 | ### Enumerating Certificates 66 | 67 | Both scripts support enumerating certificate-related objects for those juicy ADCS vectors: 68 | 69 | ```powershell 70 | # Using ShadowHound-ADM.ps1 71 | ShadowHound-ADM -OutputFilePath "C:\Results\cert_output.txt" -Certificates 72 | 73 | # Using ShadowHound-DS.ps1 74 | ShadowHound-DS -OutputFile "C:\Results\cert_output.txt" -Certificates 75 | ``` 76 | 77 | ### Handling Large Domains (ShadowHound-ADM.ps1) 78 | 79 | ```powershell 80 | # Split search across top-level containers with letter splitting 81 | ShadowHound-ADM -OutputFilePath "C:\Results\ldap_output.txt" -SplitSearch -LetterSplitSearch 82 | ``` 83 | 84 | - **`-SplitSearch`**: Splits the search across top-level containers. 85 | - **`-Recurse`**: Recurses into containers that fail to return results. 86 | - **`-LetterSplitSearch`**: Further splits searches by the first letter of CN. 87 | 88 | ## Converting Data for BloodHound 89 | 90 | If the ldap_output.txt you got using ShadowHound is too large for Bofhound (Memory error), you may split the ShadowHound output using split_output.py: 91 | ```bash 92 | # Split ldap_output.txt to 100 chunks which are named split_output_1.txt, split_output_2.txt and so on... 93 | # In order to provide bofhound with a folder containing ldap output, the files *must* be prefixed with "pyldapsearch". 94 | python3 split_output.py -i ldap_output.txt -o pyldapsearch_ldap -n 100 95 | 96 | # Provide Shadowhound with a folder containing the splitted output 97 | python3 bofhound.py -i ./folder -p All --parser ldapsearch 98 | 99 | ``` 100 | 101 | After collecting data, use [BofHound](https://github.com/coffeegist/bofhound) to convert it into BloodHound-compatible JSON files: 102 | 103 | ```bash 104 | python3 bofhound.py -i ldap_output.txt -p All --parser ldapsearch 105 | ``` 106 | 107 | For large JSON files (>100MB), consider splitting them with tools like [ShredHound](https://github.com/ustayready/ShredHound). 108 | 109 | ## Author 110 | 111 | - **Yehuda Smirnov** 112 | - Twitter: [@yudasm_](https://twitter.com/yudasm_) 113 | - BlueSky: [@yudasm.bsky.social](https://bsky.app/profile/yudasm.bsky.social) 114 | -------------------------------------------------------------------------------- /ShadowHound-ADM.ps1: -------------------------------------------------------------------------------- 1 | function ShadowHound-ADM { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory = $false, HelpMessage = 'The domain controller to query.')] 5 | [string]$Server, 6 | 7 | [Parameter(Mandatory = $false, HelpMessage = 'Path to the output file where results will be saved.')] 8 | [ValidateNotNullOrEmpty()] 9 | [string]$OutputFilePath, 10 | 11 | [Parameter(Mandatory = $false, HelpMessage = 'LDAP filter to customize the search.')] 12 | [string]$LdapFilter = '(ObjectGuid=*)', 13 | 14 | [Parameter(Mandatory = $false, HelpMessage = 'The base DN for the search.')] 15 | [string]$SearchBase, 16 | 17 | [Parameter(Mandatory = $false, HelpMessage = 'The number of objects to include in one page for paging LDAP searches.')] 18 | [int]$PageSize = 1000, 19 | 20 | [Parameter(Mandatory = $false, HelpMessage = 'PSCredential object for alternate credentials.')] 21 | [pscredential]$Credential, 22 | 23 | [Parameter(Mandatory = $false, HelpMessage = 'Splits the search across top-level containers to handle large domains.')] 24 | [switch]$SplitSearch, 25 | 26 | [Parameter(Mandatory = $false, HelpMessage = 'Splits the search by first letter of CN to handle large domains, if the query fails, will also split the letter.')] 27 | [switch]$LetterSplitSearch, 28 | 29 | [Parameter(Mandatory = $false, HelpMessage = 'Path to a file containing a list of parsed containers.')] 30 | [string]$ParsedContainers, 31 | 32 | [Parameter(Mandatory = $false, HelpMessage = 'Recursively process containers that fail.')] 33 | [switch]$Recurse, 34 | 35 | [Parameter(Mandatory = $false, HelpMessage = 'Enumerate certificates.')] 36 | [switch]$Certificates, 37 | 38 | [Parameter(Mandatory = $false, HelpMessage = 'Display help information.')] 39 | [switch]$Help 40 | ) 41 | 42 | if ($Help) { 43 | Print-Help 44 | return 45 | } 46 | 47 | if ($Certificates -and ($SplitSearch -or $LetterSplitSearch -or $Recurse -or $ParsedContainers -or $SearchBase)) { 48 | Write-Error '[!] Certificate enumeration is done seprately from the rest of the enumeration.' 49 | return 50 | } 51 | 52 | if (-not $OutputFilePath) { 53 | Write-Error '[!] -OutputFilePath is required.' 54 | return 55 | } 56 | 57 | if ($ParsedContainers -and -not $SplitSearch) { 58 | Write-Error '[!] Cannot parse containers if -SplitSearch is not provided.' 59 | return 60 | } 61 | 62 | if ($Recurse -and -not $SplitSearch) { 63 | Write-Error '[!] Cannot recurse if -SplitSearch is not provided.' 64 | return 65 | } 66 | 67 | if ($ParsedContainers -and -not (Test-Path -Path $ParsedContainers)) { 68 | Write-Error '[!] -ParsedContainers path not found, provide a valid path.' 69 | return 70 | } 71 | 72 | 73 | Print-Logo 74 | Write-Output '[+] Executing with the following parameters:' 75 | if ($server) { Write-Output " - Server: $Server" } 76 | Write-Output " - OutputFilePath: $OutputFilePath" 77 | if ($LdapFilter) { Write-Output " - LdapFilter: $LdapFilter" } 78 | if ($SearchBase) { Write-Output " - SearchBase: $SearchBase" } 79 | if ($SplitSearch) { Write-Output ' - SplitSearch enabled' } 80 | if ($LetterSplitSearch) { Write-Output ' - LetterSplitSearch enabled' } 81 | if ($Recurse) { Write-Output ' - Recurse enabled' } 82 | if ($Credential) { Write-Output " - Credential: $($Credential.UserName)" } 83 | if ($ParsedContainers) { Write-Output " - ParsedContainers: $ParsedContainers" } 84 | if ($Certificates) { Write-Output ' - Enumerating certificates' } 85 | 86 | 87 | $count = [ref]0 88 | $printingThreshold = 1000 89 | 90 | # Prepare Get-ADObject parameters 91 | $getAdObjectParams = @{ 92 | Properties = '*' 93 | LdapFilter = $LdapFilter 94 | } 95 | 96 | if ($Server) { $getAdObjectParams['Server'] = $Server } 97 | if ($SearchBase) { $getAdObjectParams['SearchBase'] = $SearchBase } 98 | if ($Credential) { $getAdObjectParams['Credential'] = $Credential } 99 | if ($PageSize) { $getAdObjectParams['ResultPageSize'] = $PageSize } 100 | 101 | # Open StreamWriter 102 | $streamWriter = New-Object System.IO.StreamWriter($OutputFilePath, $true, [System.Text.Encoding]::UTF8) 103 | try { 104 | $streamWriter.WriteLine('--------------------') 105 | if ($Certificates) { 106 | 107 | Write-Output '[*] Getting Configuration Naming Context...' 108 | $configEnumParams = @{} 109 | if ($Server) { $configEnumParams['Server'] = $Server } 110 | if ($Credential) { $configEnumParams['Credential'] = $Credential } 111 | $configContext = (Get-ADRootDSE @configEnumParams).ConfigurationNamingContext 112 | if ($null -eq $configContext) { 113 | Write-Error '[-] Failed to retrieve ConfigurationNamingContext.' 114 | return 115 | } 116 | 117 | Write-Output "[*] Enumerating PKI objects under $configContext..." 118 | $getAdObjectParams['SearchBase'] = $configContext 119 | 120 | $getAdObjectParams['LdapFilter'] = '(objectClass=pKIEnrollmentService)' 121 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 122 | 123 | $getAdObjectParams['LdapFilter'] = '(objectClass=pKICertificateTemplate)' 124 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 125 | 126 | $getAdObjectParams['LdapFilter'] = '(objectClass=certificationAuthority)' 127 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 128 | 129 | $getAdObjectParams['LdapFilter'] = '(objectclass=msPKI-Enterprise-Oid)' 130 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 131 | 132 | } elseif ($SplitSearch -eq $false -and $LetterSplitSearch -eq $false) { 133 | 134 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 135 | 136 | } elseif ($SplitSearch -eq $true) { 137 | # Get top-level containers 138 | Write-Output "[*] Discovering top level containers for $Server..." 139 | $topLevelContainers = Get-TopLevelContainers -Params $getAdObjectParams 140 | if ($null -eq $topLevelContainers) { 141 | Write-Error '[-] Something went wrong, no top-level containers found.' 142 | return 143 | } 144 | 145 | # We also need to query specifically the domain object 146 | $dcSearchParams = @{ 147 | Properties = '*' 148 | LdapFilter = '(objectClass=domain)' 149 | } 150 | 151 | if ($server) { $dcSearchParams['Server'] = $Server } 152 | if ($Credential) { $dcSearchParams['Credential'] = $Credential } 153 | 154 | Perform-ADQuery -SearchParams $dcSearchParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 155 | 156 | # In letter split search we need to make sure the top level containers are included 157 | if ($LetterSplitSearch -eq $true) { 158 | $topLevelContainers | ForEach-Object { 159 | Process-AdObject -AdObject $_ -StreamWriter $streamWriter 160 | $count.Value++ 161 | if ($count.Value % $printingThreshold -eq 0) { 162 | Write-Output "[+] Queried $($Count.Value) objects so far..." 163 | $streamWriter.Flush() 164 | } 165 | } 166 | 167 | 168 | } 169 | 170 | 171 | Write-Output "[+] Found $($topLevelContainers.Count) top-level containers." 172 | 173 | $processedContainers = @() 174 | $unprocessedContainers = @() 175 | 176 | if ($ParsedContainers) { 177 | $ParsedContainersList = Get-Content -Path $ParsedContainers 178 | } else { 179 | $ParsedContainersList = @() 180 | } 181 | 182 | # process them containers 183 | foreach ($container in $topLevelContainers) { 184 | $containerDN = $container.DistinguishedName 185 | 186 | if ($ParsedContainersList -contains $containerDN) { 187 | Write-Output "[+] Encountered already parsed container $containerDN, skipping..." 188 | $processedContainers += $containerDN 189 | continue 190 | } 191 | 192 | $containerSearchParams = $getAdObjectParams.Clone() 193 | $containerSearchParams['SearchBase'] = $containerDN 194 | 195 | Write-Output "[*] Processing container ($($processedContainers.Count + $unprocessedContainers.Count + 1)/$($topLevelContainers.Count)): $containerDN" 196 | 197 | if ($LetterSplitSearch -eq $false) { 198 | try { 199 | # Process the container 200 | Perform-ADQuery -SearchParams $containerSearchParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 201 | $processedContainers += $containerDN 202 | } catch { 203 | Write-Error "[-] Error processing container '$containerDN': $_" 204 | $unprocessedContainers += $containerDN 205 | continue 206 | } 207 | } elseif ($LetterSplitSearch -eq $true) { 208 | 209 | # Split the search by first letter 210 | $charset = ([char[]](97..122) + [char[]](48..57) + '!', '_', '@', '$', '{', '}') 211 | $OriginalFilter = $containerSearchParams['LdapFilter'] 212 | foreach ($char in $charset) { 213 | Write-Output " [*] Querying $containerDN for objects with CN starting with '$char'" 214 | $containerSearchParams['LdapFilter'] = "(&$OriginalFilter(cn=$char**))" 215 | 216 | try { 217 | Perform-ADQuery -SearchParams $containerSearchParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 218 | } catch { 219 | Write-Output " [!!] Error processing CN=$char* for container '$containerDN': $_`nTrying to split each letter again..." 220 | foreach ($subChar in $charset) { 221 | try { 222 | Write-Output " [*] Querying $containerDN for objects with CN starting with '$char$subChar'" 223 | $containerSearchParams['LdapFilter'] = "(&$OriginalFilter(cn=$char$subChar**))" 224 | Perform-ADQuery -SearchParams $containerSearchParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 225 | } catch { 226 | Write-Output " [-] Failed to process (CN=$char$subChar*) for container '$containerDN': $_`nMoving to the next sub letter..." 227 | continue 228 | } 229 | } 230 | } 231 | } 232 | 233 | $processedContainers += $containerDN 234 | } 235 | } 236 | 237 | # Output summary 238 | Write-Output "Processed $($count.Value) objects in total." 239 | if ($processedContainers.Count -gt 0) { 240 | Write-Output '[+] Successfully processed containers:' 241 | $processedContainers | ForEach-Object { Write-Output " - $_" } 242 | } 243 | if ($unprocessedContainers.Count -gt 0) { 244 | Write-Output "`n[-] Failed to process containers:" 245 | $unprocessedContainers | ForEach-Object { Write-Output " - $_" } 246 | } 247 | } elseif ($LetterSplitSearch -eq $true -and $SplitSearch -eq $false) { 248 | $charset = ([char[]](97..122) + [char[]](48..57) + '!', '_', '@', '$', '{', '}') 249 | $OriginalFilter = $getAdObjectParams['LdapFilter'] 250 | foreach ($char in $charset) { 251 | Write-Output " [*] Querying for objects with CN starting with '$char'" 252 | $getAdObjectParams['LdapFilter'] = "(&$OriginalFilter(cn=$char**))" 253 | 254 | try { 255 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 256 | } catch { 257 | Write-Output " [!!] Error processing character '$char*': $_" 258 | Write-Output ' Trying to split each letter again...' 259 | foreach ($subChar in $charset) { 260 | try { 261 | Write-Output " [*] Querying for objects with CN starting with '$char$subChar'" 262 | $getAdObjectParams['LdapFilter'] = "(&$OriginalFilter(cn=$char$subChar**))" 263 | Perform-ADQuery -SearchParams $getAdObjectParams -StreamWriter $streamWriter -Count $count -PrintingThreshold $printingThreshold 264 | } catch { 265 | Write-Output " [-] Failed to process (CN=$char$subChar*): $_" 266 | Write-Output ' Moving to the next sub letter...' 267 | continue 268 | } 269 | } 270 | } 271 | } 272 | } 273 | 274 | 275 | $summaryLine = "Retrieved $($count.Value) results total" 276 | $streamWriter.WriteLine($summaryLine) 277 | } finally { 278 | $streamWriter.Flush() 279 | $streamWriter.Close() 280 | } 281 | 282 | Write-Output "Objects have been processed and written to $OutputFilePath" 283 | Write-Output $summaryLine 284 | Write-Output '===================================================' 285 | 286 | # Handle recursion if necessary 287 | if ($Recurse -and $unprocessedContainers.Count -gt 0) { 288 | Write-Output "[*] Current SearchBase is $SearchBase" 289 | Write-Output "[*] Attempting to recurse $($unprocessedContainers.Count) failed containers/OUs:" 290 | foreach ($failedContainer in $unprocessedContainers) { 291 | Write-Output $failedContainer 292 | 293 | $recurseParams = @{ 294 | OutputFilePath = "$($failedContainer.Split(',')[0].Split('=')[1])_$OutputFilePath" 295 | SearchBase = $failedContainer 296 | SplitSearch = $true 297 | Recurse = $true 298 | } 299 | if ($Server) { $recurseParams['Server'] = $Server } 300 | if ($Credential) { $recurseParams['Credential'] = $Credential } 301 | if ($ParsedContainers) { $recurseParams['ParsedContainers'] = $ParsedContainers } 302 | if ($LdapFilter) { $recurseParams['LdapFilter'] = $LdapFilter } 303 | 304 | if ($LetterSplitSearch) { 305 | $recurseParams['LetterSplitSearch'] = $true 306 | } 307 | 308 | Write-Output "[+] Attempting to recurse $failedContainer" 309 | ShadowHound-ADM @recurseParams 310 | } 311 | } 312 | } 313 | 314 | function Print-Logo { 315 | $logo = @' 316 | ......................................................................... 317 | : ____ _ _ _ _ _ : 318 | : / ___|| |__ __ _ __| | _____ _| | | | ___ _ _ _ __ __| | : 319 | : \___ \| '_ \ / _` |/ _` |/ _ \ \ /\ / / |_| |/ _ \| | | | '_ \ / _` | : 320 | : ___) | | | | (_| | (_| | (_) \ V V /| _ | (_) | |_| | | | | (_| | : 321 | : |____/|_| |_|\__,_|\__,_|\___/ \_/\_/ |_| |_|\___/ \__,_|_| |_|\__,_| : 322 | : : 323 | : Author: Yehuda Smirnov (X: @yudasm_ BlueSky: @yudasm.bsky.social) : 324 | ......................................................................... 325 | '@ 326 | Write-Output $logo 327 | } 328 | 329 | function Print-Help { 330 | Print-Logo 331 | $helpMessage = ' 332 | ShadowHound-ADM Help 333 | 334 | SYNTAX: 335 | ShadowHound-ADM [-Server ] -OutputFilePath [-LdapFilter ] [-SearchBase ] [-PageSize ] [-Credential ] [-SplitSearch] [-LetterSplitSearch] [-ParsedContainers ] [-Recurse] [-Help] 336 | 337 | PARAMETERS: 338 | -Help 339 | Display help information. 340 | 341 | -Server [Optional] 342 | The domain controller to query, e.g., domain.local or 192.168.10.10. 343 | 344 | -OutputFilePath [Required] 345 | The path to the output file where results will be saved. 346 | 347 | -LdapFilter [Optional] 348 | LDAP filter to customize the search. 349 | Defaults to (objectGuid=*). 350 | 351 | -SearchBase [Optional] 352 | The base DN for the search, e.g., CN=top,CN=level,DC=domain,DC=local. 353 | Defaults to the root of the domain. 354 | 355 | -PageSize [Optional] 356 | The number of objects to include in one page for paging LDAP searches. 357 | 358 | -Credential [Optional] 359 | PSCredential object for alternate credentials. 360 | 361 | -SplitSearch [Optional] 362 | Splits the search across top-level containers to handle large domains. 363 | 364 | -LetterSplitSearch [Optional] 365 | Splits the search by first letter of CN to handle large domains; if the query fails, will also split the letter. 366 | 367 | -ParsedContainers [Optional] 368 | Path to a file containing a newline-separated list of Distinguished Names of parsed containers (exact match required). 369 | 370 | -Certificates [Optional] 371 | Enumerate certificates. 372 | 373 | -Recurse [Optional] 374 | Recursively process containers that fail. 375 | 376 | EXAMPLES: 377 | # Example 1: Basic usage with required parameter 378 | ShadowHound-ADM -OutputFilePath "C:\Results\output.txt" 379 | 380 | # Example 2: Specify a domain controller and custom LDAP filter 381 | ShadowHound-ADM -Server "dc.domain.local" -OutputFilePath "C:\Results\output.txt" -LdapFilter "(objectClass=user)" 382 | 383 | # Example 3: Use alternate credentials and specify a search base 384 | $cred = Get-Credential 385 | ShadowHound-ADM -OutputFilePath "C:\Results\output.txt" -Credential $cred -SearchBase "DC=domain,DC=local" 386 | 387 | # Example 4: Split the search across top-level containers with split letter search 388 | ShadowHound-ADM -OutputFilePath "C:\Results\output.txt" -SplitSearch -LetterSplitSearch 389 | 390 | # Example 5: Enumerate certificates 391 | ShadowHound-ADM -OutputFilePath "C:\Results\output.txt" -Certificates 392 | ' 393 | Write-Host $helpMessage 394 | return 395 | } 396 | 397 | function Process-AdObject { 398 | [CmdletBinding()] 399 | param ( 400 | [Parameter(Mandatory = $true)] 401 | [Microsoft.ActiveDirectory.Management.ADObject]$AdObject, 402 | 403 | [Parameter(Mandatory = $true)] 404 | [System.IO.StreamWriter]$StreamWriter 405 | ) 406 | 407 | # Define ignored properties 408 | $ignoredValues = @( 409 | 'CanonicalName', 'PropertyNames', 'AddedProperties', 'RemovedProperties', 410 | 'ModifiedProperties', 'PropertyCount', 'repsTo', 'ProtectedFromAccidentalDeletion', 411 | 'sDRightsEffective', 'modifyTimeStamp', 'Modified', 'createTimeStamp', 412 | 'Created', 'userCertificate' 413 | ) 414 | 415 | # Map object classes 416 | $objectClassMapping = @{ 417 | 'applicationSettings' = 'top, applicationSettings, nTFRSSettings' 418 | 'builtinDomain' = 'top, builtinDomain' 419 | 'classStore' = 'top, classStore' 420 | 'container' = 'top, container' 421 | 'groupPolicyContainer' = 'top, container, groupPolicyContainer' 422 | 'msImaging-PSPs' = 'top, container, msImaging-PSPs' 423 | 'rpcContainer' = 'top, container, rpcContainer' 424 | 'dfsConfiguration' = 'top, dfsConfiguration' 425 | 'dnsNode' = 'top, dnsNode' 426 | 'dnsZone' = 'top, dnsZone' 427 | 'domainDNS' = 'top, domain, domainDNS' 428 | 'fileLinkTracking' = 'top, fileLinkTracking' 429 | 'linkTrackObjectMoveTable' = 'top, fileLinkTracking, linkTrackObjectMoveTable' 430 | 'linkTrackVolumeTable' = 'top, fileLinkTracking, linkTrackVolumeTable' 431 | 'foreignSecurityPrincipal' = 'top, foreignSecurityPrincipal' 432 | 'group' = 'top, group' 433 | 'infrastructureUpdate' = 'top, infrastructureUpdate' 434 | 'ipsecFilter' = 'top, ipsecBase, ipsecFilter' 435 | 'ipsecISAKMPPolicy' = 'top, ipsecBase, ipsecISAKMPPolicy' 436 | 'ipsecNegotiationPolicy' = 'top, ipsecBase, ipsecNegotiationPolicy' 437 | 'ipsecNFA' = 'top, ipsecBase, ipsecNFA' 438 | 'ipsecPolicy' = 'top, ipsecBase, ipsecPolicy' 439 | 'domainPolicy' = 'top, leaf, domainPolicy' 440 | 'secret' = 'top, leaf, secret' 441 | 'trustedDomain' = 'top, leaf, trustedDomain' 442 | 'lostAndFound' = 'top, lostAndFound' 443 | 'msDFSR-Content' = 'top, msDFSR-Content' 444 | 'msDFSR-ContentSet' = 'top, msDFSR-ContentSet' 445 | 'msDFSR-GlobalSettings' = 'top, msDFSR-GlobalSettings' 446 | 'msDFSR-LocalSettings' = 'top, msDFSR-LocalSettings' 447 | 'msDFSR-Member' = 'top, msDFSR-Member' 448 | 'msDFSR-ReplicationGroup' = 'top, msDFSR-ReplicationGroup' 449 | 'msDFSR-Subscriber' = 'top, msDFSR-Subscriber' 450 | 'msDFSR-Subscription' = 'top, msDFSR-Subscription' 451 | 'msDFSR-Topology' = 'top, msDFSR-Topology' 452 | 'msDS-PasswordSettingsContainer' = 'top, msDS-PasswordSettingsContainer' 453 | 'msDS-QuotaContainer' = 'top, msDS-QuotaContainer' 454 | 'msTPM-InformationObjectsContainer' = 'top, msTPM-InformationObjectsContainer' 455 | 'organizationalUnit' = 'top, organizationalUnit' 456 | 'contact' = 'top, person, organizationalPerson, contact' 457 | 'user' = 'top, person, organizationalPerson, user' 458 | 'computer' = 'top, person, organizationalPerson, user, computer' 459 | 'rIDManager' = 'top, rIDManager' 460 | 'rIDSet' = 'top, rIDSet' 461 | 'samServer' = 'top, securityObject, samServer' 462 | 'msExchSystemObjectsContainer' = 'top, container, msExchSystemObjectsContainer' 463 | 'msRTCSIP-ApplicationContacts' = 'top, container, msRTCSIP-ApplicationContacts' 464 | 'msRTCSIP-ArchivingServer' = 'top, container, msRTCSIP-ArchivingServer' 465 | 'msRTCSIP-ConferenceDirectories' = 'top, container, msRTCSIP-ConferenceDirectories' 466 | 'msRTCSIP-ConferenceDirectory' = 'top, container, msRTCSIP-ConferenceDirectory' 467 | 'msRTCSIP-Domain' = 'top, container, msRTCSIP-Domain' 468 | 'msRTCSIP-EdgeProxy' = 'top, container, msRTCSIP-EdgeProxy' 469 | 'msRTCSIP-GlobalContainer' = 'top, container, msRTCSIP-GlobalContainer' 470 | 'msRTCSIP-GlobalTopologySetting' = 'top, container, msRTCSIP-GlobalTopologySetting' 471 | 'msRTCSIP-GlobalTopologySettings' = 'top, container, msRTCSIP-GlobalTopologySettings' 472 | 'msRTCSIP-GlobalUserPolicy' = 'top, container, msRTCSIP-GlobalUserPolicy' 473 | 'msRTCSIP-LocalNormalization' = 'top, container, msRTCSIP-LocalNormalization' 474 | 'msRTCSIP-LocalNormalizations' = 'top, container, msRTCSIP-LocalNormalizations' 475 | 'msRTCSIP-LocationContactMapping' = 'top, container, msRTCSIP-LocationContactMapping' 476 | 'msRTCSIP-LocationContactMappings' = 'top, container, msRTCSIP-LocationContactMappings' 477 | 'msRTCSIP-LocationProfile' = 'top, container, msRTCSIP-LocationProfile' 478 | 'msRTCSIP-LocationProfiles' = 'top, container, msRTCSIP-LocationProfiles' 479 | 'msRTCSIP-MCUFactories' = 'top, container, msRTCSIP-MCUFactories' 480 | 'msRTCSIP-MCUFactory' = 'top, container, msRTCSIP-MCUFactory' 481 | 'msRTCSIP-MonitoringServer' = 'top, container, msRTCSIP-MonitoringServer' 482 | 'msRTCSIP-PhoneRoute' = 'top, container, msRTCSIP-PhoneRoute' 483 | 'msRTCSIP-PhoneRoutes' = 'top, container, msRTCSIP-PhoneRoutes' 484 | 'msRTCSIP-Policies' = 'top, container, msRTCSIP-Policies' 485 | 'msRTCSIP-Pool' = 'top, container, msRTCSIP-Pool' 486 | 'msRTCSIP-Pools' = 'top, container, msRTCSIP-Pools' 487 | 'msRTCSIP-RouteUsage' = 'top, container, msRTCSIP-RouteUsage' 488 | 'msRTCSIP-RouteUsages' = 'top, container, msRTCSIP-RouteUsages' 489 | 'msRTCSIP-TrustedMCU' = 'top, container, msRTCSIP-TrustedMCU' 490 | 'msRTCSIP-TrustedMCUs' = 'top, container, msRTCSIP-TrustedMCUs' 491 | 'msRTCSIP-TrustedProxies' = 'top, container, msRTCSIP-TrustedProxies' 492 | 'msRTCSIP-TrustedServer' = 'top, container, msRTCSIP-TrustedServer' 493 | 'msRTCSIP-TrustedService' = 'top, container, msRTCSIP-TrustedService' 494 | 'msRTCSIP-TrustedServices' = 'top, container, msRTCSIP-TrustedServices' 495 | 'msRTCSIP-TrustedWebComponentsServer' = 'top, container, msRTCSIP-TrustedWebComponentsServer' 496 | 'msRTCSIP-TrustedWebComponentsServers' = 'top, container, msRTCSIP-TrustedWebComponentsServers' 497 | 'msWMI-Som' = 'top, msWMI-Som' 498 | 'nTFRSReplicaSet' = 'top, nTFRSReplicaSet' 499 | 'packageRegistration' = 'top, packageRegistration' 500 | 'msDS-GroupManagedServiceAccount' = 'top, person, organizationalPerson, user, computer, msDS-GroupManagedServiceAccount' 501 | 'pKIEnrollmentService' = 'top, pKIEnrollmentService' 502 | 'nTFRSSettings' = 'top, applicationSettings, nTFRSSettings' 503 | 'rpcServer' = 'top, leaf, connectionPoint, rpcEntry, rpcServer' 504 | 'rpcServerElement' = 'top, leaf, connectionPoint, rpcEntry, rpcServerElement' 505 | 'serviceConnectionPoint' = 'top, leaf, connectionPoint, serviceConnectionPoint' 506 | 'msRTCSIP-ApplicationServerService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-ApplicationServerService' 507 | 'msRTCSIP-MCUFactoryService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-MCUFactoryService' 508 | 'msRTCSIP-PoolService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-PoolService' 509 | 'msRTCSIP-Service' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-Service' 510 | 'msRTCSIP-WebComponentsService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-WebComponentsService' 511 | 'pKICertificateTemplate' = 'top, pKICertificateTemplate' 512 | 'certificationAuthority' = 'top, certificationAuthority' 513 | 'msPKI-Enterprise-Oid' = 'top, msPKI-Enterprise-Oid' 514 | } 515 | 516 | if ($null -eq $AdObject) { 517 | Write-Error 'AdObject is null' 518 | return 519 | } 520 | 521 | $outputLines = New-Object System.Collections.Generic.List[string] 522 | $outputLines.Add('--------------------') 523 | 524 | foreach ($property in $AdObject.PSObject.Properties) { 525 | $name = $property.Name 526 | $value = $property.Value 527 | 528 | # Skip properties with empty values and unwanted properties 529 | if ($null -eq $value -or ($value -is [string] -and [string]::IsNullOrWhiteSpace($value)) -or $ignoredValues -contains $name) { 530 | continue 531 | } 532 | 533 | # Cache type checks 534 | $isDateTime = $value -is [datetime] 535 | $isByteArray = $value -is [byte[]] 536 | $isGuid = $value -is [guid] 537 | $isCollection = $value -is [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection] 538 | 539 | switch ($name) { 540 | 'nTSecurityDescriptor' { 541 | if ($null -ne $value) { 542 | $binaryForm = $value.GetSecurityDescriptorBinaryForm() 543 | if ($binaryForm.Length -gt 0) { 544 | $base64Value = [System.Convert]::ToBase64String($binaryForm) 545 | $outputLines.Add("$name`: $base64Value") 546 | } 547 | } 548 | break 549 | } 550 | 'objectClass' { 551 | if ($objectClassMapping.ContainsKey($value)) { 552 | $formattedObjectClass = $objectClassMapping[$value] 553 | } else { 554 | $formattedObjectClass = ($value -join ', ') 555 | } 556 | $outputLines.Add("$name`: $formattedObjectClass") 557 | break 558 | } 559 | default { 560 | if ($isDateTime) { 561 | # Format date/time attributes in LDAP time format 562 | $formattedValue = '{0:yyyyMMddHHmmss.0Z}' -f $value.ToUniversalTime() 563 | $outputLines.Add("$name`: $formattedValue") 564 | } elseif ($isByteArray) { 565 | # Base64 encode byte arrays 566 | if ($value.Length -gt 0) { 567 | $base64Value = [System.Convert]::ToBase64String($value) 568 | $outputLines.Add("$name`: $base64Value") 569 | } 570 | } elseif ($isGuid) { 571 | $outputLines.Add("$name`: $value") 572 | } elseif ($isCollection) { 573 | switch ($name) { 574 | 'dSCorePropagationData' { 575 | # Efficiently find the latest date 576 | $latestDate = $null 577 | foreach ($date in $value) { 578 | if ($date -is [datetime]) { 579 | if ($null -eq $latestDate -or $date -gt $latestDate) { 580 | $latestDate = $date 581 | } 582 | } 583 | } 584 | if ($null -ne $latestDate) { 585 | $formattedDate = '{0:yyyyMMddHHmmss.0Z}' -f $latestDate.ToUniversalTime() 586 | $outputLines.Add("$name`: $formattedDate") 587 | } 588 | break 589 | } 590 | 'cACertificate' { 591 | if ($value.Count -gt 0 -and $value[0].Length -gt 0) { 592 | $base64Value = [System.Convert]::ToBase64String($value[0]) 593 | $outputLines.Add("$name`: $base64Value") 594 | } 595 | break 596 | } 597 | 'userCertificate' { 598 | if ($value.Count -gt 0 -and $value[0].Length -gt 0) { 599 | $base64Value = [System.Convert]::ToBase64String($value[0]) 600 | $outputLines.Add("$name`: $base64Value") 601 | } 602 | break 603 | } 604 | 'authorityRevocationList' { 605 | $outputLines.Add("$name`: $null") 606 | break 607 | } 608 | default { 609 | $joinedValues = ($value | ForEach-Object { $_.ToString() }) -join ', ' 610 | $outputLines.Add("$name`: $joinedValues") 611 | break 612 | } 613 | } 614 | } else { 615 | # General handling for other types 616 | $outputLines.Add("$name`: $value") 617 | } 618 | break 619 | } 620 | } 621 | } 622 | 623 | # Write the formatted content to the file using StreamWriter 624 | foreach ($line in $outputLines) { 625 | $StreamWriter.WriteLine($line) 626 | } 627 | } 628 | 629 | function Perform-ADQuery { 630 | [CmdletBinding()] 631 | param ( 632 | [Parameter(Mandatory = $true)] 633 | [hashtable]$SearchParams, 634 | 635 | [Parameter(Mandatory = $true)] 636 | [System.IO.StreamWriter]$StreamWriter, 637 | 638 | [Parameter(Mandatory = $true)] 639 | [ref]$Count, 640 | 641 | [Parameter(Mandatory = $false)] 642 | [int]$PrintingThreshold = 1000 643 | ) 644 | 645 | # Process the objects 646 | Get-ADObject @SearchParams | ForEach-Object { 647 | Process-AdObject -AdObject $_ -StreamWriter $StreamWriter 648 | $Count.Value++ 649 | if ($Count.Value % $PrintingThreshold -eq 0) { 650 | Write-Output " [**] Queried $($Count.Value) objects so far..." 651 | $StreamWriter.Flush() 652 | } 653 | } 654 | } 655 | 656 | function Get-TopLevelContainers { 657 | [CmdletBinding()] 658 | param ( 659 | [Parameter(Mandatory = $true)] 660 | [hashtable]$Params 661 | ) 662 | 663 | try { 664 | $topLevelParams = $Params.Clone() 665 | $topLevelParams['SearchScope'] = 'OneLevel' 666 | $TopLevelContainers = Get-ADObject @topLevelParams 667 | return $TopLevelContainers 668 | } catch { 669 | Write-Error "Failed to retrieve top-level containers: $_" 670 | return $null 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /ShadowHound-DS.ps1: -------------------------------------------------------------------------------- 1 | function ShadowHound-DS() { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory = $false, HelpMessage = 'The domain controller to query.')] 5 | [string]$Server, 6 | 7 | [Parameter(Mandatory = $false, HelpMessage = 'Path to the output file where results will be saved.')] 8 | [ValidateNotNullOrEmpty()] 9 | [string]$OutputFile, 10 | 11 | [Parameter(Mandatory = $false, HelpMessage = 'LDAP filter to customize the search.')] 12 | [string]$LdapFilter = '(ObjectGuid=*)', 13 | 14 | [Parameter(Mandatory = $false, HelpMessage = 'The base DN for the search.')] 15 | [string]$SearchBase, 16 | 17 | [Parameter(Mandatory = $false, HelpMessage = 'PSCredential object for alternate credentials.')] 18 | [pscredential]$Credential, 19 | 20 | [Parameter(Mandatory = $false, HelpMessage = 'Enumerate certificates.')] 21 | [switch]$Certificates, 22 | 23 | [Parameter(Mandatory = $false, HelpMessage = 'Display help information.')] 24 | [switch]$Help 25 | ) 26 | 27 | if ($Help) { 28 | Print-Help 29 | return 30 | } 31 | 32 | if (-not $OutputFile) { 33 | Write-Output '[-] -OutputFile is required.' 34 | return 35 | } 36 | 37 | Print-Logo 38 | 39 | Write-Output '[+] Executing with the following parameters:' 40 | if ($Server) { Write-Output " - Server: $Server" } 41 | Write-Output " - OutputFile: $OutputFile" 42 | if ($LdapFilter) { Write-Output " - LdapFilter: $LdapFilter" } 43 | if ($SearchBase) { Write-Output " - SearchBase: $SearchBase" } 44 | if ($Credential) { Write-Output " - Credential: $($Credential.UserName)" } 45 | if ($Certificates) { Write-Output ' - Enumerating certificates' } 46 | 47 | if ($Certificates) { 48 | # Enumerate certificates 49 | Write-Output '[*] Getting Configuration Naming Context...' 50 | 51 | try { 52 | if ($Server) { 53 | if ($Credential) { 54 | $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/RootDSE", $Credential.UserName, $Credential.GetNetworkCredential().Password) 55 | } else { 56 | $rootDSE = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$Server/RootDSE") 57 | } 58 | } else { 59 | if ($Credential) { 60 | $rootDSE = New-Object System.DirectoryServices.DirectoryEntry('LDAP://RootDSE', $Credential.UserName, $Credential.GetNetworkCredential().Password) 61 | } else { 62 | $rootDSE = New-Object System.DirectoryServices.DirectoryEntry('LDAP://RootDSE') 63 | } 64 | } 65 | $configContext = $rootDSE.Properties['configurationNamingContext'][0] 66 | } catch { 67 | Write-Output "[-] Failed to retrieve ConfigurationNamingContext: $_" 68 | return 69 | } 70 | 71 | if ($null -eq $configContext) { 72 | Write-Output '[-] ConfigurationNamingContext is null.' 73 | return 74 | } 75 | 76 | Write-Output "[*] Enumerating PKI objects under $configContext..." 77 | 78 | $ldapFilters = @( 79 | '(objectClass=pKIEnrollmentService)', 80 | '(objectClass=pKICertificateTemplate)', 81 | '(objectClass=certificationAuthority)', 82 | '(objectclass=msPKI-Enterprise-Oid)' 83 | ) 84 | 85 | $count = 0 86 | $streamWriter = New-Object System.IO.StreamWriter($OutputFile, $true, [System.Text.Encoding]::UTF8) 87 | try { 88 | 89 | foreach ($ldapFilter in $ldapFilters) { 90 | Write-Output " [*] Searching with filter: $ldapFilter" 91 | 92 | if ($Server) { 93 | $ldapPath = "LDAP://$Server/$configContext" 94 | } else { 95 | $ldapPath = "LDAP://$configContext" 96 | } 97 | 98 | if ($Credential) { 99 | $directoryEntry = New-Object System.DirectoryServices.DirectoryEntry($ldapPath, $Credential.UserName, $Credential.GetNetworkCredential().Password) 100 | } else { 101 | $directoryEntry = New-Object System.DirectoryServices.DirectoryEntry($ldapPath) 102 | } 103 | 104 | $searcher = New-Object System.DirectoryServices.DirectorySearcher($directoryEntry) 105 | $searcher.Filter = $ldapFilter 106 | $searcher.PageSize = 1000 107 | $silenceofthezero = $searcher.PropertiesToLoad.Add('*') 108 | $searcher.SecurityMasks = 'Dacl,Group,Owner' 109 | 110 | try { 111 | $searchResults = $searcher.FindAll() 112 | } catch { 113 | Write-Output " [!!] Error during search with filter $ldapFilter`: $_" 114 | continue 115 | } 116 | 117 | foreach ($searchResult in $searchResults) { 118 | Process-AdObject -SearchResult $searchResult -StreamWriter $streamWriter 119 | $count++ 120 | if ($count % 1000 -eq 0) { 121 | Write-Output " [*] $count objects processed..." 122 | $streamWriter.Flush() 123 | } 124 | } 125 | } 126 | 127 | $streamWriter.WriteLine("Retrieved $count results total") 128 | 129 | } finally { 130 | $streamWriter.Flush() 131 | $streamWriter.Close() 132 | } 133 | 134 | 135 | Write-Output "Objects have been processed and written to $OutputFile" 136 | Write-Output "Retrieved $count results total" 137 | return 138 | } 139 | 140 | # If not enumerating certificates, proceed as usual 141 | if ($SearchBase) { 142 | $ldapPath = 'LDAP://' 143 | if ($Server) { 144 | $ldapPath += "$Server/" 145 | } 146 | $ldapPath += "$SearchBase" 147 | } elseif ($Server) { 148 | $ldapPath = "LDAP://$Server" 149 | } else { 150 | $rootDSE = New-Object System.DirectoryServices.DirectoryEntry('LDAP://RootDSE') 151 | $ldapPath = "LDAP://$($rootDSE.Properties.defaultNamingContext[0])" 152 | } 153 | 154 | if ($Credential) { 155 | $directoryEntry = New-Object System.DirectoryServices.DirectoryEntry($ldapPath, $Credential.UserName, $Credential.GetNetworkCredential().Password) 156 | } else { 157 | $directoryEntry = New-Object System.DirectoryServices.DirectoryEntry($ldapPath) 158 | } 159 | 160 | $searcher = New-Object System.DirectoryServices.DirectorySearcher($directoryEntry) 161 | 162 | if ($LdapFilter) { 163 | $searcher.Filter = $LdapFilter 164 | } else { 165 | $searcher.Filter = '(objectGuid=*)' 166 | } 167 | 168 | $searcher.PageSize = 1000 169 | 170 | $silenceofthezero = $searcher.PropertiesToLoad.Add('*') 171 | $searcher.SecurityMasks = 'Dacl,Group,Owner' 172 | 173 | $count = 0 174 | 175 | $streamWriter = New-Object System.IO.StreamWriter($OutputFile, $true, [System.Text.Encoding]::UTF8) 176 | try { 177 | 178 | try { 179 | $searchResults = $searcher.FindAll() 180 | } catch { 181 | Write-Output "Error during search: $_" 182 | return 183 | } 184 | 185 | foreach ($searchResult in $searchResults) { 186 | Process-AdObject -SearchResult $searchResult -StreamWriter $streamWriter 187 | $count++ 188 | if ($count % 1000 -eq 0) { 189 | Write-Output " [*] $count objects processed..." 190 | $streamWriter.Flush() 191 | } 192 | } 193 | $streamWriter.WriteLine("Retrieved $count results total") 194 | } finally { 195 | $streamWriter.Flush() 196 | $streamWriter.Close() 197 | } 198 | 199 | Write-Output "Objects have been processed and written to $OutputFile" 200 | Write-Output "Retrieved $count results total" 201 | } 202 | 203 | 204 | function Process-AdObject { 205 | param ( 206 | [System.DirectoryServices.SearchResult]$SearchResult, 207 | [System.IO.StreamWriter]$StreamWriter 208 | ) 209 | 210 | if ($null -eq $SearchResult) { 211 | Write-Output '[-] Search result is null.' 212 | return 213 | } 214 | 215 | $propertiesList = @() 216 | $propertiesList += '--------------------' 217 | 218 | foreach ($name in $SearchResult.Properties.PropertyNames) { 219 | $valueCollection = $SearchResult.Properties[$name] 220 | 221 | if ($null -eq $valueCollection -or $valueCollection.Count -eq 0 -or $name -in $ignoredValues ) { 222 | continue 223 | } 224 | 225 | # Handle nTSecurityDescriptor 226 | if ($name -eq 'nTSecurityDescriptor') { 227 | $entry = $SearchResult.GetDirectoryEntry() 228 | $securityDescriptor = $entry.ObjectSecurity 229 | 230 | if ($null -ne $securityDescriptor) { 231 | $binaryForm = $securityDescriptor.GetSecurityDescriptorBinaryForm() 232 | $base64Value = [System.Convert]::ToBase64String($binaryForm) 233 | $propertiesList += "$name`: $base64Value" 234 | } 235 | } elseif ($valueCollection[0] -is [DateTime]) { 236 | foreach ($value in $valueCollection) { 237 | $formattedValue = $value.ToUniversalTime().ToString('yyyyMMddHHmmss.0Z') 238 | $propertiesList += "$name`: $formattedValue" 239 | } 240 | } elseif ($name -eq 'objectClass') { 241 | $values = $valueCollection | ForEach-Object { $_.ToString() } 242 | 243 | if ($objectClassMapping.ContainsKey($values[0])) { 244 | $formattedObjectClass = $objectClassMapping[$values[0]] 245 | } else { 246 | $formattedObjectClass = ($values -join ', ') 247 | } 248 | 249 | $propertiesList += "$name`: $formattedObjectClass" 250 | } elseif ($valueCollection.Count -gt 1) { 251 | if ($name -eq 'dSCorePropagationData') { 252 | $latestDate = ($valueCollection | Where-Object { $_ -is [datetime] } | Sort-Object { $_.ToUniversalTime() } -Descending | Select-Object -First 1) 253 | if ($null -ne $latestDate) { 254 | $formattedDate = $latestDate.ToUniversalTime().ToString('yyyyMMddHHmmss.0Z') 255 | $propertiesList += "$name`: $formattedDate" 256 | } 257 | } elseif ($name -eq 'cACertificate') { 258 | $value = $valueCollection[0] 259 | $propertiesList += "$name`: " + ([Convert]::ToBase64String($value)) 260 | } elseif ($name -eq 'authorityRevocationList') { 261 | $propertiesList += "$name`: $null" 262 | } elseif ($name -eq 'userCertificate') { 263 | $value = $valueCollection[0] 264 | $propertiesList += "$name`: " + ([Convert]::ToBase64String($value)) 265 | } else { 266 | $values = $valueCollection | ForEach-Object { $_.ToString() } 267 | $propertiesList += "$name`: " + ($values -join ', ') 268 | } 269 | } elseif ($valueCollection[0] -is [byte[]]) { 270 | if ($name -eq 'objectSid') { 271 | $sid = New-Object System.Security.Principal.SecurityIdentifier($valueCollection[0], 0) 272 | $propertiesList += "$name`: $sid" 273 | } elseif ($name -eq 'objectguid') { 274 | $guid = [System.Guid]::New($valueCollection[0]) 275 | $propertiesList += "$name`: $($guid.ToString())" 276 | } else { 277 | $value = $valueCollection[0] 278 | $base64Value = [System.Convert]::ToBase64String($value) 279 | $propertiesList += "$name`: $base64Value" 280 | } 281 | } else { 282 | $value = $valueCollection[0] 283 | $propertiesList += "$name`: $value" 284 | } 285 | } 286 | 287 | $StreamWriter.WriteLine([string]::Join("`n", $propertiesList)) 288 | $StreamWriter.WriteLine('') # Add an empty line between objects 289 | } 290 | 291 | $ignoredValues = @('CanonicalName', 'PropertyNames', 'AddedProperties', 292 | 'RemovedProperties', 'ModifiedProperties', 'PropertyCount', 293 | 'repsTo', 'ProtectedFromAccidentalDeletion', 'sDRightsEffective', 294 | 'modifyTimeStamp', 'Modified', 'createTimeStamp', 295 | 'Created', 'userCertificate') 296 | 297 | $objectClassMapping = @{ 298 | 'applicationSettings' = 'top, applicationSettings, nTFRSSettings' 299 | 'builtinDomain' = 'top, builtinDomain' 300 | 'classStore' = 'top, classStore' 301 | 'container' = 'top, container' 302 | 'groupPolicyContainer' = 'top, container, groupPolicyContainer' 303 | 'msImaging-PSPs' = 'top, container, msImaging-PSPs' 304 | 'rpcContainer' = 'top, container, rpcContainer' 305 | 'dfsConfiguration' = 'top, dfsConfiguration' 306 | 'dnsNode' = 'top, dnsNode' 307 | 'dnsZone' = 'top, dnsZone' 308 | 'domainDNS' = 'top, domain, domainDNS' 309 | 'fileLinkTracking' = 'top, fileLinkTracking' 310 | 'linkTrackObjectMoveTable' = 'top, fileLinkTracking, linkTrackObjectMoveTable' 311 | 'linkTrackVolumeTable' = 'top, fileLinkTracking, linkTrackVolumeTable' 312 | 'foreignSecurityPrincipal' = 'top, foreignSecurityPrincipal' 313 | 'group' = 'top, group' 314 | 'infrastructureUpdate' = 'top, infrastructureUpdate' 315 | 'ipsecFilter' = 'top, ipsecBase, ipsecFilter' 316 | 'ipsecISAKMPPolicy' = 'top, ipsecBase, ipsecISAKMPPolicy' 317 | 'ipsecNegotiationPolicy' = 'top, ipsecBase, ipsecNegotiationPolicy' 318 | 'ipsecNFA' = 'top, ipsecBase, ipsecNFA' 319 | 'ipsecPolicy' = 'top, ipsecBase, ipsecPolicy' 320 | 'domainPolicy' = 'top, leaf, domainPolicy' 321 | 'secret' = 'top, leaf, secret' 322 | 'trustedDomain' = 'top, leaf, trustedDomain' 323 | 'lostAndFound' = 'top, lostAndFound' 324 | 'msDFSR-Content' = 'top, msDFSR-Content' 325 | 'msDFSR-ContentSet' = 'top, msDFSR-ContentSet' 326 | 'msDFSR-GlobalSettings' = 'top, msDFSR-GlobalSettings' 327 | 'msDFSR-LocalSettings' = 'top, msDFSR-LocalSettings' 328 | 'msDFSR-Member' = 'top, msDFSR-Member' 329 | 'msDFSR-ReplicationGroup' = 'top, msDFSR-ReplicationGroup' 330 | 'msDFSR-Subscriber' = 'top, msDFSR-Subscriber' 331 | 'msDFSR-Subscription' = 'top, msDFSR-Subscription' 332 | 'msDFSR-Topology' = 'top, msDFSR-Topology' 333 | 'msDS-PasswordSettingsContainer' = 'top, msDS-PasswordSettingsContainer' 334 | 'msDS-QuotaContainer' = 'top, msDS-QuotaContainer' 335 | 'msTPM-InformationObjectsContainer' = 'top, msTPM-InformationObjectsContainer' 336 | 'organizationalUnit' = 'top, organizationalUnit' 337 | 'contact' = 'top, person, organizationalPerson, contact' 338 | 'user' = 'top, person, organizationalPerson, user' 339 | 'computer' = 'top, person, organizationalPerson, user, computer' 340 | 'rIDManager' = 'top, rIDManager' 341 | 'rIDSet' = 'top, rIDSet' 342 | 'samServer' = 'top, securityObject, samServer' 343 | 'msExchSystemObjectsContainer' = 'top, container, msExchSystemObjectsContainer' 344 | 'msRTCSIP-ApplicationContacts' = 'top, container, msRTCSIP-ApplicationContacts' 345 | 'msRTCSIP-ArchivingServer' = 'top, container, msRTCSIP-ArchivingServer' 346 | 'msRTCSIP-ConferenceDirectories' = 'top, container, msRTCSIP-ConferenceDirectories' 347 | 'msRTCSIP-ConferenceDirectory' = 'top, container, msRTCSIP-ConferenceDirectory' 348 | 'msRTCSIP-Domain' = 'top, container, msRTCSIP-Domain' 349 | 'msRTCSIP-EdgeProxy' = 'top, container, msRTCSIP-EdgeProxy' 350 | 'msRTCSIP-GlobalContainer' = 'top, container, msRTCSIP-GlobalContainer' 351 | 'msRTCSIP-GlobalTopologySetting' = 'top, container, msRTCSIP-GlobalTopologySetting' 352 | 'msRTCSIP-GlobalTopologySettings' = 'top, container, msRTCSIP-GlobalTopologySettings' 353 | 'msRTCSIP-GlobalUserPolicy' = 'top, container, msRTCSIP-GlobalUserPolicy' 354 | 'msRTCSIP-LocalNormalization' = 'top, container, msRTCSIP-LocalNormalization' 355 | 'msRTCSIP-LocalNormalizations' = 'top, container, msRTCSIP-LocalNormalizations' 356 | 'msRTCSIP-LocationContactMapping' = 'top, container, msRTCSIP-LocationContactMapping' 357 | 'msRTCSIP-LocationContactMappings' = 'top, container, msRTCSIP-LocationContactMappings' 358 | 'msRTCSIP-LocationProfile' = 'top, container, msRTCSIP-LocationProfile' 359 | 'msRTCSIP-LocationProfiles' = 'top, container, msRTCSIP-LocationProfiles' 360 | 'msRTCSIP-MCUFactories' = 'top, container, msRTCSIP-MCUFactories' 361 | 'msRTCSIP-MCUFactory' = 'top, container, msRTCSIP-MCUFactory' 362 | 'msRTCSIP-MonitoringServer' = 'top, container, msRTCSIP-MonitoringServer' 363 | 'msRTCSIP-PhoneRoute' = 'top, container, msRTCSIP-PhoneRoute' 364 | 'msRTCSIP-PhoneRoutes' = 'top, container, msRTCSIP-PhoneRoutes' 365 | 'msRTCSIP-Policies' = 'top, container, msRTCSIP-Policies' 366 | 'msRTCSIP-Pool' = 'top, container, msRTCSIP-Pool' 367 | 'msRTCSIP-Pools' = 'top, container, msRTCSIP-Pools' 368 | 'msRTCSIP-RouteUsage' = 'top, container, msRTCSIP-RouteUsage' 369 | 'msRTCSIP-RouteUsages' = 'top, container, msRTCSIP-RouteUsages' 370 | 'msRTCSIP-TrustedMCU' = 'top, container, msRTCSIP-TrustedMCU' 371 | 'msRTCSIP-TrustedMCUs' = 'top, container, msRTCSIP-TrustedMCUs' 372 | 'msRTCSIP-TrustedProxies' = 'top, container, msRTCSIP-TrustedProxies' 373 | 'msRTCSIP-TrustedServer' = 'top, container, msRTCSIP-TrustedServer' 374 | 'msRTCSIP-TrustedService' = 'top, container, msRTCSIP-TrustedService' 375 | 'msRTCSIP-TrustedServices' = 'top, container, msRTCSIP-TrustedServices' 376 | 'msRTCSIP-TrustedWebComponentsServer' = 'top, container, msRTCSIP-TrustedWebComponentsServer' 377 | 'msRTCSIP-TrustedWebComponentsServers' = 'top, container, msRTCSIP-TrustedWebComponentsServers' 378 | 'msWMI-Som' = 'top, msWMI-Som' 379 | 'nTFRSReplicaSet' = 'top, nTFRSReplicaSet' 380 | 'packageRegistration' = 'top, packageRegistration' 381 | 'msDS-GroupManagedServiceAccount' = 'top, person, organizationalPerson, user, computer, msDS-GroupManagedServiceAccount' 382 | 'pKIEnrollmentService' = 'top, pKIEnrollmentService' 383 | 'nTFRSSettings' = 'top, applicationSettings, nTFRSSettings' 384 | 'rpcServer' = 'top, leaf, connectionPoint, rpcEntry, rpcServer' 385 | 'rpcServerElement' = 'top, leaf, connectionPoint, rpcEntry, rpcServerElement' 386 | 'serviceConnectionPoint' = 'top, leaf, connectionPoint, serviceConnectionPoint' 387 | 'msRTCSIP-ApplicationServerService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-ApplicationServerService' 388 | 'msRTCSIP-MCUFactoryService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-MCUFactoryService' 389 | 'msRTCSIP-PoolService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-PoolService' 390 | 'msRTCSIP-Service' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-Service' 391 | 'msRTCSIP-WebComponentsService' = 'top, leaf, connectionPoint, serviceConnectionPoint, msRTCSIP-WebComponentsService' 392 | 'pKICertificateTemplate' = 'top, pKICertificateTemplate' 393 | 'certificationAuthority' = 'top, certificationAuthority' 394 | 'msPKI-Enterprise-Oid' = 'top, msPKI-Enterprise-Oid' 395 | } 396 | 397 | function Print-Help { 398 | Print-Logo 399 | $helpMessage = ' 400 | ShadowHound-DS Help 401 | 402 | SYNTAX: 403 | ShadowHound-DS [-Server ] -OutputFile [-LdapFilter ] [-SearchBase ] [-Credential ] [-Certificates] [-Help] 404 | 405 | PARAMETERS: 406 | -Help 407 | Display this help information. 408 | 409 | -Server [Optional] 410 | The domain controller to query. If not specified, the default DC is used. 411 | 412 | -OutputFile [Required] 413 | The path to the output file where results will be saved. 414 | 415 | -LdapFilter [Optional] 416 | LDAP filter to customize the search. 417 | Defaults to (objectGuid=*). 418 | 419 | -SearchBase [Optional] 420 | The base DN for the search. 421 | 422 | -Credential [Optional] 423 | PSCredential object for alternate credentials. 424 | 425 | -Certificates [Optional] 426 | Enumerate certificate-related objects. 427 | 428 | EXAMPLES: 429 | # Example 1: Basic usage with required parameter 430 | ShadowHound-DS -OutputFile "C:\Results\ldap_output.txt" 431 | 432 | # Example 2: Specify a domain controller 433 | ShadowHound-DS -Server "dc.domain.local" -OutputFile "C:\Results\ldap_output.txt" 434 | 435 | # Example 3: Use a custom LDAP filter 436 | ShadowHound-DS -OutputFile "C:\Results\ldap_output.txt" -LdapFilter "(objectClass=computer)" 437 | 438 | # Example 4: Specify a search base 439 | ShadowHound-DS -OutputFile "C:\Results\ldap_output.txt" -SearchBase "DC=domain,DC=local" 440 | 441 | # Example 5: Enumerate certificate-related objects 442 | ShadowHound-DS -OutputFile "C:\Results\cert_output.txt" -Certificates 443 | 444 | # Example 6: Use alternate credentials 445 | $cred = Get-Credential 446 | ShadowHound-DS -OutputFile "C:\Results\ldap_output.txt" -Credential $cred 447 | ' 448 | Write-Output $helpMessage 449 | return 450 | } 451 | 452 | 453 | function Print-Logo { 454 | $logo = @' 455 | ········································································· 456 | : ____ _ _ _ _ _ : 457 | : / ___|| |__ __ _ __| | _____ _| | | | ___ _ _ _ __ __| | : 458 | : \___ \| '_ \ / _` |/ _` |/ _ \ \ /\ / / |_| |/ _ \| | | | '_ \ / _` | : 459 | : ___) | | | | (_| | (_| | (_) \ V V /| _ | (_) | |_| | | | | (_| | : 460 | : |____/|_| |_|\__,_|\__,_|\___/ \_/\_/ |_| |_|\___/ \__,_|_| |_|\__,_| : 461 | : : 462 | : Author: Yehuda Smirnov (X: @yudasm_ BlueSky: @yudasm.bsky.social) : 463 | ········································································· 464 | '@ 465 | Write-Output $logo 466 | } 467 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Friends-Security/ShadowHound/0e251dc81c7bb620eeab82615ada92d20bfb0f2f/logo.png -------------------------------------------------------------------------------- /split_output.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import argparse 4 | 5 | object_delimiter = "--------------------" 6 | 7 | 8 | def split_large_file_into_chunks(input_file, base_output_name, num_chunks): 9 | 10 | # Step 1: Count total objects 11 | total_objects = 0 12 | with open(input_file, "r", encoding="utf-8-sig") as infile: 13 | for line in infile: 14 | if line.strip() == object_delimiter: 15 | total_objects += 1 16 | if total_objects % 100000 == 0: 17 | print(f"[*] Objects counted: {total_objects}") 18 | 19 | if total_objects == 0: 20 | print("[-] No objects found in the file.") 21 | return 22 | 23 | objects_per_chunk = math.ceil(total_objects / num_chunks) 24 | print(f"[+] Total objects: {total_objects}") 25 | print(f"[*] Objects per chunk: {objects_per_chunk}") 26 | 27 | # Step 2: Split objects into chunks 28 | current_chunk_index = 0 29 | current_object_count = 0 # Total objects processed so far 30 | chunk_object_count = 0 # Objects in the current chunk 31 | current_chunk_lines = [] 32 | 33 | with open(input_file, "r", encoding="utf-8-sig") as infile: 34 | for line in infile: 35 | current_chunk_lines.append(line) 36 | 37 | if line.strip() == object_delimiter: 38 | current_object_count += 1 39 | chunk_object_count += 1 40 | 41 | # If we have reached the number of objects per chunk, write the chunk 42 | if chunk_object_count >= objects_per_chunk: 43 | write_chunk_to_file( 44 | current_chunk_lines, 45 | base_output_name, 46 | current_chunk_index, 47 | chunk_object_count, 48 | ) 49 | current_chunk_index += 1 50 | current_chunk_lines = [] 51 | chunk_object_count = 0 52 | 53 | # After finishing reading the file, write any remaining lines 54 | if current_chunk_lines: 55 | write_chunk_to_file( 56 | current_chunk_lines, 57 | base_output_name, 58 | current_chunk_index, 59 | chunk_object_count, 60 | ) 61 | 62 | 63 | def write_chunk_to_file(chunk_lines, base_output_name, chunk_index, chunk_object_count): 64 | output_file = f"{base_output_name}_chunk_{chunk_index}.txt" 65 | with open(output_file, "w", encoding="utf-8-sig") as outfile: 66 | # Write the starting delimiter 67 | outfile.write(f"{object_delimiter}\n{object_delimiter}\n") 68 | # Write the chunk lines 69 | outfile.writelines(chunk_lines) 70 | # Write the ending lines 71 | outfile.write(f"Retrieved {chunk_object_count} results total\n") 72 | print(f"[+] Chunk {chunk_index} written to {output_file}") 73 | 74 | 75 | if __name__ == "__main__": 76 | parser = argparse.ArgumentParser(description="Split a large file into chunks.") 77 | parser.add_argument( 78 | "-i", 79 | "--input_file", 80 | type=str, 81 | required=True, 82 | help="Path to the input text file.", 83 | ) 84 | parser.add_argument( 85 | "-o", 86 | "--base_output_name", 87 | type=str, 88 | required=True, 89 | help="Base name for the output files.", 90 | ) 91 | parser.add_argument( 92 | "-n", 93 | "--num_chunks", 94 | type=int, 95 | required=True, 96 | help="Number of chunks to split the file into.", 97 | ) 98 | 99 | args = parser.parse_args() 100 | 101 | split_large_file_into_chunks( 102 | args.input_file, args.base_output_name, args.num_chunks 103 | ) 104 | --------------------------------------------------------------------------------