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

  • Pragmatically speaking, can't you just add `Import-Module C:\Users\\OneDrive\Documents\PowerShell\Modules\ModuleFolder\` to your `pwsh.exe` call? – mklement0 Jan 06 '23 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` –  Jan 06 '23 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? – mklement0 Jan 06 '23 at 18:14
  • Sorry about that, let me try again, I created a module, uploaded to PowerShell Gallery, user A will install it using `Install-Module` cmdlet in a normal PowerShell window. it will be installed by default in `C:\Users\\OneDrive\Documents\PowerShell\Modules` . user A will run my module with the cmdlet it exposes and passes parameters to it. module runs and does its job, at the end, needs to create a scheduled task so user won't need to repeat the process of running the cmdlet with parameters again. the task it creates needs to run whether user is logged on or and don't ask for pwd. –  Jan 06 '23 at 18:22
  • 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. – iRon Jan 06 '23 at 19:00
  • @iRon Lol at the contradiction part, it's a multi-purpose module that requires input at least once, from the user, after that needs to be run regularly. anyway, yes you are right, I mean I had to disable the ASR rule `Block process creations originating from PSExec and WMI commands` before I could test my module with PSExec. so you're saying I should actually forget about trying to run my module's cmdlet as SYSTEM in task scheduler and my best option is to continue with the `-LogonType S4U -UserId $env:USERNAME -RunLevel Highest`? –  Jan 06 '23 at 19:15
  • Why does the module "require input at least once, from the user"? – Bill_Stewart Jan 06 '23 at 19:48
  • 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. – Scepticalist Jan 06 '23 at 19:53
  • @Bill_Stewart because that's how I made it and how it operates. @Scepticalist thank you very much, that pretty much answers my question and marks it as solved. I found the `-scope` parameter of `install-module` , this link provides all the information: https://learn.microsoft.com/en-us/powershell/module/powershellget/install-module?view=powershell-7.3#-scope –  Jan 06 '23 at 21:23
  • I guess what you want to do can only be done at an application with a manifest level but than again; [Unsigned manifests can simplify development and testing of your application. However, unsigned manifests introduce substantial security risks in a production environment. Only consider using unsigned manifests if your ClickOnce application runs on computers within an intranet that is completely isolated from the internet or other sources of malicious code.](https://learn.microsoft.com/visualstudio/ide/how-to-sign-application-and-deployment-manifests#generate-unsigned-manifests) – iRon Jan 06 '23 at 21:29
  • "because that's how I made and and how it operates" - It seems to me that this is a good reason to consider a design that doesn't require this. – Bill_Stewart Jan 06 '23 at 22:37

1 Answers1

0

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you very much, just finished reading it thoroughly and it's so informative and educational. –  Jan 12 '23 at 12:46