OSCE3 study materials
OSCE3 study resources

Persistence and C2 in a non-persistent VDI pool

OSCE3 · Put your knowledge to work in context.

All case studies

Persistence and C2 in a non-persistent VDI pool

Your phishing payload lands a C# implant in memory in a Windows 11 23H2 VDI session. The pool is non-persistent: at 03:00 the session is logged off and the VM is recomposed from a gold image, so anything written outside the user's FSLogix profile container disappears. The profile, including NTUSER.DAT, lives on \fslogix\profiles and is retained. Defender for Endpoint is deployed with ASR rules that block Office child processes, and ETW detections fire on WMI event subscriptions, service creation, and scheduled tasks created outside standard paths. Egress goes through an authenticated proxy at 10.10.5.20:8080 that performs TLS inspection with a corporate root CA present in the machine store; only *.svc-contoso.example on 443 is allowed. DNS is internal-only and resolves nothing external. You are a standard user with code execution in your own session only. You need persistence across the 03:00 reset and a C2 channel that survives it.

1. Design persistence that survives the 03:00 recompose, and explain why each of the obvious alternatives fails.

Read the worked answer

Start from what actually persists: only the FSLogix container. Anything on the VM's system disk is gone at 03:00, so the payload and its trigger both have to live inside the profile.

Survivors:

  • The payload sits under %APPDATA% (redirected into the FSLogix VHDX), not under C:\ProgramData or %TEMP%.
  • Trigger: HKCU\Software\Classes\CLSID\{...}\InprocServer32 -> your DLL, hijacking a CLSID that explorer.exe or the Teams client loads at logon. It lives in NTUSER.DAT, which is inside the retained container, and it fires in the user's context without any admin right.

Why the alternatives fail here:

  • Services and WMI subscriptions need admin, are wiped on recompose, and are exactly what the deployed ETW detections watch.
  • Scheduled tasks created outside standard paths are an explicit detection in this environment, and the task files themselves sit outside the container.
  • HKCU\...\Run would technically persist (it is in NTUSER.DAT), but it is the highest-signal key on the box and ASR coverage on this estate is tuned for it. Use it only as a fallback.
  • Anything in %LOCALAPPDATA% only survives if folder redirection covers it - verify, do not assume.

Validation: write a canary into %APPDATA%, log off and back on, confirm both the canary and the CLSID key survived before you commit.

2. Design the C2 channel against the proxy and TLS-inspection constraints, and state the one property of your implant that will break it if you get it wrong.

Read the worked answer

The constraints are narrow enough to dictate the design: hostname allowlist, terminating proxy, internal-only DNS.

  • Address: a hostname under svc-contoso.example that you control (CNAME to a relay you own). The SNI and the CONNECT target must both be inside the allowlist; domain fronting dies because the proxy inspects SNI and Host.
  • Name resolution: never resolve the name yourself - DNS answers nothing external. Let the proxy do the CONNECT by hostname.
  • Proxy auth: use WinHTTP's default proxy configuration (WinHttpGetIEProxyConfigForDefaultProxy + WINHTTP_OPTION_PROXY, with WINHTTP_OPTION_AUTOLOGON_POLICY at MEDIUM) so SSPI negotiates the machine/user credential for you. Hand-rolling a Proxy-Authorization header is the usual reason this fails.
  • TLS: the proxy terminates and re-originates, so the leaf certificate you see is not yours. Trust the machine store (the corporate root is there) and never pin a leaf or a public key. Redirect settings matter: WINHTTP_OPTION_REDIRECT_POLICY_NEVER so a 302 does not leak you to a host outside the allowlist.
  • Shape: HTTPS only, browser-plausible User-Agent and headers, jittered sleep, small request/response bodies - the proxy almost certainly filters on UA and on non-HTTP flows.

The one property that breaks it: certificate pinning of your own leaf (or a self-signed cert). With inspection in the path it is indistinguishable from a MITM and the channel never comes up - which you would misread as a network block.

3. You have 40 minutes before the next 03:00 reset. What single test tells you whether the whole design holds?

Read the worked answer

Do not wait for 03:00 - reproduce the reset with your own logoff, because that is the only step you control as a standard user.

  1. Write a timestamped canary to %APPDATA% and to %LOCALAPPDATA% so you learn exactly which subtree FSLogix retains.
  2. Plant the CLSID hijack pointing at a harmless DLL that writes a file to the profile and beacons once.
  3. Log off, wait for the profile to unload from the share, log back on, and check: canary present? DLL loaded? one beacon received?

Interpretation:

  • Canary gone from both subtrees -> the container is not retaining AppData for this pool; pivot to OneDrive Known Folder Move redirection or a startup entry in a redirected folder, and re-test.
  • Canary present but no beacon -> the CLSID is not being loaded by anything on this image; pick a different CLSID that explorer or the logon stack genuinely touches, or accept a Run key with the detection cost.
  • Beacon present but from the wrong path -> your DLL is being loaded out of a temp copy; check that the path is inside the container.

Do this once, deliberately, rather than discovering a broken chain at 03:05 with the session already gone.