Hijacking Windows Services

The Richest Place to Look

A Windows service is a background program managed by the Service Control Manager, the equivalent of a daemon on Linux. And here’s why services are the first thing you check for privilege escalation:

Most services run as LocalSystem, which is SYSTEM. So a service is a program that already runs with the highest privileges on the box. If you can influence what it executes, you inherit those privileges.

This is the “privileged thing you can change” idea from the last note, in its purest form. There are three ways to bend a service to your code, and they’re variations on one move.


Mapping the Services

First, list every service and the binary it runs:

Get-CimInstance -ClassName win32_service |
  Select Name, State, PathName |
  Where-Object {$_.State -like 'Running'}

What you’re scanning for: binaries outside C:\Windows\System32. A service whose .exe lives in C:\Program Files\SomeApp or C:\MySQL is user-installed, which means a developer (not Microsoft) chose its directory and its permissions. That’s where mistakes live.


To check whether you can tamper with a binary, read its permissions with icacls:

PS> icacls "C:\MySQL\bin\mysqld.exe"
C:\MySQL\bin\mysqld.exe  NT AUTHORITY\SYSTEM:(F)
                               BUILTIN\Administrators:(F)
                               BUILTIN\Users:(F)

The permission masks that matter:

MaskMeaning
FFull control
MModify
RXRead & execute
R / WRead / Write only

Seeing BUILTIN\Users:(F) or (M) on a service binary is the jackpot: your group can overwrite a file that runs as SYSTEM.


1. Service Binary Hijacking

The most direct technique. If you can write to the service’s .exe, replace it with your own.


The malicious binary just needs to do something useful as SYSTEM, like creating an admin account:

#include <stdlib.h>
int main() {
    system("net user hacker Passw0rd! /add");
    system("net localgroup administrators hacker /add");
    return 0;
}

Cross-compile it on Kali with mingw, then swap it in:

x86_64-w64-mingw32-gcc adduser.c -o mysqld.exe
move C:MySQLinmysqld.exe mysqld.exe.bak   # back up the original
move .mysqld.exe C:MySQLinmysqld.exe       # drop yours in

Now you need the service to restart so it runs your binary. A low-privileged user usually can’t stop a service (net stop → access denied). But there’s a reliable trick:

  • If the service’s Startup Type is Auto, it restarts on boot
  • If you hold SeShutdownPrivilege (whoami /priv), you can reboot the machine yourself
shutdown /r /t 0

After the reboot, the service auto-starts, runs your binary as SYSTEM, and your admin account exists.

In a real engagement, never reboot a production box casually. A machine that doesn’t come back up can take the client’s business down with it. Reboots happen only in coordination with their IT staff.


2. DLL Hijacking

Often the binary isn’t writable. So instead of replacing the .exe, you go after a DLL it loads. This needs one mechanism: the DLL search order.


When a program loads a DLL by name, Windows hunts for it in a fixed order:

  1. The application’s own directoryfirst
  2. The system directory (System32)
  3. The 16-bit system directory
  4. The Windows directory
  5. The current directory
  6. Directories in PATH

Because the application’s directory is searched first, two situations hand you code execution:

  • The program loads a DLL that’s missing (a flawed install, a leftover from an update). You supply it.
  • You can write to a directory that’s searched before the real DLL’s location.

Either way, you plant a malicious DLL with the expected name in the app’s folder.


You find which DLL to target with Process Monitor: filter for the process, watch CreateFile operations, and look for a DLL that returns NAME NOT FOUND in the application directory before being loaded from System32.

Your DLL gets its code into the program’s DllMain entry point, specifically the DLL_PROCESS_ATTACH case, which fires the moment the DLL is loaded:

#include <stdlib.h>
#include <windows.h>

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACH:           // runs when the DLL loads
            system("net user hacker Passw0rd! /add");
            system("net localgroup administrators hacker /add");
            break;
    }
    return TRUE;
}
x86_64-w64-mingw32-gcc evil.c --shared -o TextShaping.dll

One crucial detail:

The DLL runs with the privileges of whoever starts the program. If you launch it yourself as a low-priv user, your code runs as you, which is pointless. The win is to plant the DLL and wait for a privileged user (or a service) to run the program, so your DllMain executes as them.


3. Unquoted Service Paths

The third technique exploits a parsing bug, and it’s the most elegant. It applies when a service’s binary path contains spaces and is not wrapped in quotes.


When Windows starts a service, it passes the path to CreateProcess. If the path has spaces and no quotes, the function can’t tell where the filename ends, so it guesses, left to right, trying each split as ...<part>.exe.

For the unquoted path C:\Program Files\Enterprise Apps\Current Version\GammaServ.exe, Windows attempts, in this order:

C:\Program.exe
C:\Program Files\Enterprise.exe
C:\Program Files\Enterprise Apps\Current.exe     ← if you can write here, you win
C:\Program Files\Enterprise Apps\Current Version\GammaServ.exe

The first matching file gets executed as the service account. So if any intermediate directory is writable, you plant an .exe named for that split.


Find vulnerable services by listing unquoted, spaced paths outside the Windows directory:

wmic service get name,pathname | findstr /i /v "C:\Windows\\" | findstr /i /v """

C:\ and C:\Program Files\ normally need admin to write, but the app’s own subfolder often doesn’t. In the example, Users had Write on C:\Program Files\Enterprise Apps\, so:

copy .adduser.exe 'C:Program FilesEnterprise AppsCurrent.exe'
Start-Service GammaService

Windows tries ...\Enterprise Apps\Current.exe before reaching the real GammaServ.exe, and runs your file as SYSTEM. (It may throw a “service failed to start” error since your binary isn’t the real service, but your payload already ran.)


Same Shape, Three Locks

Step back and all three are the exact same idea:

TechniqueThe privileged thingThe spot you can write
Binary hijackingthe service’s .exethe binary file itself
DLL hijackinga DLL the program loadsthe app directory (searched first)
Unquoted pathsthe service’s executionan intermediate folder in the path

Every one is a thing that runs as SYSTEM + a place you’re allowed to write. Find that pairing and you escalate.

PowerUp automates the hunt (Get-ModifiableServiceFile, Get-UnquotedService, Write-ServiceBinary), but as always, its abuse functions can miss real vulnerabilities, so confirm by hand when a lead looks promising.

Services aren’t the only Windows component you can bend this way. Scheduled tasks and a few dangerous privileges offer the same kind of opening, which is the next note.


Practice Boxes

  • Steel Mountain - Service binary hijacking to SYSTEM, with both manual and PowerUp approaches.
  • Windows PrivEsc - A dedicated lab covering service hijacking, unquoted paths, DLL hijacking, and more in one box.