All notable changes to adPEAS will be documented in this file.
The format is based on Keep a Changelog.
Get-GPORegistrySettings— new GPO check that flags security-relevant registry values deployed via Group Policy which, when actively set, enable an attack (credential theft, lateral movement, privilege escalation, defense evasion). Only positively-set values are reported — absence of a hardening value is never flagged, because GPO/SYSVOL parsing cannot distinguish "not configured in this GPO" from "secure". Both delivery mechanisms are parsed from SYSVOL: Administrative Templates (Registry.pol, PReg binary format) and Group Policy Preferences (Registry.xml). The dangerous-value catalogue is centralised inadPEAS-RegistryKeys.ps1($Script:DangerousRegistryKeys) and covers, among others: the Zerologon/OneLogonVulnerableChannelAllowList(Critical),AlwaysInstallElevated(Critical, only when both HKLM and HKCU are set), WDigest cleartext caching,LocalAccountTokenFilterPolicy/EnableLUA(remote pass-the-hash), RDP Restricted Admin, Point and Print (PrintNightmare), WSUS-over-HTTP, weak LM/NTLMv1, plus explicitly disabled defenses (Defender, LSA Protection, Credential Guard, SMB signing). Each finding is mapped to the affected OUs / domain-wide scope via GPO links and ships with an HTML report card and hover tooltip. Scope is GPO-deployed values only — values set locally on a host are out of scope by design (no Remote Registry; consistent with the LDAP+SMB, no-RSAT model). Inspired by OneLogon (https://github.com/rub-softsec/onelogon).
-
Silent fallback to a stale session when an explicit
-Credentialbind failed.Connect-LDAPdoes not throw on a failed bind — it displays the reason (e.g. LDAP 49 invalid credentials) and returns$null, leaving any pre-existing session in$Script:LDAPContext/$Script:LdapConnectionintact.Ensure-LDAPConnectiondiscarded that return value (| Out-Null) and then reported "Successfully connected" using the stale context, so a command run with wrong/mistyped credentials (e.g.Set-DomainUser -Credential …) silently proceeded under the previous identity instead of failing. It now checks theConnect-LDAPreturn value and returns$falseon failure, so the caller aborts with the real authentication error instead of operating as the wrong principal. -
Consistent web endpoint reporting for ADCS/Exchange probes. When a web endpoint probe ran against a reachable, active server but did not succeed (e.g. a CA's web enrollment endpoint blocked during a VPN scan), it silently produced no output — indistinguishable from "the service exposes no web endpoint". A central
Show-WebEndpointUnreachable(Core/adPEAS-Messages) now emits a uniform Note for both ADCS and Exchange; inactivity-skipped servers stay silent. Additionally, HTTP/HTTPS availability is now tracked per endpoint (AvailableHttp/AvailableHttps) inInvoke-HTTPRequest, so HTTPS-only endpoints (ADCS CEP/CES, Exchange EWS/MAPI/RPC/PowerShell/ActiveSync) are no longer dropped from the endpoint list; global transport flags are derived additively so HTTP-only, HTTPS-only and mixed cases all render correctly. -
Actionable error messages for directory write operations. When a write (
New-DomainComputer/User/Group/GPO,Set-Domain*, RBCD and Shadow Credential operations) is rejected by a Domain Controller — common after AD hardening — the LDAPSendRequestthrows aDirectoryOperationExceptionthat PowerShell wraps, so the tester previously saw only the generic "The server cannot handle directory requests." with no LDAP result code and no AD sub-error. A newResolve-LDAPWriteErrordecoder (adPEAS-ErrorCodes.ps1) unwraps the exception chain, extracts the LDAPResultCodeand the server-side extended sub-error (the 8-hex-digit prefix, e.g.0000216D= MachineAccountQuota exhausted/0,00002098= insufficient access rights), and emits a "Likely cause: …" hint. Falls back to the raw message when the failure is not a directory write, so no information is ever lost.-PassThruresult objects now also carryResultCode/ResultName. -
lockoutDurationnonsensical value in the password policy check. When a domain sets the account lockout duration to "until an administrator unlocks" (AD stores this as theInt64.MinValue/0x8000000000000000"never" sentinel),Get-DomainPasswordPolicydivided the sentinel instead of recognising it and printedlockoutDuration: 15372286728.0913 minutes. It now applies the sameInt64.MinValueguard thatmaxPwdAgealready had, to bothlockoutDuration(→ "Forever (manual unlock)") andlockoutObservationWindow. Normal values are unchanged. (Root cause is a latent mislabelling of the interval attributes as FileTime attributes inInvoke-LDAPSearch, left in place because the BloodHound collector deliberately relies on the raw passthrough.)
-
Get-GPOUserRightsAssignment— new Rights check that parses the[Privilege Rights]section ofGptTmpl.infin every GPO and flags sensitive Windows user rights assigned to non-privileged principals. Two tiers: privilege- escalation/credential privileges (SeDebug, SeBackup, SeRestore, SeTakeOwnership, SeImpersonate, SeAssignPrimaryToken, SeCreateToken, SeTcb, SeLoadDriver, SeEnableDelegation, SeSyncAgent, SeManageVolume, SeSecurity, SeRelabel, SeTrustedCredManAccess) → Finding; logon rights (RDP/service/batch/interactive, change system time, shutdown) → Hint. Rights granted to broad principals (Everyone, Authenticated Users, Domain Users) are escalated to Finding. Each finding is mapped to the affected OUs / domain-wide scope via GPO links; privileged principals, built-in operator groups and well-known service/builtin default holders are hidden unless-IncludePrivileged. Closes the gap vs. dedicated GPO parsers — adPEAS previously only detectedSeMachineAccountPrivilege(via Get-AddComputerRights). -
Set-DomainGPO— Backup/Revert for GPO modifications, aligned to theSet-CertificateTemplate -Export/-Importidiom (operator-driven, no automatic backup — "the tester must know what they're doing"):-Export <path>snapshots the full restorable GPO state to one JSON: AD attributes (gPCMachineExtensionNames,gPCUserExtensionNames,versionNumber,nTSecurityDescriptoras SDDL) plus a recursive base64 copy of the entire SYSVOL tree.-Import <path>restores the GPO server-side: rewrites SYSVOL to the snapshot, deletes files injected after the backup, restores the extension attributes and version, and warns on security-descriptor drift.- Surgical reverse switches to remove a single injected payload by name without a
JSON:
-RemoveScheduledTask,-RemoveLocalGroupMember,-RemoveService,-RemoveDeployedFile,-RemoveFirewallRule,-RemoveStartupScript,-RemoveLogonScript. Each strips the corresponding Client-Side-Extension from the AD extension list when the last payload of that type is gone and bumps the GPO version. - Note: restores the GPO definition (server-side); effects a GPP already applied on
clients are not auto-reverted (roadmap:
-ClientRevert).
-
Get-BitLockerRecoveryKeyAccess— new Creds check that lists the BitLocker recovery keys the current user can read from AD. BitLocker recovery information is escrowed asmsFVE-RecoveryInformationchild objects below each computer (not as a computer attribute); the check runs a single domain-wide, server-side filtered subtree query(&(objectClass=msFVE-RecoveryInformation)(msFVE-RecoveryPassword=*)). The presence filter is ACL-gated, so only readable keys are returned — no per-computer enumeration. A schema/feasibility check short-circuits (and is cached for the session) so domains without BitLocker escrow skip the query entirely. The owning computer name is derived from the parent DN without extra LDAP queries, and recovery/volume GUIDs are decoded for display. Reported as a Hint (yellow) since read access is often a legitimate recovery/helpdesk capability, with full HTML report card and hover tooltip.
- HTML report card titles — object cards whose title template referenced a
custom property placeholder rendered it literally. The "GPO User Right" card
showed
GPO User Right: {userRightName}instead of the resolved right name (e.g. Manage auditing and security log).Get-ObjectTypeTitlenow resolves any remaining{propertyName}placeholder from the matching object property, which also fixes the BitLocker Recovery Key card's{ComputerName}placeholder.
Request-ADCSCertificate— ESC3 (enrollment agent) via-OnBehalfOf/-PFX/-PFXPassword: enroll-on-behalf-of another user using an enrollment-agent certificate (PKCS#7 SignedData).Request-ADCSCertificate— ESC13 / ESC15 (EKUwu) via-ApplicationPolicies(raw OIDs or friendly names), injected through theszOID_APPLICATION_CERT_POLICIESextension.Request-ADCSCertificate— retrieve pending requests via-RetrieveID/-KeyFile; the private key is persisted when a request goes pending so it can be completed later.Request-ADCSCertificate— new request controls:-SID(with LDAP auto-resolution) /-NoSID,-Method Auto/Web/COM,-CAName, and-Portfor a custom certsrv Web Enrollment port.
- Diamond Ticket now "recuts" from the genuine PAC instead of
rebuilding a synthetic one. It preserves the real identity/session
fields,
PrimaryGroupIdandUserAccountControlfrom the base TGT and only appends the requested group(s), removing the synthetic fingerprints (default group set 512/513/518/519/520) that are a known IOC. Diamond without-GroupRIDsnow injects only Domain Admins (512). Golden/Silver output is byte-identical to before. Invoke-TicketForge -PTTnow warns when Windows cannot locate a KDC for the ticket's realm (host not joined, no_kerberos._tcp.<realm>SRV record), soSEC_E_NO_LOGON_SERVERSis not mistaken for an invalid ticket. Suggests an NRPT rule / ksetup mapping as the fix.
New-DomainComputerfailed to create accounts via MachineAccountQuota — the LDAP AddRequest setdNSHostNameanduserPrincipalNameat creation time, which AD validated-writes reject ("A value in the request is invalid") for unprivileged MAQ creators, aborting the whole creation. These are now set best-effort after creation, so the account is always created.Request-ADCSCertificate(COM/RPC) leaked a Base64 string instead of decoding the issued certificate (now X509 / PKCS#7-aware), and the COM/RPC fallback is now reachable when/certsrv/web enrollment is absent.- Requester SID dropped from the issued certificate — the SID is now
also embedded in the SAN as a URL
(
tag:microsoft.com,2022-09-14:sid:) in addition to the NTDS CA Security Extension, so it survives CAs that strip the requester-supplied extension (matches Certipy).
Get-DomainUser/Get-Domain*silently dropped attributes (e.g.objectSid,displayName,userPrincipalName,accountExpires)terminalServer/userParametersshown as raw byte arrays — both are now decoded to readable text (TS per-user settings and Per-User CAL tracking token respectively)protocolSettings(Exchange) shown as cryptic§-separated strings — now rendered as<Protocol>: enabled/disabledper entry, dropping the per-user encoding/use-defaults flags that are noise for security review- ADCS certificate templates not rendered to console — templates were
collected but their attribute conversion never reached
Show-Object - Certificate Authority common name shown under generic
displayNamelabel — CA-specific name field now used - Groups falsely flagged as
INACTIVE— the activity check applied the user-onlylastLogonTimestampheuristic unconditionally; groups never log on, so they always tripped the heuristic - GPO findings with empty
LinkedOUsrendered blank — now shown explicitly asNot linked - Overpass-the-Hash (RC4) failed with AS-REP decryption error
- Relative file paths resolved against the process directory instead
of the current PowerShell location (
$PWD)
- Resizable name column in HTML report — drag the column divider per object card to widen/narrow the attribute-name column (Excel-style), e.g. for screenshots. Transient only, resets to default on reload.
- GPO/object attribute reordering in HTML report now uses a dedicated drag handle (small grip at the left of each row) instead of making the whole row draggable, so row text stays selectable.
- Umlauts garbled in GPO local group / scheduled task findings — GPP
Groups.xml/ScheduledTasks.xmlare UTF-8, but were read with the ANSI code page (Windows PowerShell 5.1 default without BOM), mojibaking names such as "Domänen-Benutzer" → "Domänen-Benutzer" - Inactive accounts shown as active when the AD object had no
lastLogonTimestamp(very old / never-used accounts, e.g. stale computers found via SPN) — activity status now falls back topwdLastSet/whenCreated - HTML report card text could not be selected or copied — finding and object card content is now freely selectable; selecting text no longer expands/collapses the card
- Crash on second
Invoke-adPEAScall —op_Subtractionerror when runningInvoke-adPEASa second time in the same session.Connect-adPEAScleared$Script:StartTimeon reconnect, breaking the end-of-scan duration calculation -OPSECmode still listed Bloodhound in the module overview (Executing Modules: ..., Bloodhound) even though collection was correctly skipped — confusing for users who couldn't tell at a glance whether BloodHound had run- GPO Local Group card title showed "Local Group" placeholder instead of the actual group name
- LDAP "Not Configured" signing/channel-binding values were shown as Hint instead of Finding in GPO analysis
- SMB Signing DC-only configuration was shown as Hint even when server signing is Required — member servers fall back to OS defaults, so this should be a Finding
- Certificate template ACL was tagged as Primary finding even when the current low-privileged user had no write rights — now Primary only when actually exploitable by the running identity
Connect-adPEAScache cleanup on reconnect was incomplete — stale cached state from the previous session could leak into the new one
- Certificate template ACL display in ADCS report — shows who has write/modify rights on each template (relevant for ESC4 context)
- BloodHound CE v6.2 compatibility — major collector overhaul to fix
import errors:
- Replace
LocalAdmins/RemoteDesktopUsers/DcomUsers/PSRemoteUserswithLocalGroups/UserRights/DumpSMSAPassword(SharpHound v2.12 format) CARegistryDatamoved to top-level field (was causing Neo4j Map{} errors)IsWebClientRunningandSmbInfocorrectly set tonullin DCOnly modeserviceprincipalnamesguard against empty LDAP hashtable (@{})HasSIDHistorynow a TypedPrincipal array instead of boolIssuancePolicy.GroupLinknow a TypedPrincipal instead of raw string- ADCS flags (
enrollmentflag,certificatenameflag,flags) converted to strings as required by BH CE v6.2 - ~50 missing Properties and top-level fields added across all 13 object types (users, groups, computers, domain, OUs, containers, GPOs, cert templates, enterprise CAs, root CAs, AIA CAs, NTAuth stores, issuance policies)
- JSON output now compact by default;
PrettyPrintopt-in - Missing helper functions
Convert-CAFlagToString,Convert-CertNameFlagToString,Convert-EnrollFlagToStringadded (absence caused runtime crash on Enterprise CA collection) $sidHistoryTypednow correctly built as TypedPrincipal array (was undefined —HasSIDHistorywas alwaysnull)- Null-reference crashes on trust objects, OUs, and containers fixed
- Replace
- NTAuth Certificate / AIA CA showed "Unknown" as name in PKI Trust Infrastructure card — now correctly resolves CN from Subject DN
- BloodHound collection crash on OUs/containers without
objectGuid - Top Priority Action click in HTML report now scrolls to card header
- GPO link order priority — SMB signing and LDAP configuration checks now
show which GPO is effectively applied (
IsEffectiveSetting) based on GPO link order precedence (DC OU beats Domain-level) - HTML diff report in
Compare-adPEASReportfor visual scan comparison - PublishedOn property on certificate templates
- scriptPath display for user accounts now shows with conditional severity
- GPO local group membership check not reporting findings
- Add-Computer severity now correctly distinguishes restricted scope (Administrators only → secure) from broad scope (Authenticated Users, Everyone → finding)
Initial release of adPEAS v2 — a complete rewrite of adPEAS v1.
- Unified LdapConnection architecture replacing legacy DirectoryEntry/DirectorySearcher
- Native Kerberos stack — AS-REQ/AS-REP, TGS-REQ/TGS-REP, Pass-the-Ticket, all in pure PowerShell
- Authentication methods — Password, NT-Hash (OPtH), AES keys (PtK), PKINIT, Pass-the-Cert/Schannel, Windows integrated auth
- Kerberos-first authentication with automatic fallback to NTLM Impersonation and SimpleBind
- UnPAC-the-Hash — automatic NT-Hash recovery after PKINIT authentication
- 41+ security checks across 9 categories (Domain, Creds, Rights, Delegation, ADCS, Accounts, GPO, Computer, Application)
- ADCS vulnerability detection — ESC1 through ESC15
- Severity scoring system with Critical/High/Medium/Low/Info levels and risk scores
- Interactive HTML reports with search, filtering, sorting, dark/light theme, and tooltips
- JSON export for machine-readable output, offline report conversion, and scan comparison
- Report comparison (
Compare-adPEASReport) — diff two scans to track remediation progress - Offline report conversion (
Convert-adPEASReport) — regenerate reports from JSON without LDAP connection - Incremental scanning with
-OutputAppendto merge findings across multiple runs - BloodHound CE collector — built-in data collection for attack path analysis
- Offensive operations — Kerberoasting, AS-REP Roasting, Golden/Silver/Diamond Tickets, RBCD abuse, Shadow Credentials, GPO abuse
- Session-based workflow — connect once, run multiple checks interactively
- Tab-completion for AD object names (
-BuildCompletionCache) - OPSEC mode — skip active testing (Kerberoast, ASREPRoast, BloodHound)
- Verbose logging to file for troubleshooting (
-VerboseLogging) - LDAP timeout configuration (
-TimeoutSeconds) for slow connections (SOCKS, VPN) - RSA-SHA256 license system with build-time embedding and runtime override
- Four release variants — readable, minimized, ultra-compressed, and obfuscated
- Zero external dependencies — no RSAT, no ActiveDirectory module, no PowerView
- All LDAP operations use
System.DirectoryServices.Protocols.LdapConnection - Modular source structure (
src/modules/) compiled into single standalone.ps1file - SID-based identity checks for language-independent operation
- PowerView dependency
- DirectoryEntry/DirectorySearcher usage