Automating with PowerShell: Impersonating users while running as SYSTEM

I’ve demonstrated in a couple of blogs like the OneDrive Sync Monitoring and the OneDrive File Monitoring that it’s possible to impersonate the current user when a script is actually started by the NT AUTHORITY\SYSTEM account.

My friends asked me if it would not be possible for other scripts to use the same approach. In the previous blogs I’ve shown that by loading the component by MurrayJu we got the ability to impersonate. I converted this into a module which you can find on https://github.com/KelvinTegelaar/RunAsUser.

This module allows you to run any script that is initiated by SYSTEM and execute it as the currently logged on user. This gives us a lot of freedom. Most RMM systems(and intune!) don’t allow monitoring under the currently logged on user. This often means that you have to work around accessing resources directly in their profile.

Some examples would be accessing installers that run in the users AppData folder, or registry items created under HKCU. Another could be scripts that require accessing shared drives or printers that are only mapped in user-space.

This is also super useful for intune scripts, because you just need to present things to the user or install things using their credentials directly.

Using the module

So, using the module is very straight forward. To install the module execute the following command:

install-module RunAsUser

After you’ve installed the module you can jump straight into scripting. There are some things to account for; The script requires SYSTEM credentials or the SeDelegateSessionUserImpersonatePrivilege privilege.

The second thing is that the output can’t be directly captured. If you want to get output from the script you’ll have to write it to a file and pick that up again in the SYSTEM session. This might sound a little confusing so I have an example below.

$scriptblock = {
$IniFiles = Get-ChildItem "$ENV:LOCALAPPDATA\Microsoft\OneDrive\settings\Business1" -Filter 'ClientPolicy*' -ErrorAction SilentlyContinue

if (!$IniFiles) {
    write-host 'No Onedrive configuration files found. Stopping script.'
    exit 1
}
 
$SyncedLibraries = foreach ($inifile in $IniFiles) {
    $IniContent = get-content $inifile.fullname -Encoding Unicode
    [PSCustomObject]@{
        'Item Count' = ($IniContent | Where-Object { $_ -like 'ItemCount*' }) -split '= ' | Select-Object -last 1
        'Site Name'  = ($IniContent | Where-Object { $_ -like 'SiteTitle*' }) -split '= ' | Select-Object -last 1
        'Site URL'   = ($IniContent | Where-Object { $_ -like 'DavUrlNamespace*' }) -split '= ' | Select-Object -last 1
    }
}
$SyncedLibraries | ConvertTo-Json | Out-File 'C:\programdata\Microsoft OneDrive\OneDriveLibraries.txt'
}
try{
Invoke-AsCurrentUser -scriptblock $scriptblock
} catch{
write-error "Something went wrong"
}
start-sleep 2 #Sleeping 2 seconds to allow script to write to disk.
$SyncedLibraries = (get-content "C:\programdata\Microsoft OneDrive\OneDriveLibraries.txt" | convertfrom-json)
if (($SyncedLibraries.'Item count' | Measure-Object -Sum).sum -gt '280000') { 
write-host "Unhealthy - Currently syncing more than 280k files. Please investigate."
$SyncedLibraries
}
else {
write-host "Healthy - Syncing less than 280k files."
}

In the script, we’re executing the Script Block using Invoke-AsCurrentUser command. This runs that entire block of code as the currently logged on user. We then sleep for 2 seconds allowing the script block to finish writing to disk. After this finishes, we pick up the file again under the system account and process the results.

So in short; using this module opens up a lot of user-based monitoring for systems that normally only allow executing under the SYSTEM account. Hopefully this helps people solve some challenges.

As a closing remark I’d like to thank Ben Reader (@Powers_hell) for his help on the module. He assisted in cleaning up the code right after release, making it all look and feel a lot smoother and he assisted in better error handling. Thanks Buddy! 🙂

As always, Happy PowerShelling.

19 Comments

  1. Steve July 17, 2020 at 4:26 pm

    Cool, looking forward to giving this a try, thanks. 🙂

    An alternative method I’ve used up till now is to save the user script block as a PS1 locally somewhere and then create a new scheduled task in the users context that points to the saved script, trigger it, and then remove it again… that kind of works fine, but this looks much more comprehensive. 👍

  2. K September 9, 2020 at 11:23 am

    3 days of scouring the internet, should have come here in the first place.

  3. Kushal September 28, 2020 at 1:04 pm

    Hello, does it have to have sedelegatesessionuserimpersonateprivilege set to enable ?
    can you share how to set this attribute to enable ?

  4. Kushal September 28, 2020 at 1:06 pm

    For me I get this error :
    invoke-ascurrentuser : Not running with correct privilege. You must run this script as system or have the SeDelegateSessionUserImpersonatePrivilege token.
    At line:3 char:1
    + invoke-ascurrentuser -scriptblock $scriptblock

    1. Kelvin Tegelaar September 28, 2020 at 1:14 pm

      Hi Kushal,

      You’re probably not executing the script as SYSTEM. You’ll have to execute it using a RMM or other tool that elevates it to SYSTEM.

  5. SRTechOps October 7, 2020 at 8:53 pm

    As it stands (unless I’m mistaken) if no user is logged in, the task will execute successfully but not actually work.
    Is there any way we could have it ‘park’ the script and then run as user the next time someone logs in?
    I’ve used this runasuser module combined with burnttoast based on your suggestions and I’m set it up in our RMM so we can send custom messages to users (To notify of Office365 outages or maintenance windows). However if a user isn’t logged in at the time we send the message they will never see it, so it means that sending a message at 7am when we discover an issue won’t be seen by most users as they start later in the day.
    Also – as always, thank you for your amazing contributions to the community.

    1. PV April 1, 2021 at 4:07 pm

      What RMM are you using? I’m currently trying to get this to work using the “Run Script” Feature of SCCM but to no avail. I’m getting access denied errors on my domain joined devices. I’m sorry if i’m not able to help you with your problem, I’m still in the process of trying to understand mine.

  6. Eric Chapman October 15, 2020 at 12:37 am

    I’m attempting to run this from “Backstage” in Connectwise Control. (formerly screenconnect).

    This is a powershell window running under the context of SYSTEM.

    The commands seem to run just fine, but no output seems to work. just a number is output to the shell. Is it possible the cmdlet is not acting as expected due to being on a different console?

    Any ideas?

    1. Liam May 12, 2021 at 10:33 pm

      I’m getting the same problem. No idea what to do.

  7. Adam Wheeless November 12, 2020 at 3:15 pm

    If I launch a command prompt as SYSTEM (psexec.exe -s -i cmd.exe), I can call a HelloWorld.ps1 based on the example above successfully. However, if I run a PowerShell ISE terminal as SYSTEM (spexec.exe -s -i powershell_ise.exe) and try to execute the same code I get a Windows PowerShell ISE error stating: Error processing arguments: There is no option with the following name: ExecutionPolicy.

    Any idea about what is going on?

    1. Kelvin Tegelaar November 12, 2020 at 5:51 pm

      The module tries to use the same exectuable as it was launched under, because ISE is the actual executable and has no support for running scripts directly it fails.

      You can use the -UseWindowsPowerShell option in the invoke-ascurrentuser command, it uses the Windows PowerShell host in that case.

  8. Brad December 15, 2020 at 7:45 am

    Is it possible to pass arguments into the script block?
    I am trying to use RunAsUser along with BurntToast to display a message to the current user while sending the script from my RMM.

    $messagetext = $args[0]

    $scriptblock = {
    $heroimage = New-BTImage -Source ‘https://www.mydomain.com/BurntToastLogo.png’ -HeroImage
    $Text1 = New-BTText -Content “Message from RMM”
    $Text2 = New-BTText -Content $messagetext
    $Button = New-BTButton -Content “Snooze” -snooze -id ‘SnoozeTime’
    $Button2 = New-BTButton -Content “Dismiss” -dismiss
    $5Min = New-BTSelectionBoxItem -Id 5 -Content ‘5 minutes’
    $10Min = New-BTSelectionBoxItem -Id 10 -Content ’10 minutes’
    $1Hour = New-BTSelectionBoxItem -Id 60 -Content ‘1 hour’
    $4Hour = New-BTSelectionBoxItem -Id 240 -Content ‘4 hours’
    $1Day = New-BTSelectionBoxItem -Id 1440 -Content ‘1 day’
    $Items = $5Min, $10Min, $1Hour, $4Hour, $1Day
    $SelectionBox = New-BTInput -Id ‘SnoozeTime’ -DefaultSelectionBoxItemId 10 -Items $Items
    $action = New-BTAction -Buttons $Button, $Button2 -inputs $SelectionBox
    $Binding = New-BTBinding -Children $Text1, $Text2 -HeroImage $heroimage
    $Visual = New-BTVisual -BindingGeneric $Binding
    $Content = New-BTContent -Visual $Visual -Actions $action
    Submit-BTNotification -Content $Content
    }

    Invoke-AsCurrentUser -scriptblock $scriptblock

    The problem is $args[0] doesn’t seem to get pulled in to the script block and -ArgumentList doesn’t seem to work. It doesn’t seem to matter if the $messagetext = $args[0] is inside the script block or outside.

    I am hoping to have the single script in the RMM and just set the text when I run the script so I can send custom messages each time.

    Great module by the way, I’ve got it working for a few other things but just can’t seem to get this to work. Thank you.

    1. Kelvin Tegelaar December 15, 2020 at 9:38 am

      Currently no. I am working on passing arguments but haven’t really gotten the team for it yet… 🙂

  9. Jonathan Guyer February 11, 2021 at 4:05 pm

    Awesome module! Works well, though, kept getting error in log that it was unable to find type [Microsoft.Powershell.Commands.PowershellGet.Telemetry]. Added section to script to suppress this error as workaround.

    My one question is best way to get output of scriptblock that is runas user? The script that runs the module gives no details in log and my understanding is its run as a separate script, but like that output dumped back into the parent script log, if possible.

    1. Kelvin Tegelaar February 11, 2021 at 6:23 pm

      You can’t get the output as it spawns an entirely different process. You could either write to a transcript in your scriptblock, or write the lines that you need to output to a file.

  10. Pieter April 1, 2021 at 11:23 am

    I’m having a peculiar problem with SCCM not being able to execute my script and getting “Access Denied”. Below I’ll add 2 transcripts in which I only see a difference in the “Host Application” reference. I’m not skilled and experienced in Powershell and I’m in the process of learning. Any help is appreciated!

    This is the transcript when it’s being run by SCCM:
    **********************
    Windows PowerShell transcript start
    Start time: 20210401111516
    Username: domain\SYSTEM
    RunAs User: domain\SYSTEM
    Configuration Name:
    Machine: computername (Microsoft Windows NT 10.0.19042.0)
    Host Application: C:\WINDOWS\system32\WindowsPowerShell\v1.0\PowerShell.exe -NonInteractive -NoProfile -ExecutionPolicy RemoteSigned -Command & { . ‘C:\WINDOWS\CCM\ScriptStore\AC440FD7-6243-4581-9DAF-1B9F27F638D6_7E8A0EC013EA75315CACA096B65E9F65763AE8F04323420FBB2C2EB7CA2955D7.ps1’ | ConvertTo-Json -Compress }
    Process ID: 18444
    PSVersion: 5.1.19041.610
    PSEdition: Desktop
    PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.19041.610
    BuildVersion: 10.0.19041.610
    CLRVersion: 4.0.30319.42000
    WSManStackVersion: 3.0
    PSRemotingProtocolVersion: 2.3
    SerializationVersion: 1.1.0.1
    **********************
    Invoke-AscurrentUser : Could not execute as currently logged on user: Exception calling “StartProcessAsCurrentUser” with
    “6” argument(s): “CreateProcessAsUser failed. (Access Denied, Win32ErrorCode 5 – 0x00000005)”
    At C:\WINDOWS\CCM\ScriptStore\AC440FD7-6243-4581-9DAF-1B9F27F638D6_7E8A0EC013EA75315CACA096B65E9F65763AE8F04323420FBB2C2
    EB7CA2955D7.ps1:35 char:1
    + Invoke-AscurrentUser -scriptblock $script
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], MethodInvocationException
    + FullyQualifiedErrorId : System.Management.Automation.MethodInvocationException,Invoke-AsCurrentUser
    Invoke-AscurrentUser : Could not execute as currently logged on user: Exception calling “StartProcessAsCurrentUser” wit
    h “6” argument(s): “CreateProcessAsUser failed. (Access Denied, Win32ErrorCode 5 – 0x00000005)”
    At C:\WINDOWS\CCM\ScriptStore\AC440FD7-6243-4581-9DAF-1B9F27F638D6_7E8A0EC013EA75315CACA096B65E9F65763AE8F04323420FBB2C
    2EB7CA2955D7.ps1:35 char:1
    + Invoke-AscurrentUser -scriptblock $script
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [Write-Error], MethodInvocationException
    + FullyQualifiedErrorId : System.Management.Automation.MethodInvocationException,Invoke-AsCurrentUser
    **********************
    Windows PowerShell transcript end
    End time: 20210401111517
    **********************

    ———————————————————————————————————————————————————————————————————————————————————————————

    And below when I run it with “psexec” as system account:

    **********************
    Windows PowerShell transcript start
    Start time: 20210401111416
    Username: domain\SYSTEM
    RunAs User: domain\SYSTEM
    Configuration Name:
    Machine: computername (Microsoft Windows NT 10.0.19042.0)
    Host Application: powershell.exe -executionpolicy bypass -file c:\users\…script.ps1
    Process ID: 7172
    PSVersion: 5.1.19041.610
    PSEdition: Desktop
    PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.19041.610
    BuildVersion: 10.0.19041.610
    CLRVersion: 4.0.30319.42000
    WSManStackVersion: 3.0
    PSRemotingProtocolVersion: 2.3
    SerializationVersion: 1.1.0.1
    **********************
    Transcript started, output file is C:\globaltranscript.txt
    18172
    **********************
    Windows PowerShell transcript end
    End time: 20210401111419
    **********************

    1. Kelvin Tegelaar April 1, 2021 at 7:23 pm

      the error is access denied, so either you’re not really running the script as system, or there is EDR in the way. 🙂

  11. Tyler Berger June 8, 2021 at 8:49 pm

    What does Invoke-AsCurrentUser return if there is no user logged in?

  12. Jarrad July 5, 2021 at 4:01 am

    Would it be possible to have this script upgrade FortiClient and have the MSI switch /promptrestart come up under the User Context?

Leave a comment

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.