Public/Invoke-Locksmith.ps1

function Invoke-Locksmith {
    <#
    .SYNOPSIS
    Finds the most common malconfigurations of Active Directory Certificate Services (AD CS).
 
    .DESCRIPTION
    Locksmith uses the Active Directory (AD) Powershell (PS) module to identify 10 misconfigurations
    commonly found in Enterprise mode AD CS installations.
 
    .COMPONENT
    Locksmith requires the AD PS module to be installed in the scope of the Current User.
    If Locksmith does not identify the AD PS module as installed, it will attempt to
    install the module. If module installation does not complete successfully,
    Locksmith will fail.
 
    .PARAMETER Mode
    Specifies sets of common script execution modes.
 
    -Mode 0
    Finds any malconfigurations and displays them in the console.
    No attempt is made to fix identified issues.
 
    -Mode 1
    Finds any malconfigurations and displays them in the console.
    Displays example Powershell snippet that can be used to resolve the issue.
    No attempt is made to fix identified issues.
 
    -Mode 2
    Finds any malconfigurations and writes them to a series of CSV files.
    No attempt is made to fix identified issues.
 
    -Mode 3
    Finds any malconfigurations and writes them to a series of CSV files.
    Creates code snippets to fix each issue and writes them to an environment-specific custom .PS1 file.
    No attempt is made to fix identified issues.
 
    -Mode 4
    Finds any malconfigurations and creates code snippets to fix each issue.
    Attempts to fix all identified issues. This mode may require high-privileged access.
 
    .PARAMETER Scans
    Specify which scans you want to run. Available scans: 'All' or Auditing, ESC1, ESC2, ESC3, ESC4, ESC5, ESC6, ESC8, or 'PromptMe'
 
    -Scans All
    Run all scans (default).
 
    -Scans PromptMe
    Presents a grid view of the available scan types that can be selected and run them after you click OK.
 
    .PARAMETER OutputPath
    Specify the path where you want to save reports and mitigation scripts.
 
    .INPUTS
    None. You cannot pipe objects to Invoke-Locksmith.ps1.
 
    .OUTPUTS
    Output types:
    1. Console display of identified issues.
    2. Console display of identified issues and their fixes.
    3. CSV containing all identified issues.
    4. CSV containing all identified issues and their fixes.
 
    .EXAMPLE
    Invoke-Locksmith -Mode 0 -Scans All -OutputPath 'C:\Temp'
 
    Finds all malconfigurations and displays them in the console.
 
    .EXAMPLE
    Invoke-Locksmith -Mode 2 -Scans All -OutputPath 'C:\Temp'
 
    Finds all malconfigurations and displays them in the console. The findings are saved in a CSV file in C:\Temp.
 
    .NOTES
    The Windows PowerShell cmdlet Restart-Service requires RunAsAdministrator.
    #>


    [CmdletBinding(HelpUri = 'https://jakehildreth.github.io/Locksmith/Invoke-Locksmith')]
    param (
        #[string]$Forest, # Not used yet
        #[string]$InputPath, # Not used yet

        # The mode to run Locksmith in. Defaults to 0.
        [Parameter()]
        [ValidateSet(0, 1, 2, 3, 4)]
        [int]$Mode = 0,

        # The scans to run. Defaults to 'All'.
        [Parameter()]
        [ValidateSet('Auditing',
            'ESC1',
            'ESC2',
            'ESC3',
            'ESC4',
            'ESC5',
            'ESC6',
            'ESC7',
            'ESC8',
            'ESC9',
            'ESC11',
            'ESC13',
            'ESC15',
            'EKUwu',
            'ESC16',
            'ESC17',
            'All',
            'PromptMe'
        )]
        [array]$Scans = 'All',

        # The directory to save the output in (defaults to the current working directory).
        [Parameter()]
        [ValidateScript({ Test-Path -Path $_ -PathType Container })]
        [string]$OutputPath = $PWD,

        # The credential to use for working with ADCS.
        [Parameter()]
        [System.Management.Automation.PSCredential]$Credential
    )

    $Version = '<ModuleVersion>'
    $LogoPart1 = @'
    _ _____ _______ _ _ _______ _______ _____ _______ _ _
    | | | | |____/ |______ | | | | | |_____|
    |_____ |_____| |_____ | \_ ______| | | | __|__ | | |
'@

    $LogoPart2 = @'
        .--. .--. .--.
       /.-. '----------. /.-. '----------. /.-. '----------.
       \'-' .---'-''-'-' \'-' .--'--''-'-' \'-' .--'--'-''-'
        '--' '--' '--'
'@

    $VersionBanner = " v$Version"

    Write-Host $LogoPart1 -ForegroundColor Magenta
    Write-Host $LogoPart2 -ForegroundColor White
    Write-Host $VersionBanner -ForegroundColor Red

    # Check if ActiveDirectory PowerShell module is available, and attempt to install if not found
    $RSATInstalled = Test-IsRSATInstalled
    if ($RSATInstalled) {
        # Continue
    } else {
        Install-RSATADPowerShell
    }

    # Exit if running in restricted admin mode without explicit credentials
    if (!$Credential -and (Get-RestrictedAdminModeSetting)) {
        Write-Warning "Restricted Admin Mode appears to be in place, re-run with the '-Credential domain\user' option"
        break
    }

    ### Initial variables
    # For output filenames
    [string]$FilePrefix = "Locksmith $(Get-Date -Format 'yyyy-MM-dd hh-mm-ss')"

    # Extended Key Usages for client authentication. A requirement for ESC1, ESC3 Condition 2, and ESC13
    $ClientAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.2|1\.3\.6\.1\.5\.2\.3\.4|1\.3\.6\.1\.4\.1\.311\.20\.2\.2|2\.5\.29\.37\.0'

    # Extended Key Usages for server authentication. A requirement for ESC17
    $ServerAuthEKUs = '1\.3\.6\.1\.5\.5\.7\.3\.1'

    # GenericAll, WriteDacl, and WriteOwner all permit full control of an AD object.
    # WriteProperty may or may not permit full control depending the specific property and AD object type.
    $DangerousRights = 'GenericAll|Write'

    # Extended Key Usage for client authentication. A requirement for ESC3.
    $EnrollmentAgentEKU = '1\.3\.6\.1\.4\.1\.311\.20\.2\.1'

    # The well-known GUIDs for Enroll and AutoEnroll rights on AD CS templates.
    $SafeObjectTypes = '0e10c968-78fb-11d2-90d4-00c04f79dc55|a05b8cc2-17bc-4802-a710-e7c15ab866a2'

    <#
        -519$ = Enterprise Admins group
    #>

    $SafeOwners = '-519$'

    <#
        -512$ = Domain Admins group
        -519$ = Enterprise Admins group
        -544$ = Administrators group
        -18$ = SYSTEM
        -517$ = Cert Publishers
        -500$ = Built-in Administrator
        -516$ = Domain Controllers
        -521$ = Read-Only Domain Controllers
        -9$ = Enterprise Domain Controllers
        -498$ = Enterprise Read-Only Domain Controllers
        -526$ = Key Admins
        -527$ = Enterprise Key Admins
        S-1-5-10 = SELF
    #>

    $SafeUsers = '-512$|-519$|-544$|-18$|-517$|-500$|-516$|-521$|-498$|-9$|-526$|-527$|S-1-5-10'

    <#
        S-1-0-0 = NULL SID
        S-1-1-0 = Everyone
        S-1-5-7 = Anonymous Logon
        S-1-5-32-545 = BUILTIN\Users
        S-1-5-11 = Authenticated Users
        -513$ = Domain Users
        -515$ = Domain Computers
    #>

    $UnsafeUsers = 'S-1-0-0|S-1-1-0|S-1-5-7|S-1-5-32-545|S-1-5-11|-513$|-515$'

    ### Generated variables
    # $Dictionary = New-Dictionary

    $Forest = Get-ADForest
    $ForestGC = $(Get-ADDomainController -Discover -Service GlobalCatalog -ForceDiscover | Select-Object -ExpandProperty Hostname) + ':3268'
    # $DNSRoot = [string]($Forest.RootDomain | Get-ADDomain).DNSRoot
    $EnterpriseAdminsSID = ([string]($Forest.RootDomain | Get-ADDomain).DomainSID) + '-519'
    $PreferredOwner = [System.Security.Principal.SecurityIdentifier]::New($EnterpriseAdminsSID)
    # $DomainSIDs = $Forest.Domains | ForEach-Object { (Get-ADDomain $_).DomainSID.Value }

    # Add SIDs of (probably) Safe Users to $SafeUsers
    Get-ADGroupMember $EnterpriseAdminsSID | ForEach-Object {
        $SafeUsers += '|' + $_.SID.Value
    }

    $Forest.Domains | ForEach-Object {
        $DomainSID = (Get-ADDomain $_).DomainSID.Value
        <#
            -517 = Cert Publishers
            -512 = Domain Admins group
        #>

        $SafeGroupRIDs = @('-517', '-512')

        # Administrators group
        $SafeGroupSIDs = @('S-1-5-32-544')
        foreach ($rid in $SafeGroupRIDs ) {
            $SafeGroupSIDs += $DomainSID + $rid
        }
        foreach ($sid in $SafeGroupSIDs) {
            $users += (Get-ADGroupMember $sid -Server $_ -Recursive).SID.Value
        }
        foreach ($user in $users) {
            $SafeUsers += '|' + $user
        }
    }
    $SafeUsers = $SafeUsers.Replace('||', '|')

    if ($Credential) {
        $Targets = Get-Target -Credential $Credential
    } else {
        $Targets = Get-Target
    }

    Write-Host "Gathering AD CS Objects from $($Targets)..."
    if ($Credential) {
        $ADCSObjects = Get-ADCSObject -Targets $Targets -Credential $Credential
        Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -Credential $Credential -ForestGC $ForestGC
        $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects -Credential $Credential -ForestGC $ForestGC
        $ADCSObjects += $CAHosts
    } else {
        $ADCSObjects = Get-ADCSObject -Targets $Targets
        Set-AdditionalCAProperty -ADCSObjects $ADCSObjects -ForestGC $ForestGC
        $CAHosts = Get-CAHostObject -ADCSObjects $ADCSObjects -ForestGC $ForestGC
        $ADCSObjects += $CAHosts
    }

    Set-AdditionalTemplateProperty -ADCSObjects $ADCSObjects

    # Add SIDs of CA Hosts to $SafeUsers
    $CAHosts | ForEach-Object { $SafeUsers += '|' + $_.objectSid }

    #if ( $Scans ) {
    # If the Scans parameter was used, Invoke-Scans with the specified checks.
    $ScansParameters = @{
        ADCSObjects        = $ADCSObjects
        ClientAuthEkus     = $ClientAuthEKUs
        ServerAuthEKUs     = $ServerAuthEKUs
        DangerousRights    = $DangerousRights
        EnrollmentAgentEKU = $EnrollmentAgentEKU
        Mode               = $Mode
        SafeObjectTypes    = $SafeObjectTypes
        SafeOwners         = $SafeOwners
        SafeUsers          = $SafeUsers
        Scans              = $Scans
        UnsafeUsers        = $UnsafeUsers
        PreferredOwner     = $PreferredOwner
    }
    $Results = Invoke-Scans @ScansParameters
    # Re-hydrate the findings arrays from the Results hash table
    $AllIssues = $Results['AllIssues']
    $AuditingIssues = $Results['AuditingIssues']
    $ESC1 = $Results['ESC1']
    $ESC2 = $Results['ESC2']
    $ESC3 = $Results['ESC3']
    $ESC4 = $Results['ESC4']
    $ESC5 = $Results['ESC5']
    $ESC6 = $Results['ESC6']
    $ESC7 = $Results['ESC7']
    $ESC8 = $Results['ESC8']
    $ESC9 = $Results['ESC9']
    $ESC11 = $Results['ESC11']
    $ESC13 = $Results['ESC13']
    $ESC15 = $Results['ESC15']
    $ESC16 = $Results['ESC16']
    $ESC17 = $Results['ESC17']

    # If these are all empty = no issues found, exit
    if ($null -eq $Results) {
        Write-Host "`n$(Get-Date) : No ADCS issues were found.`n" -ForegroundColor Green
        Write-Host 'Thank you for using ' -NoNewline
        Write-Host "❤ Locksmith ❤ `n" -ForegroundColor Magenta
        break
    }

    switch ($Mode) {
        0 {
            Format-Result -Issue $AuditingIssues -Mode 0
            Format-Result -Issue $ESC1 -Mode 0
            Format-Result -Issue $ESC2 -Mode 0
            Format-Result -Issue $ESC3 -Mode 0
            Format-Result -Issue $ESC4 -Mode 0
            Format-Result -Issue $ESC5 -Mode 0
            Format-Result -Issue $ESC6 -Mode 0
            Format-Result -Issue $ESC7 -Mode 0
            Format-Result -Issue $ESC8 -Mode 0
            Format-Result -Issue $ESC9 -Mode 0
            Format-Result -Issue $ESC11 -Mode 0
            Format-Result -Issue $ESC13 -Mode 0
            Format-Result -Issue $ESC15 -Mode 0
            Format-Result -Issue $ESC16 -Mode 0
            Format-Result -Issue $ESC17 -Mode 0
            Write-Host @"
[!] You ran Locksmith in Mode 0 which only provides an high-level overview of issues
identified in the environment. For more details including:
 
  - Detailed Risk Rating
  - General remediation guidance and/or code for all issues
  - Custom remediation guidance and/or code for some issues!
  - Revert guidance and/or code (in case remediation breaks something!)
  - Distinguished Name of impacted object(s)
 
Try Mode 1!
 
# Module version
Invoke-Locksmith -Mode 1
 
# Script version
.\Invoke-Locksmith.ps1 -Mode 1`n
"@
 -ForegroundColor Yellow
        }
        1 {
            Format-Result -Issue $AuditingIssues -Mode 1
            Format-Result -Issue $ESC1 -Mode 1
            Format-Result -Issue $ESC2 -Mode 1
            Format-Result -Issue $ESC3 -Mode 1
            Format-Result -Issue $ESC4 -Mode 1
            Format-Result -Issue $ESC5 -Mode 1
            Format-Result -Issue $ESC6 -Mode 1
            Format-Result -Issue $ESC7 -Mode 1
            Format-Result -Issue $ESC8 -Mode 1
            Format-Result -Issue $ESC9 -Mode 1
            Format-Result -Issue $ESC11 -Mode 1
            Format-Result -Issue $ESC13 -Mode 1
            Format-Result -Issue $ESC15 -Mode 1
            Format-Result -Issue $ESC16 -Mode 1
            Format-Result -Issue $ESC17 -Mode 1
        }
        2 {
            $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSIssues.CSV"
            Write-Host "Writing AD CS issues to $Output..."
            try {
                $AllIssues | Select-Object Forest, Technique, Name, Issue, @{l = 'Risk'; e = { $_.RiskName } } | Export-Csv -NoTypeInformation $Output
                Write-Host "$Output created successfully!`n"
            } catch {
                Write-Host 'Ope! Something broke.'
            }
        }
        3 {
            $Output = Join-Path -Path $OutputPath -ChildPath "$FilePrefix ADCSRemediation.CSV"
            Write-Host "Writing AD CS issues to $Output..."
            try {
                $AllIssues | Select-Object Forest, Technique, Name, DistinguishedName, Issue, Fix, @{l = 'Risk'; e = { $_.RiskName } }, @{l = 'Risk Score'; e = { $_.RiskValue } }, @{l = 'Risk Score Detail'; e = { $_.RiskScoring -join "`n" } } | Export-Csv -NoTypeInformation $Output
                Write-Host "$Output created successfully!`n"
            } catch {
                Write-Host 'Ope! Something broke.'
            }
        }
        4 {
            $params = @{
                AuditingIssues = $AuditingIssues
                ESC1           = $ESC1
                ESC2           = $ESC2
                ESC3           = $ESC3
                ESC4           = $ESC4
                ESC5           = $ESC5
                ESC6           = $ESC6
                ESC11          = $ESC11
                ESC13          = $ESC13
            }
            Invoke-Remediation @params
        }
    }
    Write-Host 'Thank you for using ' -NoNewline
    Write-Host 'Locksmith <3 ' -ForegroundColor Magenta -NoNewline
    Write-Host "(https://github.com/jakehildreth/Locksmith)`n"
}