Search code examples
powershellvbscriptscheduled-tasksinvoke-commandsilent

Running commands as logged on user (remotely)


Thought I would share this quick function I made for myself, feel free to adapt it and improve it according to your needs.

Sometimes you want to run commands as the logged on user of a remote computer.

As you know, some commands show output for the user who runs it and if you run the same command with Invoke-Command, it won't return the user's information, but yours). Get-Printer is an example amongst many others.

There is no easy, quick way of running commands as the logged on user natively without any third-party apps like PsExec or others so I made this quick function that uses VBS, PS1 and Scheduled Task to make it happen.

It runs completly silently for the user (thanks to the VBS) and the output is shown in your console. Please note it assumes the remote computer has a C:\TEMP.

Created in a Windows 10, powershell v 5.1.17763.503 environement.

I don't pretend it's final and perfect, it's the simplest way I found to do what is needed and I just wanted to share it with you guys as it can be very useful!

Check the comments for explanation of the code and feel free to use it as you wish. Please share your version as I'm curious to see people improve it. A good idea would be to make it support multiple computers, but as I said it's a quick function I did I don't have too much time to put into refining it.

That being said, I had no problems using it multiple times as is :)

*Output returned is in form of a string, if you want to have a proper object, add '| ConvertFrom-String' and play with it :)

PLEASE NOTE: The surefire way of grabbing the username of who is currently logged on is via QWINSTA (since Win32_ComputerSystem - Username is only reliable if a user is logged on LOCALLY, it won't be right if a user is using RDP/RemoteDesktop). So this is what I used to grab the username, however, please note that in our french environement the name of the username property in QWINSTA is "UTILISATEUR",so you have to change that to your needs (english or other language) for it to work. If I remember correctly, it's "USERNAME" in english.

On this line:

$LoggedOnUser = (qwinsta /SERVER:$ComputerName) -replace '\s{2,22}', ',' | ConvertFrom-Csv | Where-Object {$_ -like "*Acti*"} | Select-Object -ExpandProperty UTILISATEUR

See code in the answer below.


Solution

  • function RunAsUser {
    
    Param ($ComputerName,$Scriptblock)
    
    #Check that computer is reachable
    Write-host "Checking that $ComputerName is online..."
    
    if (!(Test-Connection $ComputerName -Count 1 -Quiet)) {
    Write-Host "$ComputerName is offline" -ForegroundColor Red
    break
    }
    
    #Check that PsRemoting works (test Invoke-Command and if it doesn't work, do 'Enable-PsRemoting' via WMI method).
    #*You might have the adjust this one to suit your environement.
    #Where I work, WMI is always working, so when PsRemoting isn't, I enable it via WMI first.
    Write-host "Checking that PsRemoting is enabled on $ComputerName"
    if (!(invoke-command $ComputerName { "test" } -ErrorAction SilentlyContinue)) {
    Invoke-WmiMethod -ComputerName $ComputerName -Path win32_process -Name create -ArgumentList "powershell.exe -command Enable-PSRemoting -SkipNetworkProfileCheck -Force" | Out-Null
    
        do {
        Start-Sleep -Milliseconds 200
        } until (invoke-command $ComputerName { "test" } -ErrorAction SilentlyContinue)
    }
    
    
    #Check that a user is logged on the computer
    Write-host "Checking that a user is logged on to $ComputerName..."
    $LoggedOnUser = (qwinsta /SERVER:$ComputerName) -replace '\s{2,22}', ',' | ConvertFrom-Csv | Where-Object {$_ -like "*Acti*"} | Select-Object -ExpandProperty UTILISATEUR
    if (!($LoggedOnUser) ) {
    Write-Host "No user is logged on to $ComputerName" -ForegroundColor Red
    break
    }
    
    
    #Creates a VBS file that will run the scriptblock completly silently (prevents the user from seeing a flashing powershell window)
    @"
    Dim wshell, PowerShellResult
    set wshell = CreateObject("WScript.Shell")
    Const WindowStyle = 0
    Const WaitOnReturn = True
    For Each strArg In WScript.Arguments
    arg = arg & " " & strArg
    Next 'strArg
    PowerShellResult = wshell.run ("PowerShell " & arg & "; exit $LASTEXITCODE", WindowStyle, WaitOnReturn)
    WScript.Quit(PowerShellResult)
    "@ | out-file "\\$ComputerName\C$\TEMP\RAU.vbs" -Encoding ascii -force
    
    #Creates a script file from the specified '-Scriptblock' parameter which will be ran as the logged on user by the scheduled task created below.
    #Adds 'Start-Transcript and Stop-Transcript' for logging the output.
    $Scriptblock = "Start-Transcript C:\TEMP\RAU.log -force" + $Scriptblock + "Stop-Transcript"
    $Scriptblock | out-file "\\$ComputerName\C$\TEMP\RAU.ps1" -Encoding utf8  -force
    
    #On the remote computer, create a scheduled task that runs the .ps1 script silently in the user's context (with the help of the vbs)
    Write-host "Running task on $ComputerName..."
    Invoke-Command -ComputerName $ComputerName -ArgumentList $LoggedOnUser -ScriptBlock {
        param($loggedOnUser)
    
        $SchTaskParameters = @{
        TaskName = "RAU"
        Description = "-"
        Action = (New-ScheduledTaskAction -Execute "wscript.exe" -Argument "C:\temp\RAU.vbs C:\temp\RAU.ps1")
        Settings = (New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -DontStopOnIdleEnd)
        RunLevel = "Highest"
        User = $LoggedOnUser
        Force = $true
        }
    
        #Register and Start the task
        Register-ScheduledTask @SchTaskParameters | Out-Null
        Start-ScheduledTask -TaskName "RAU"
    
        #Wait until the task finishes before continuing
        do {
        Write-host "Waiting for task to finish..."
        $ScheduledTaskState = Get-ScheduledTask -TaskName "RAU" | Select-Object -ExpandProperty state
        start-sleep 1
        } until ( $ScheduledTaskState -eq "Ready" )
    
        #Delete the task
        Unregister-ScheduledTask -TaskName "RAU" -Confirm:$false
    }
    Write-host "Task completed on $ComputerName"      
    #Grab the output of the script from the transcript and remove the header (first 19) and footer (last 5)
    $RawOutput = Get-Content "\\$ComputerName\C$\temp\RAU.log" | Select-Object -Skip 19
    $FinalOutput = $RawOutput[0..($RawOutput.length-5)]
    
    #Shows output
    return $FinalOutput
    
    
    #Delete the output file and script files
    Remove-Item "\\$ComputerName\C$\temp\RAU.log" -force
    Remove-Item "\\$ComputerName\C$\temp\RAU.vbs" -force
    Remove-Item "\\$ComputerName\C$\temp\RAU.ps1" -force
    
    }
    
    #____________________________________________________
    
    #Example command
    #Note: Sometimes Start-Transcript doesn't show the output for a certain command, so if you run into empty output, add: ' | out-host' or '| out-default' at the end of the command not showing output.
    $Results = RunAsUser -ComputerName COMP123 -Scriptblock {
    get-printer | Select-Object name,drivername,portname | Out-host
    }
    
    
    $Results
    
    #If needed, you can turn the output (which is a string for the moment) to a proper powershell object with ' | ConvertFrom-String'