Search code examples
powershellpowershell-cmdletwindows-task-schedulerpowershell-modulepowershell-7.3

How to run my PowerShell module cmdlet as SYSTEM?


Created a PowerShell module, it has a function and exposes a cmdlet for that function. the built-in PowerShell 5.1 and pwsh.exe 7.3.1 (Installed using MSI installer) can detect and run the cmdlet without problem.

now I need that cmdlet to "run whether the user is logged on or not" in Windows task scheduler.

the problem arises when I try to run my PowerShell module's cmdlet as NT AUTHORITY\SYSTEM.

Which I need to do because in task scheduler, that appears to be the only way to get scheduled task "run whether the user is logged on or not". (I don't want to manually enter username or password of any Windows user account)

enter image description here

Ideally, I'd rather use built in administrators security group but as you can see then i won't be able to run the task if the user is not logged on.

enter image description here

so I'm really stuck here not sure what to do. I assume this is one of the edge cases I'm encountering.

I need to find a way so that when PowerShell is running as SYSTEM, it will still be able to detect my module's cmdlet.

I know my cmdlet isn't detected when PowerShell is running as SYSTEM because I tested it with PsExec64.

I put my PowerShell module in here (that's where they get installed by default from online galleries):

C:\Users\<UserName>\OneDrive\Documents\PowerShell\Modules\ModuleFolder

This is the entire block of the PowerShell script I use to create my task.

$action = New-ScheduledTaskAction -Execute "pwsh.exe" -Argument "-command 'myCmdLet -parameter1 $variable1"

# First thing I tried
$TaskPrincipal = New-ScheduledTaskPrincipal -GroupId "BUILTIN\Administrators" -RunLevel Highest

# Second thing I tried
$TaskPrincipal = New-ScheduledTaskPrincipal -UserID "NT AUTHORITY\SYSTEM" -LogonType ServiceAccount -RunLevel Highest

$trigger = New-ScheduledTaskTrigger -AtStartup

Register-ScheduledTask -Action $action -Trigger $trigger -Principal $TaskPrincipal -TaskName "Name" -Description "Description"

$TaskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Compatibility Win8 -StartWhenAvailable

Set-ScheduledTask -TaskName "Name" -Settings $TaskSettings 

UPDATE:

I found a way:

$TaskPrincipal = New-ScheduledTaskPrincipal -LogonType S4U -UserId $env:USERNAME -RunLevel Highest

which runs the task as administrator (since the module cmdlet won't even work without administrator privileges) and also checks these 2 boxes which I really wanted to do. however, still looking for a way to make my module's cmdlet known to PowerShell when it runs as SYSTEM, it will provide 1 benefit, AFAIK, which is to remove the dependency on a specific user account existing on the computer.

enter image description here


Solution

  • To summarize:

    • Your problem was that the module you want your scheduled task to use was installed in the scope of the current user, which a task running as a different user, such as NT AUTORITY\SYSTEM, would not see.

      • The simples solution is to install the module for all users, via the -Scope AllUsers argument passed to Install-Module (requires elevation).

      • However, there is a solution even if that is not possible or desired, namely to pass the full module path to an explicit Import-Module call performed in the context of the task, as shown below.

    • Since your task needs to run with elevation, as you state, running in the context of the current user is only an option if that user is an administrator; as you have found, you can set that up as follows (requires elevation):

      $taskPrincipal = 
        New-ScheduledTaskPrincipal -LogonType S4U -UserId $env:USERNAME -RunLevel Highest
      
    • Running a task as user NT AUTORITY\SYSTEM invariably runs:

      • with elevation, given that this account is a highly-privileged built-in account with extensive local privileges, and which "acts as the computer on the network".
      • invisibly (in the same hidden session / window station that services run in, hence the use of -LogonType Service.
      • whether or not a user is currently logged on
      • with C:\Windows\System32 as the working director by default
      • with value <hostname>$ reflected in $env:USERNAME, where <hostname> is the name of the local machine, and a $HOME / $env:USERPROFILE folder of C:\Windows\system32\config\systemprofile.

    The following self-contained example:

    • creates a task that executes once, 5 seconds after creation
    • runs as NT AUTHORITY\SYSTEM
    • creates a sample script module in the current user's home dir. that the task explicitly imports
    • calls a function from that module, which reports information about the runtime environment and logs the result in a text file in the current user's home dir.
    • waits for the task to run and shows the logged information.
    • cleans up after itself (the temporary files created are ~/_test.psm1 and ~/_test.txt, and the temporary task is named _Test)
    #requires -RunAsAdministrator
    
    # Abort on any error.
    # Note: Since the *-ScheduledTask* cmdlets are implemented as 
    #       CDXML-based *script* modules, they only see preference vars.
    #       in the *global* scope - see https://github.com/PowerShell/PowerShell/issues/4568
    $prevEaPref = $global:ErrorActionPreference
    $global:ErrorActionPreference = 'Stop'
    
    try {
    
        # Create a simple script module that exports function Get-Foo,
        # in the current user's home dir., named '_test.psm1'
        @'
    function Get-Foo { "Hi, I'm running at $(Get-Date)`n * as $env:USERNAME (whose home dir. is '$HOME')`n * in '$PWD'`n * $(('NON-elevated', 'ELEVATED')[[bool] (net session 2>$null)])." }
    '@ > ~/_test.psm1
    
        # Set up the scheduled task:
    
        # The command (program) to run.
        # Import the test-module via its full path, call Get-Foo, and redirect all output streams
        # to file '_test.txt' in the current users' home dir.
        $action = New-ScheduledTaskAction -Execute powershell -Argument "-ExecutionPolicy Bypass -NoProfile -Command & { Import-Module `"$((Get-Item ~/_test.psm1).FullName)`"; Get-Foo } *> `"$((Get-Item ~).FullName)\_test.txt`""
    
        # Run as 'NT AUTHORITY\SYSTEM', which runs:
        #  * invisibly
        #  * whether or not someone is logged on
        #  * implicitly with elevation
        $user = New-ScheduledTaskPrincipal -UserID 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount
    
        # # Advanced settings such as whether to allow start on demand, not running when on batter power, ... 
        # $settings = New-ScheduledTaskSettingsSet
    
        # When to run it: Run a few seconds from now, once.
        $secsFromNow = 5
        $when = (Get-Date).AddSeconds($secsFromNow)
        $trigger = New-ScheduledTaskTrigger -Once -At $when
    
        # Create the task from the above.
        $newTask = New-ScheduledTask -Action $action -Principal $user -Trigger $trigger
    
        # Register the task with name '_Test'
        Write-Verbose -Verbose "Creating task..."
        Register-ScheduledTask '_Test' -InputObject $newTask -Force
    
        Write-Verbose -Verbose "Task will execute invisibly in $secsFromNow seconds, running as 'NT AUTHORITY\SYSTEM'. Waiting (plus a few extra seconds)..."
        Start-Sleep ($secsFromNow + 5) # Wait an extra few seconds to give the task time to complete.
    
        Write-Verbose -Verbose "Task is assumed to have run. Output logged:"
        Get-Content ~/_test.txt
    }
    finally {
        # Clean up.
        Remove-Item -ErrorAction Ignore ~/_test.psm1, ~/_test.txt
        Unregister-ScheduledTask -ErrorAction Ignore -TaskName _Test -Confirm:$false
        $global:ErrorActionPreference = $prevEaPref
    }
    

    Sample output:

    VERBOSE: Creating task...
    
    TaskPath                                       TaskName                          State
    --------                                       --------                          -----
    \                                              _Test                             Ready
    VERBOSE: Task will execute invisibly in 5 seconds, running as 'NT AUTHORITY\SYSTEM'. Waiting (plus a few extra seconds)...
    VERBOSE: Task is assumed to have run. Output logged:
    Hi, I'm running at 01/11/2023 17:06:04
     * as WORKSTATION1$ (whose home dir. is 'C:\Windows\system32\config\systemprofile')
     * in 'C:\Windows\system32'
     * ELEVATED.
    

    The last 4 lines are the module function's (Get-Foo's) output, proving that the module was successfully imported in the context of the task.