dahall / TaskScheduler

Provides a .NET wrapper for the Windows Task Scheduler. It aggregates the multiple versions, provides an editor and allows for localization.
MIT License
1.21k stars 191 forks source link

ImportTask Security Bug #820

Closed GromitD90 closed 5 years ago

GromitD90 commented 5 years ago

When using TaskService ImportTask to import an xml file which had the Security setting "Run only when user is logged on" selected the resultant imported Task has the security setting "Run whether user is logged on or not" selected. Importing the xml file manually works correctly.

Note that I have only been able to test this where the xml file was exported and imported (using a Program calling ImportTask) on the same PC. Trying to run the same Program on another PC results in an exception (which doesn't appear to return any details) and the task is not Imported.

Here is a snippet of the test code I'm using

string taskname = Path.GetFileNameWithoutExtension(item); using (TaskService ts = new TaskService()) { try { TaskService.Instance.RootFolder.ImportTask(taskname, item); } catch (Exception e) { Console.WriteLine("Exception when adding to task scheduler", e); Console.WriteLine("Make sure you are running App in Administrator mode"); }

            }

The first Console.WriteLine does not print anything for e

Steps to reproduce the behavior:

  1. Create a Task in Task Scheduler with the option on the General Page set for "Run only when user is logged on".
  2. Export the task to an xml file
  3. Delete the task
  4. Import the xml file using a Program that uses ImportTask
  5. Observe the settings in the imported task.
  6. Copy the xml file to another PC and run the same Program from step 4 on that PC and see if you get an exception.

Expected behavior

  1. Task imports with correct settings
  2. Task can be imported on a different PC

Environment (please complete the following information):

dahall commented 5 years ago

When creating a task with the "Run only when user is logged on" option, you are effectively creating a task to run as a specific user using the TaskLogonType.InteractiveToken registration option. The export therefore includes the SID of the user so that specific user would have the interactive right when imported to a different machine. Here's the XML segment, which format is defined by Microsoft:

<Principals>
   <Principal id="Author">
      <UserId>S-1-5-21-123456789-1234567898-123456789-123456</UserId>
      <LogonType>InteractiveToken</LogonType>
      <RunLevel>LeastPrivilege</RunLevel>
   </Principal>
</Principals>

In your export, you'll see your account's SID under the UserId tag. When importing, you will get an error if this SID is not recognized on the importing machine.

As an aside, you do not need to create a TaskService instance if you are using static Instance. Your code should look like:

string taskname = Path.GetFileNameWithoutExtension(item);
try { TaskService.Instance.RootFolder.ImportTask(taskname, item); }
catch (Exception e)
{
   Console.WriteLine("Exception when adding to task scheduler", e);
   Console.WriteLine("Make sure you are running App in Administrator mode");
}

If this is not the problem, please include the full exception information in your reply so I can better diagnose the issue.

dahall commented 5 years ago

Also, given that this library simply wraps the Microsoft API for the Windows Task Scheduler, you don't need to write your own import program. Every Windows machine since Vista includes the SCHTASKS.EXE command-line tool which can do this import.

SCHTASKS /Create /XML "filename.xml" [/RU username]
GromitD90 commented 5 years ago

Thanks Dave. I’ve tried using the schtasks command line approach but I cannot get it to work without supplying a username and password of the target system – for example this works

schtasks /Create /tn TaskName /XML "Path to xml file” /RU username /RP password

If I omit the /RU and /RP entries it gives me an error “No mapping between account names and security ids was done”

If I run Import Task from the Task Scheduler I do not have to supply any username and password – it just creates a task that uses the account that was currently active when the Import was done and “Run only when user is logged on” is selected, which is what I want.

Is there any way on the command line to force a “use currently active account credentials” ?

Mike

dahall commented 5 years ago

I think I understand what you're asking. Instead of using ImportTask, use the RegisterTask method and pull in the XML file using System.IO.File.ReadAllText(xmlFileName). This let's you have full control over the account used to run the task and how that account is configured. I think for what you want, you could use:

string taskname = Path.GetFileNameWithoutExtension(item);
try { TaskService.Instance.RootFolder.RegisterTask(taskname, File.ReadAllText(xmlFileName),
   TaskCreation.CreateOrUpdate, null /* Current User */, null /* No Pwd */, TaskLogonType.InteractiveToken); }
catch (Exception e)
{
   Console.WriteLine("Exception when adding to task scheduler", e);
   Console.WriteLine("Make sure you are running App in Administrator mode");
}
GromitD90 commented 5 years ago

I tried this but I still get an exception when trying to create the task scheduler entry. For the life of me I cannot get the Exception e to display

I’ve tried e.ToString(); in the console.writeline and also e.GetBaseException().ToString() but nothing gets written to the console

I got the following details from event viewer

Application: ConsoleApp15.exe Framework Version: v4.0.30319 Description: The process was terminated due to an unhandled exception. Exception Info: System.Runtime.InteropServices.COMException at Microsoft.Win32.TaskScheduler.V2Interop.ITaskFolder.RegisterTask(System.String, System.String, Int32, System.Object, System.Object, Microsoft.Win32.TaskScheduler.TaskLogonType, System.Object) at Microsoft.Win32.TaskScheduler.TaskFolder.RegisterTask(System.String, System.String, Microsoft.Win32.TaskScheduler.TaskCreation, System.String, System.String, Microsoft.Win32.TaskScheduler.TaskLogonType, System.String) at ConsoleApp15.Program.Main(System.String[])

Here is the code as it currently stands. The first Console.writeline displays the correct path to the xml file

foreach (string item in files) { Console.WriteLine(item); // each item is the full pathname to the xml file string taskname = Path.GetFileNameWithoutExtension(item); try { TaskService.Instance.RootFolder.RegisterTask(taskname, File.ReadAllText(item), TaskCreation.CreateOrUpdate, null / Current User /, null / No Pwd /, TaskLogonType.InteractiveToken); } catch (Exception e) { Console.WriteLine("Exception when adding to task scheduler", e.GetBaseException().ToString()); Console.WriteLine("Make sure you are running App in Administrator mode"); }

dahall commented 5 years ago

I just ran the following successfully. Hopefully it will help with another way to do this:

const string tPath = @"C:\Users\User1\Desktop\Test.xml";
Task t = ts.AddTask("Test", QuickTriggerType.Daily, "myprogram.exe", null, null, null, TaskLogonType.InteractiveToken, null);
t.Export(tPath);
ts.RootFolder.DeleteTask("Test", false);
TaskDefinition td = ts.NewTask();
td.XmlText = System.IO.File.ReadAllText(tPath);
// I did this with SYSTEM, but you could put any user options here,
// including the one I showed earlier.
ts.RootFolder.RegisterTaskDefinition("Test", td, TaskCreation.Create, "SYSTEM", null, TaskLogonType.ServiceAccount, null);
GromitD90 commented 5 years ago

Sorry for the delay in getting back to you. I tried the code you sent to me and yes I can import the xml file however it always ends up with the option Run whether the user is logged on or not.

I modified the code to stop after your initial task creation and export. I looked at the task “test” in the task manager and it clearly shows Run only when the user is logged on.

After the code deletes the task and reimports it the setting is now Run whether the user is logged on or not.

If I manually import the exported xml file it is created correctly with Run only when the user is logged in.

I didn’t try playing with a user account vs the system setting.

dahall commented 5 years ago

If you want to have it be "Run whether the user is logged on or not" and NOT have a password, then you must set the user name to the current user (or null) and TaskLogonType.S4U when you register the task. If you want "Run only when the user is logged on" use TaskLogonType.InteractiveToken.

// Run whether the user is logged on or not
ts.RootFolder.RegisterTaskDefinition("Test", td, TaskCreation.Create, "DOMAIN\\username", null, TaskLogonType.S4U, null);

// Run only when the user is logged on
ts.RootFolder.RegisterTaskDefinition("Test", td, TaskCreation.Create, null, null, TaskLogonType.InteractiveToken, null);
GromitD90 commented 5 years ago

Again many thanks for your help. It’s all working correctly for me now.

I can’t say how much I appreciate your patience.