Ditching PsExec: Running Interactive SYSTEM Shells Natively in PowerShell

If you’ve spent any time in Windows System Administration over the last decade, I can almost guarantee you’ve reached for PsExec at least once. Originally from Sysinternals and now officially part of Microsoft, PsExec is one of those deceptively simple tools that has quietly saved thousands of IT professionals from hours of sheer agony. A single executable, zero installation, no messy dependencies. You drop it on a machine, and it just works.

If you have never had the pleasure, here is the short version: PsExec lets you execute processes on remote systems interactively, exactly as if you were sitting right in front of them. You can run commands on a remote server, push a script across a fleet of machines, and redirect input and output over the network—all without needing to spin up a full remote desktop session. It works beautifully for local troubleshooting too, allowing you to run processes under different user accounts or with specific privileges in ways that Windows simply doesn’t expose through a right-click.

The Trick That Started Everything

But here is the specific feature that genuinely stopped me in my tracks the first time I discovered it:

PsExec can run any application as the SYSTEM account—directly on your interactive desktop—as long as you call it from an elevated session.

code
<code><span style="color: #75715e"># Launch an interactive PowerShell session as SYSTEM</span>
psexec -i -s powershell.exe</code>

That is it. One line. A PowerShell window pops up on your screen, and if you type whoami inside it, you get back:

code
<code><span style="color: #75715e"># The ultimate privilege confirmation</span>
nt authority\system</code>

I remember just staring at that for a moment. SYSTEM. On the interactive desktop. Visible, interactive, and fully usable. Not some background service process hopelessly hidden away in Session 0. An actual, tangible window right in front of me, running with the absolute highest local privilege the Windows OS possesses.

If you have ever had to troubleshoot why a scheduled task behaves differently from your own session, debug a service that only misbehaves under the SYSTEM context, test registry permissions, or validate that a software deployment will actually work in production—this single trick is invaluable. It became a permanent fixture in my toolkit almost immediately.

Yes, Solutions Already Exist

Before I go any further, let me be entirely upfront: this is not a problem nobody has ever solved. A quick search turns up several existing, highly capable options.

  • PowerRunAsSystem by PhrozenIO is a Gallery script that spawns interactive SYSTEM processes using native APIs.
  • Invoke-CommandAs by Marc Kellerman cleverly uses the Task Scheduler to run script blocks as SYSTEM. It’s incredibly solid for headless automation, though it doesn’t give you a visible, interactive window.
  • Invoke-TokenManipulation from PowerSploit also uses token-based process creation, but it lives inside a penetration testing framework, bringing all that heavy security context (and antivirus flagging) along with it.

So, the tools already exist. I could have easily downloaded one, solved my immediate problem, and moved on with my day in ten minutes.

But honestly? That was never really the point.

The Personal Part

I have been writing PowerShell for years. I started the exact same way most people do: blindly copying scripts from forums, tweaking variables just enough to get the job done, and gradually building an understanding of the language by being—if I am completely honest with myself—productively lazy. Why do a tedious task manually ten times when a script can do it perfectly once?

But the more I used PowerShell, the more its true depth revealed itself. Add-Type let me write inline C# and call .NET classes directly from my scripts. P/Invoke allowed me to reach deep into native Windows DLLs and trigger Win32 APIs without ever leaving the comforting blue prompt. Every time I thought I understood the boundaries of what the language could do, another door swung open.

Eventually, PowerShell stopped feeling like just a scripting language. It felt like a Swiss Army knife that had a backdoor into the entire Windows operating system built right into the handle.

And that is exactly when the question formed in my head. PsExec isn’t magic. It is just a standard executable calling publicly documented Windows APIs: OpenProcess, DuplicateTokenEx, CreateProcessWithTokenW. If PowerShell can natively call Win32 APIs, why couldn’t I just do exactly what PsExec does, entirely in a script, with zero executables?

I filed that thought away. But it kept stubbornly surfacing whenever PsExec would have been perfect but was strictly forbidden—in locked-down managed environments, on hardened servers, or behind application control policies that aggressively flagged third-party binaries. The question wouldn’t let me go.

So, finally, I sat down and actually tried to build it.

The Walls

What followed was easily one of the most educational experiences I have ever had with Windows internals. Educational in the specific, painful sense that I learned an enormous amount about the Windows security model mostly by colliding with it, repeatedly, at high speed.

Wall One. I decided to duplicate a token from winlogon.exe (which reliably runs as SYSTEM) and pass it to CreateProcessAsUser. It seemed perfectly logical.
Result: Error 1314. ERROR_PRIVILEGE_NOT_HELD. This happened even while running as an Administrator, holding a token lifted straight from a SYSTEM process. CreateProcessAsUser requires SeTcbPrivilege when launching across session boundaries. Administrator tokens simply do not carry this privilege. It isn’t disabled or restricted; it is physically absent.

Wall Two. Fine, I thought. I’ll just enable it first using AdjustTokenPrivileges.
Result: Error 1300. ERROR_NOT_ALL_ASSIGNED. You cannot enable a privilege that doesn’t exist in the token to begin with.

Wall Three. I tried impersonating SYSTEM using the winlogon token before making the call. Thread-level impersonation temporarily gives your current thread SYSTEM context. Some things actually worked inside that brief window! But CreateProcessAsUser still slapped me with a 1314. Impersonation is thread-level, but CreateProcessAsUser checks the process-level token. The thread whispered “SYSTEM,” but the process shouted “Admin.” Not good enough.

Each wall sent me crawling back to the documentation. And eventually, the documentation told me a hard truth I had been ignoring:

SeTcbPrivilege is never given to administrators by design. The only process that holds it by default is one running as genuine SYSTEM—meaning a core Windows Service or the kernel itself. PsExec doesn’t use API magic; it installs a tiny, temporary service on the fly. That service starts with a true SYSTEM token and spawns your process for you. A real service doing the dirty work on your behalf.

That realisation entirely changed my approach. I had been stubbornly asking: “How do I make an Admin token do SYSTEM things?” The correct question was actually: “Do I even need CreateProcessAsUser at all, or is there a completely different API that skips the SeTcbPrivilege requirement?”

And here, I have to mention something that has genuinely changed how I engineer solutions today. We are operating in a different era now. A few years ago, arriving at that reframed question would have cost me hours of scanning archaic Win32 MSDN archives and deciphering decade-old forum posts by security researchers. AI doesn’t do the thinking for you, but when you describe a highly specific roadblock conversationally, it points you toward the correct API surface instantly. The understanding was still mine to build, but the time between hitting the wall and finding the door was radically compressed.

The Piece That Made It Click

The breakthrough came when I stopped trying to force CreateProcessAsUser to work and found the API built explicitly for this exact scenario:

CreateProcessWithTokenW.

Added back in Windows Vista, it is specifically designed for situations where you hold a token from another context and want to launch a process with it, but you are not running as SYSTEM yourself. The only privilege it demands is SeImpersonatePrivilege. And guess what? Elevated Admin tokens hold that privilege natively. No dirty tricks required.

The final, victorious sequence looked like this:

  1. Duplicate the winlogon token as an impersonation token.
  2. Impersonate SYSTEM on the current thread—just long enough for the next move.
  3. Duplicate winlogon again as a primary token, then call SetTokenInformation to reassign its session ID to the active console session (this step needs SeTcbPrivilege, which is why we are actively impersonating SYSTEM here).
  4. Revert the impersonation, dropping cleanly back to our Admin context.
  5. Call CreateProcessWithTokenW with our beautifully prepared primary token. It succeeds because Admin holds SeImpersonatePrivilege.

The window opened.

nt authority\system.

That was a genuinely incredible moment. Not because the text on the screen was a surprise, but because the path to get there was littered with so many dead ends. Seeing it finally execute meant something. I had actually built it.

What It Became

The final result is Invoke-AsSystem.ps1. It is a completely self-contained PowerShell script. No dependencies, no external binaries, no modules to install. You drop it on any machine with an elevated session, and it works. If you forget to elevate, it doesn’t just throw a wall of red text at you; it politely warns you and offers to relaunch itself with a UAC prompt. You just press a key, and it handles the rest.

code
<code><span style="color: #75715e"># Open an interactive SYSTEM shell seamlessly</span>
.\Invoke-AsSystem.ps1 -FilePath "powershell.exe"

<span style="color: #75715e"># Run a specific command as SYSTEM and keep the window open to review the output</span>
.\Invoke-AsSystem.ps1 -FilePath "powershell.exe" -ArgumentList "-NoProfile -NoExit -Command whoami"

<span style="color: #75715e"># Execute a background tool as SYSTEM, wait for completion, and pass the exit code back</span>
.\Invoke-AsSystem.ps1 -FilePath "C:\Tools\MyDiag.exe" -Wait</code>

How It Compares

Now that you know how the sausage is made, here is how this script stacks up against the alternatives I mentioned earlier. This isn’t about declaring a “winner”—each tool has a specific philosophy and use case. It is about understanding your trade-offs.

Feature Invoke-AsSystem.ps1 PowerRunAsSystem Invoke-CommandAs Invoke-TokenManipulation PsExec -i -s
Interactive desktop window Yes Yes No Yes Yes
No external tools or binaries Yes Yes Yes Yes No
Single self-contained file Yes Yes No (Module) No (Framework) No (Binary)
UAC re-launch prompt if not elevated Yes No No No No
-Wait with exit code passthrough Yes No Yes (as job) No Yes
Works without internet/Gallery access Yes Yes No No No

The two things that genuinely differentiate my script are the graceful UAC re-launch prompt—none of the alternatives handle a non-elevated user context quite as cleanly—and its ruthless zero-dependency, no-install architecture. That matters immensely in secure environments where outbound internet access or PowerShell Gallery connectivity is heavily restricted or completely severed.

But ultimately, writing this was never purely about the final destination. It was about wanting something personal. I wanted a tool I built with my own hands, that works exactly the way my brain expects it to work. I am not a formally trained Windows internals expert, nor am I a professional C# developer. Much of my deeper understanding has come simply from studying other people’s scripts, reverse-engineering how their API calls work, and adapting those mechanics to solve my own problems. I am just a PowerShell enthusiast who got intensely curious, hit a lot of painful walls, and kept pushing until the code compiled and the window opened.

The understanding I came away with might be narrow and highly specific—just enough to solve this one distinct problem. But it is mine, and that counts for something.

The complete script is available over on my GitHub. If you decide to pull it down and end up hitting a wall of your own, drop a comment below. Chances are, I already have the bruises from bouncing off that exact same wall myself.

Until next time.

Leave a Reply

Your email address will not be published. Required fields are marked *