1

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

11
  • Pragmatically speaking, can't you just add Import-Module C:\Users\<UserName>\OneDrive\Documents\PowerShell\Modules\ModuleFolder\<YourModule> to your pwsh.exe call? Commented Jan 6, 2023 at 17:40
  • I can't. the module I'm making needs inputs from the user, after those inputs are entered with the module's cmdlet, commands will be run and then a task will be created with those inputs in Windows task scheduler which I need to be run whether or not any user is logged on. so the module file itself that resides there doesn't have any input and only has a function that accepts parameters. I don't get it why there is no proper official parameter for PowerShell to check the box for Run whether user is logged on or not and Do not store password The task will only have access to local resources Commented Jan 6, 2023 at 17:47
  • I don't understand: The suggested Import-Module call would simply make the automatic importing of your module that you're currently trying to rely on explicit. By definition you're trying to run non-interactively, right? Commented Jan 6, 2023 at 18:14
  • 1
    Besides the contradiction in "the module I'm making needs inputs from the user" and "run whether the user is logged on or not". The SYSTEM account is not supposed to interact with a user as it exposes a security risk. Any attemps in that direction will either be prohibited or trigger UAC (as with PSExec). Communication between these accounts might/should only be done by seperated processes through small channels as the registry or files that contain safe settings avoiding anything that is executable. Commented Jan 6, 2023 at 19:00
  • 3
    In simple terms, PS looks for modules in specified directories. If you install for a User it's placed in the User profile directory, the SYSTEM cannot "see" your module there. You should install the module for "All Users", which places it in a system directory so that ALL users can access it. Whether it will work even then is doubtful. Commented Jan 6, 2023 at 19:53

1 Answer 1

1

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 simplest 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 directory by default

    • with value <hostname>$ reflected in $env:USERNAME, where <hostname> is the name of the local machine, and $HOME / $env:USERPROFILE containing C:\Windows\system32\config\systemprofile.

      • Running whoami.exe reveals the true account name:
        nt authority\system

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you very much, just finished reading it thoroughly and it's so informative and educational.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.