TL;DR: The Windows Task Scheduler service (Windows 10 and Windows Server 2016) try to load the non-existing DLL WptsExtensions.dll from %windir%\system32 enabling persistence as NT AUTHORITY\SYSTEM.

A couple of days ago I talked with a colleague and the topic turned to persistence and quickly to Scheduled Tasks. Personally, I don’t like Scheduled Tasks as a persistence mechanism as I think they are too obvious, noisy and every blue-teamer and admin knows about them, so you get caught quickly. After the talk I wondered if there is a way to persist with the Task Scheduler service without creating a new Scheduled Task. The following section explains the way I found. It is by no means a very original method or very stealthy, but at least a little stealthier as another task like 0neDrive Updater.


With Windows 10 Microsoft introduced the WPTaskScheduler.dll (WP Task Scheduler DLL) that is loaded by the following stack:

- schedsvc.dll -> ServiceMain()
  - schedsvc.dll -> JobsService...
    - schedsvc.dll -> JobsService::Initialize()
      - schedsvc.dll -> JobsService::InitializeWPTS()

The InitializeWPTS-function loads WPTaskScheduler.dll and resolves WptsInitialize as shown in the following screenshot.


After the function is correctly resolved it gets called and that enables us to persist. One of the first tasks inside WPTaskScheduler.dll::WptsInitialize() is to call InitializeWPTS(). That function in turn loads the non-existing WptsExtensions.dll and resolves the following functions, if the DLL was found and loaded correctly:


So how do I persist now?

The easiest way to persist is to create a DLL exposing the required functions (see above) and copying that DLL to %windir%\system32\WptsExtensions.dll. Reloading the scheduler service loads our DLL. Most of the time if you are already on the system executing code you don’t need to reload the Task Scheduler service, but post-exploitation tactics vary depending on the operator…

The cherry on top …

Sometimes our favorite C2 framework is not that stable, so payloads crash or get stuck. The nice thing, if we implement only stub-functions for the required Wpts... exports is, that the WP Task Scheduler module is not initialized correctly which means, that the JobsService will call our ThreadAttach and ThreadDetach functions multiple times an hour.

Using this circumstance we can implement a checkAlive function that tries to determine if our beacon is still functioning correctly and if not the beacon-thread can be killed and a new beacon-thread is spawned resuming the C2 connection without manual intervention of the operator.