Running PowerShell as the Logged-On User from SYSTEM Context
If you have ever deployed a PowerShell script through Intune, a RMM agent, or Task Scheduler running as SYSTEM, you have hit this wall at least once: the script works perfectly when you run it interactively, but returns nothing — or the wrong thing — when deployed at scale.
The reason is almost always the same. The script is collecting user-specific data. And SYSTEM is not the user.
The Problem: SYSTEM and the User Are Not the Same Session
When Intune or your RMM agent executes a PowerShell script, it runs in the SYSTEM context. SYSTEM is a highly privileged account, but it is completely isolated from the interactive user session happening on the same machine at the same time.
This means:
HKCU:\registry reads return nothing — SYSTEM’s hive has no meaningful datagpresult /r /scope userproduces output for SYSTEM, not the logged-on userGet-Printerlists system-level printers, not the user’s mapped printers- Drive mappings, folder redirections, and shell folder customisations are all invisible
If you are trying to audit what GPOs are applied to a user, what printers they have, or what their shell folder paths are — you need to be in their session. Not SYSTEM’s.
The Traditional Workaround and Why It Falls Short
The typical approach is to grab the current username from a WMI query (Win32_ComputerSystem.UserName), build a remote path to their profile hive, and mount it with reg.exe load. Then you query offline.
That works for registry reads. But it does not work for:
gpresult— this requires live policy application, not offline hive inspection- Printer enumeration — printers are session-aware
- Any COM or shell-based enumeration that requires a live user token
For those cases you actually need to run code inside the user’s session, impersonating their token. That is exactly what Invoke-VBasCurrentUser does.
How It Works
The function embeds a C# extension (RunAsUser.ProcessExtensions) that is compiled inline on first use and cached for the session — no external module or install step required. It uses Windows impersonation APIs to:
- Enumerate active WTS sessions to find the interactive user
- Call
WTSQueryUserTokento get their session token - Duplicate the token with
DuplicateTokenEx - Build a matching environment block with
CreateEnvironmentBlock - Call
CreateProcessAsUserto spawn a new PowerShell process under that token
The result is a real PowerShell process running as the logged-on user, started from SYSTEM — with the user’s full token, their HKCU, their session printers, their applied GPOs.
The only requirement on the calling side is SeDelegateSessionUserImpersonatePrivilege, which SYSTEM always has.
The Function
Invoke-VBasCurrentUser lives in the VB.WorkstationReport module. It is a standards-compliant wrapper around the original logic — all parameters are preserved. The C# source is embedded directly in the .ps1 file and compiled on first use via Add-Type. Once compiled it is cached for the PowerShell session, so subsequent calls have no compilation overhead. No external module install, no dependency check, no prerequisites beyond SYSTEM privileges.
# Install the dependency once
Install-Module RunAsUser -Scope AllUsers
# Import the module
Import-Module VB.WorkstationReport
Use Cases and Examples
1 — Enumerate Applied GPOs for the Logged-On User
The classic trigger for this function. When you deploy a GPO troubleshooting script via Intune or an RMM agent, gpresult run as SYSTEM tells you what policies apply to SYSTEM — not the user.
Invoke-VBasCurrentUser -ScriptBlock {
gpresult /r /scope user | Out-File 'C:\Temp\gpo_report.txt' -Encoding UTF8
}
After the script completes, C:\Temp\gpo_report.txt contains the GPO Resultant Set of Policy for the actual logged-on user. Pull it back through your RMM or upload it to a central store.
2 — Write the Username into the Output Path
Sometimes you want the report to be named after the user. Use -ExpandStringVariables to expand variables from the SYSTEM calling scope into the user-context scriptblock before it runs.
$LoggedOnUser = (Get-CimInstance -ClassName Win32_ComputerSystem).UserName -replace '.*\\', ''
Invoke-VBasCurrentUser -ScriptBlock {
gpresult /r /scope user | Out-File "C:\Temp\gpo_$LoggedOnUser.txt" -Encoding UTF8
} -ExpandStringVariables
Without -ExpandStringVariables, $LoggedOnUser would be evaluated inside the spawned process (where it is not defined) and would be empty.
3 — Enumerate Printers in the User Session
Get-Printer is session-aware. Running it as SYSTEM returns system-managed printers only, not the user’s mapped UNC or IP printers. Running it as the current user gives you the full picture.
Invoke-VBasCurrentUser -ScriptBlock {
Get-Printer | Select-Object Name, Type, PortName, PrinterStatus |
Export-Csv 'C:\Temp\printers.csv' -NoTypeInformation -Encoding UTF8
} -CaptureOutput
4 — Export HKCU Group Policy Registry Branch
For environments where you need the raw registry state rather than the formatted gpresult output:
Invoke-VBasCurrentUser -ScriptBlock {
$RegPath = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Group Policy'
reg export $RegPath 'C:\Temp\gpo_hkcu.reg' /y
}
5 — Long Scripts: Avoid the Command Line Length Limit
Base64-encoding a large scriptblock can push past the OS command line limit (32767 chars on Windows 8+). Use -CacheToDisk to write the script to a temp file instead.
Invoke-VBasCurrentUser -ScriptBlock {
# long multi-step audit scriptblock
# ...
} -CacheToDisk
The temp file is cleaned up automatically after execution.
6 — Fire and Forget
When you want to kick off a process in the user session without blocking the SYSTEM script:
Invoke-VBasCurrentUser -ScriptBlock {
Start-Process 'C:\Tools\UserAudit.exe'
} -NoWait
Putting It Together: Full GPO Audit Pattern
A common real-world pattern for RMM deployment:
# Step 1 -- Resolve the logged-on username from SYSTEM
$LoggedOnUser = (Get-CimInstance -ClassName Win32_ComputerSystem).UserName -replace '.*\\', ''
$OutputFile = Join-Path 'C:\Temp' "GPO_${LoggedOnUser}_$(Get-Date -Format 'yyyyMMdd-HHmm').txt"
# Step 2 -- Ensure output folder exists
if (-not (Test-Path 'C:\Temp')) { New-Item -ItemType Directory -Path 'C:\Temp' | Out-Null }
# Step 3 -- Run gpresult in the user session
Invoke-VBasCurrentUser -ScriptBlock {
gpresult /r /scope user | Out-File $OutputFile -Encoding UTF8
} -ExpandStringVariables
# Step 4 -- Confirm the file was created
if (Test-Path $OutputFile) {
Write-Output "GPO report saved: $OutputFile"
} else {
Write-Warning "GPO report not created -- check Invoke-VBasCurrentUser output"
}
What It Will Not Do
A few things worth being clear about:
- Requires an active interactive session. If no user is logged on, the WTS token query fails and the function writes an error.
- One logged-on user only. On a terminal server or multi-session host with several active users, the function targets the first active session found. It is not designed for multi-session enumeration.
- No pipeline support. This function executes a scriptblock. It does not return structured objects. If you need structured data back, write the output to a file inside the scriptblock and read the file from the SYSTEM script after execution.
Installation
# Install or update VB.WorkstationReport -- no other dependencies needed
Install-Module VB.WorkstationReport -Scope AllUsers
# or if pulling from the local repo:
Import-Module C:\Path\To\VB.WorkstationReport\VB.WorkstationReport.psd1
Invoke-VBasCurrentUser is in the Public\ folder and is exported automatically. The C# type compiles on first call and is cached for the session — nothing else to install.
Source
The function is part of the VB.WorkstationReport module. The embedded C# impersonation logic is adapted from the open-source RunAsUser module by KelvinTegelaar, published under the MIT licence.
📦 Related PowerShell Module
This post relates to VB.WorkstationReport — available on PowerShell Gallery.
Install-Module -Name VB.WorkstationReport -Repository PSGallery🤝 Connect with Me
Found this useful? I write about PowerShell, Windows infrastructure, and enterprise automation.
