# Suricata Windows Service Install/Update Creates Unquoted LocalSystem ImagePath, Enabling Local Privilege Escalation in Writable-Parent Deployments

Date: 2026-05-25

Finding: Suricata's Windows service install/update logic creates an unquoted LocalSystem service `ImagePath`, enabling local privilege escalation in writable-parent deployments.

Severity: High local privilege escalation; deployment-dependent; not remotely triggerable by network traffic.

CWE: CWE-428, Unquoted Search Path or Element

MITRE ATT&CK: T1574.009, Hijack Execution Flow: Path Interception by Unquoted Path

Status: Confirmed in source, validated with a benign Windows process-creation PoC, and validated with a disposable SCM service running as LocalSystem.

## Summary

Suricata's Windows service installation and service parameter update logic constructs the service `ImagePath` by copying the running executable path with `GetModuleFileName`, appending command-line arguments separated by spaces, and passing the resulting string to the Windows Service Control Manager without quoting the executable path.

When a Windows service binary path contains spaces and is not quoted, Windows process creation can resolve and execute an earlier path component before the intended executable. If an attacker can place an executable at one of those earlier candidate paths, that executable can be launched instead of `suricata.exe`.

The issue affects the Windows-specific service paths:

- `SCServiceInstall` in `src/win32-service.c`
- `SCServiceChangeParams` in `src/win32-service.c`

Because the service is created with `lpServiceStartName == NULL`, Microsoft documents that the service runs as `LocalSystem`. A disposable SCM validation confirmed this behavior: the hijacked marker executable ran as `SYSTEM` with SID `S-1-5-18`.

This is best classified as a high-severity local privilege escalation. It has critical impact on affected Windows hosts, but it is not remotely triggerable by network traffic and still depends on local write access to a candidate path plus a service start or restart event.

This should not be treated as a pure deployment misconfiguration. The unsafe service configuration originates in Suricata's own service installation/update logic. Exploitability is deployment-dependent: default protected `C:\Program Files\Suricata` installations are harder to exploit under normal ACLs, while spaced installations beneath commonly writable parents, or parents writable in the assessed environment such as `C:\ProgramData`, `C:\Users\Public`, or permissive operational directories, are realistically exploitable.

## Technical Root Cause Analysis

### Vulnerable Service Install Path Construction

Location: `src/win32-service.c`, function `SCServiceInstall`

Relevant code:

```c
if (GetModuleFileName(NULL, path, MAX_PATH) == 0 ){
    SCLogError("Can't get path to service binary: %d", (int)GetLastError());
    break;
}

/* skip name of binary itself */
for (i = 1; i < argc; i++) {
    if ((strlen(argv[i]) <= strlen("--service-install")) && (strncmp("--service-install", argv[i], strlen(argv[i])) == 0)) {
        continue;
    }
    strlcat(path, " ", sizeof(path) - strlen(path) - 1);
    strlcat(path, argv[i], sizeof(path) - strlen(path) - 1);
}

service = CreateService(
        scm,
        PROG_NAME,
        PROG_NAME,
        SERVICE_ALL_ACCESS,
        SERVICE_WIN32_OWN_PROCESS,
        SERVICE_DEMAND_START,
        SERVICE_ERROR_NORMAL,
        path,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL);
```

`GetModuleFileName(NULL, path, MAX_PATH)` returns the current executable path, for example:

```text
C:\Program Files\Suricata\suricata.exe
```

The code then appends service arguments using a raw space separator, producing a service binary path similar to:

```text
C:\Program Files\Suricata\suricata.exe -c C:\Program Files\Suricata\suricata.yaml
```

The executable portion is not enclosed in quotes before being passed as `lpBinaryPathName`.

Microsoft's `CreateServiceA` documentation explicitly states that if the service binary path contains a space, it must be quoted so that it is correctly interpreted. Microsoft also documents that when `lpServiceStartName` is `NULL`, `CreateService` uses the `LocalSystem` account.

Reference:

- https://learn.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-createservicea

### Vulnerable Service Change Path Construction

Location: `src/win32-service.c`, function `SCServiceChangeParams`

The same pattern is repeated when changing service startup parameters:

```c
if (GetModuleFileName(NULL, path, MAX_PATH) == 0 ){
    SCLogError("Can't get path to service binary: %d", (int)GetLastError());
    break;
}

/* skip name of binary itself */
for (i = 1; i < argc; i++) {
    if ((strlen(argv[i]) <= strlen("--service-change-params")) && (strncmp("--service-change-params", argv[i], strlen(argv[i])) == 0)) {
        continue;
    }
    strlcat(path, " ", sizeof(path) - strlen(path) - 1);
    strlcat(path, argv[i], sizeof(path) - strlen(path) - 1);
}

if (!ChangeServiceConfig(
            service,
            SERVICE_WIN32_OWN_PROCESS,
            SERVICE_DEMAND_START,
            SERVICE_ERROR_NORMAL,
            path,
            NULL,
            NULL,
            NULL,
            NULL,
            NULL,
            PROG_NAME))
```

This means a service that was initially created safely by another mechanism can later be made vulnerable if `--service-change-params` is run from a Suricata executable path containing spaces.

### Why This Is Vulnerable

Microsoft documents the exact unsafe behavior in `CreateProcessA` security remarks. When `lpApplicationName` is `NULL` and the executable path in `lpCommandLine` contains spaces, Windows may interpret earlier path components as executable candidates.

For a command line such as:

```text
C:\Program Files\Suricata\suricata.exe -c C:\Program Files\Suricata\suricata.yaml
```

Windows may try candidate paths such as:

```text
C:\Program.exe
C:\Program Files\Suricata\suricata.exe
```

The general class becomes exploitable when an attacker can create one of the earlier executable candidates.

Reference:

- https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa

### Root Cause

The root cause is unsafe construction of a Windows service command line:

- The executable path and arguments are combined into a single string.
- The executable path is not quoted.
- Arguments are appended without Windows command-line escaping.
- The service is configured to run as LocalSystem by default.
- No validation is performed to detect ambiguous service binary paths before installation or update.

The code assumes that the path returned by `GetModuleFileName` can safely become the first token of a service `ImagePath`. That assumption is false on Windows when paths contain whitespace.

## Affected Assets & Attack Surface

### Affected Platform

This issue is Windows-specific and affects builds compiled with `OS_WIN32`.

The exposed functionality is present in the Suricata command-line options:

```text
--service-install
--service-change-params
```

The relevant run modes are dispatched from `src/suricata.c`:

```c
case RUNMODE_INSTALL_SERVICE:
    if (SCServiceInstall(argc, argv)) {
        return TM_ECODE_FAILED;
    }
    SCLogInfo("Suricata service has been successfully installed.");
    return TM_ECODE_DONE;

case RUNMODE_CHANGE_SERVICE_PARAMS:
    if (SCServiceChangeParams(argc, argv)) {
        return TM_ECODE_FAILED;
    }
    SCLogInfo("Suricata service startup parameters has been successfully changed.");
    return TM_ECODE_DONE;
```

### Affected Assets

Potentially affected assets include:

- Windows hosts running Suricata as a Windows service.
- Windows hosts where Suricata was installed or updated using Suricata's built-in service installation logic.
- Windows hosts where service parameters were changed using Suricata's `--service-change-params` mode.
- Windows deployments installed under paths containing spaces, including common paths such as `C:\Program Files\Suricata`.
- Custom Windows deployments installed under commonly writable parents, or parents writable in the assessed environment, such as `C:\ProgramData`, `C:\Users\Public`, shared application directories, or operational tool directories with weak ACLs.

### Exposed Trust Boundaries

This issue crosses three important trust boundaries:

1. Filesystem trust boundary: a low-privileged user may be able to write an executable to an earlier ambiguous path component.
2. Service Control Manager trust boundary: the unquoted path is persisted as privileged service configuration.
3. Privilege boundary: the service is launched as LocalSystem when `lpServiceStartName` is `NULL`.

### Attacker Discovery Methods

A local attacker or incident responder can discover vulnerable services using standard Windows enumeration:

```powershell
Get-CimInstance Win32_Service |
  Where-Object {
    $_.PathName -match '\s' -and
    $_.PathName -notmatch '^"'
  } |
  Select-Object Name, StartName, State, PathName
```

Equivalent command-line discovery:

```cmd
wmic service get name,displayname,pathname,startmode | findstr /i /v "\""
```

An attacker would then inspect the resulting path for writable candidate locations. For example:

```text
C:\ProgramData\Suricata Service\Real Dir\suricata.exe
```

can create the candidate:

```text
C:\ProgramData\Suricata.exe
```

if `C:\ProgramData` permits the attacker to create files.

## Exploitation Walkthrough

The following walkthrough describes exploitation at a level suitable for defensive validation and incident-response readiness. It avoids persistence and destructive payload behavior.

### Preconditions

Exploitation requires:

- Windows host.
- Suricata installed or updated as a Windows service through the vulnerable code path.
- Service `ImagePath` contains an unquoted executable path with whitespace.
- Attacker can write an executable to an earlier candidate path.
- Service is started or restarted after the candidate executable is present.

### Adversarial Workflow

1. Enumerate services and identify an unquoted Suricata service path.

   Example vulnerable service path:

   ```text
   C:\ProgramData\Suricata Service\Real Dir\suricata.exe --service-mode -c C:\ProgramData\Suricata\suricata.yaml
   ```

2. Determine Windows' earlier executable candidates.

   For the example above, candidate paths include:

   ```text
   C:\ProgramData\Suricata.exe
   C:\ProgramData\Suricata Service\Real.exe
   C:\ProgramData\Suricata Service\Real Dir\suricata.exe
   ```

3. Test write access to earlier candidate paths.

   In the validated environment, the current non-admin user could write to:

   ```text
   C:\ProgramData
   C:\Users\Public
   %TEMP%
   ```

   The same user could not write to:

   ```text
   C:\
   C:\Program Files
   C:\Program Files (x86)
   ```

4. Place a benign validation executable at the first writable candidate path.

   For the example:

   ```text
   C:\ProgramData\Suricata.exe
   ```

5. Wait for or trigger service start.

   A local low-privileged attacker may not be able to start a demand-start service directly, depending on the service DACL. Practical trigger opportunities include:

   - system reboot if operational tooling starts the service;
   - administrator troubleshooting;
   - service restart by monitoring or management tooling;
   - scheduled maintenance;
   - EDR, deployment, or orchestration workflows that restart services.

6. Windows resolves and runs the attacker-controlled candidate executable instead of the intended `suricata.exe`.

7. The candidate executable runs in the service context. For the affected `CreateService` call, that context defaults to LocalSystem.

### Constraint Reduction

The exploitability is weaker for the common default installation under `C:\Program Files\Suricata\suricata.exe` because the main earlier candidate is typically protected from standard-user writes:

```text
C:\Program.exe
```

`C:\Program Files\Suricata.exe` becomes an earlier candidate only if a later path component contains another space, for example `C:\Program Files\Suricata Service\suricata.exe`.

However, the precondition is substantially reduced in custom deployments:

- If Suricata is installed under `C:\ProgramData\Suricata Service\...`, a standard user may be able to create `C:\ProgramData\Suricata.exe` where `C:\ProgramData` is writable by that user.
- If Suricata is installed under `C:\Users\Public\Suricata Service\...`, a standard user may be able to create a candidate executable under `C:\Users\Public`.
- If enterprise deployment tooling installs Suricata into a shared operations directory with spaces and permissive ACLs, the same attack applies.

Therefore the finding should not be dismissed as purely theoretical or limited to misconfigured root directories. It is directly exploitable in realistic writable-parent deployments.

## Proof-of-Concept & Evidence

### Source Evidence

The vulnerable source locations are:

- `src/win32-service.c:222`: obtains executable path with `GetModuleFileName`.
- `src/win32-service.c:232-233`: appends arguments using raw spaces.
- `src/win32-service.c:249`: passes unquoted `path` to `CreateService`.
- `src/win32-service.c:253-254`: passes `NULL` for service account and password.
- `src/win32-service.c:347`: same path construction in service parameter update.
- `src/win32-service.c:357-358`: same raw argument append.
- `src/win32-service.c:376`: passes unquoted `path` to `ChangeServiceConfig`.

Git blame showed the vulnerable service construction pattern dates back to the older Windows service implementation and has remained materially unchanged in current code.

### Benign PoC Design

The validation PoC used:

- a launcher that calls `CreateProcessA(NULL, cmdline, ...)`;
- a benign payload executable that records:
  - actual module path;
  - `argv[0]`;
  - username;
  - user SID;
  - parsed arguments;
- positive and negative controls.

The PoC did not persist, elevate, modify services, or perform destructive actions.

### Control A: Unquoted Path With Earlier Candidate Present

Command line passed to `CreateProcessA`:

```text
C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe --service-mode -c suricata.yaml
```

Earlier candidate present:

```text
C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc.exe
```

Observed result:

```text
module_path=C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc.exe
argv0=C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc
user=One
sid=S-1-5-21-3388575877-2469963850-3643760867-1003
argc=7
arg1=path
arg2=test\Real
arg3=Dir\suricata.exe
arg4=--service-mode
arg5=-c
arg6=suricata.yaml
```

Conclusion: the earlier candidate executable was selected and launched.

### Control B: Quoted Path With Earlier Candidate Present

Command line passed to `CreateProcessA`:

```text
"C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe" --service-mode -c suricata.yaml
```

Earlier candidate still present:

```text
C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc.exe
```

Observed result:

```text
module_path=C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe
argv0=C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe
user=One
sid=S-1-5-21-3388575877-2469963850-3643760867-1003
argc=4
arg1=--service-mode
arg2=-c
arg3=suricata.yaml
```

Conclusion: quoting the executable path prevents the hijack.

### Control C: Unquoted Path After Removing Earlier Candidate

Command line passed to `CreateProcessA`:

```text
C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe --service-mode -c suricata.yaml
```

Earlier candidate removed.

Observed result:

```text
module_path=C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe
argv0=C:\Users\One\AppData\Local\Temp\suricata_usp_validation_env\svc path test\Real Dir\suricata.exe
user=One
sid=S-1-5-21-3388575877-2469963850-3643760867-1003
argc=4
arg1=--service-mode
arg2=-c
arg3=suricata.yaml
```

Conclusion: the hijack depends on candidate placement, and the intended binary runs when no earlier candidate exists.

### Writable-Parent Validation

A realistic writable-parent case was validated using:

```text
C:\ProgramData\Suricata Service\Real Dir\suricata.exe --service-mode -c C:\ProgramData\Suricata\suricata.yaml
```

Candidate path:

```text
C:\ProgramData\Suricata.exe
```

Observed result:

```text
module_path=C:\ProgramData\Suricata.exe
argv0=C:\ProgramData\Suricata
user=One
sid=S-1-5-21-3388575877-2469963850-3643760867-1003
argc=6
arg1=Service\Real
arg2=Dir\suricata.exe
arg3=--service-mode
arg4=-c
arg5=C:\ProgramData\Suricata\suricata.yaml
```

Conclusion: a standard non-admin user can exploit the path resolution primitive when Suricata is installed under a spaced directory beneath a commonly writable parent, or a parent writable in the assessed environment, such as `C:\ProgramData`.

### Writable-Parent ACL and Write Evidence

A standard non-admin user was able to create and delete benign probe files in commonly writable parent locations in the assessed environment:

```text
writable=false path=C:\suricata_usp_write_probe.tmp
writable=false path=C:\Program Files\suricata_usp_write_probe.tmp
writable=false path=C:\Program Files (x86)\suricata_usp_write_probe.tmp
writable=true  path=C:\ProgramData\suricata_usp_write_probe.tmp
writable=true  path=C:\Users\Public\suricata_usp_write_probe.tmp
writable=true  path=%TEMP%\suricata_usp_write_probe.tmp
```

The `C:\ProgramData` ACL also showed a `BUILTIN\Users` write grant in the assessed environment. This supports the deployment-dependent risk distinction: common protected Program Files installs are harder to exploit, while writable-parent service paths can be exploited by a standard user without unusual privileges where parent ACLs permit candidate creation.

### Disposable SCM Validation

A disposable Windows service was created from an elevated shell using an intentionally unquoted Suricata-shaped service path beneath a spaced writable-parent directory.

Service binary path used for validation:

```text
C:\ProgramData\SuricataUSP Validation\Real Dir\suricata.exe --service-mode -c C:\ProgramData\SuricataUSPValidation\suricata.yaml
```

Earlier candidate executable placed:

```text
C:\ProgramData\SuricataUSP.exe
```

The service configuration confirmed that the `BINARY_PATH_NAME` was unquoted and the service account was `LocalSystem`:

```text
SERVICE_NAME: SuricataUSPValidation
        TYPE               : 10  WIN32_OWN_PROCESS
        START_TYPE         : 3   DEMAND_START
        ERROR_CONTROL      : 1   NORMAL
        BINARY_PATH_NAME   : C:\ProgramData\SuricataUSP Validation\Real Dir\suricata.exe --service-mode -c C:\ProgramData\SuricataUSPValidation\suricata.yaml
        DISPLAY_NAME       : SuricataUSPValidation
        SERVICE_START_NAME : LocalSystem
```

Starting the disposable service invoked the marker executable at the earlier candidate path and executed it as LocalSystem:

```text
service_main_invoked=1
module_path=C:\ProgramData\SuricataUSP.exe
user=SYSTEM
sid=S-1-5-18
argc=1
arg0=SuricataUSPValidation
```

The disposable service was stopped/deleted after validation, and the temporary candidate executable and test directories were removed.

## Impact Assessment

### Confidentiality

High. LocalSystem execution allows access to sensitive files, process memory, credentials, and security tooling state.

### Integrity

High. An attacker can modify system configuration, service configuration, Suricata configuration, rules, binaries, logs, and other security controls.

### Availability

High. The attacker can prevent Suricata from running, crash the service, remove detection content, or disrupt host/network monitoring.

### Security Monitoring Impact

This finding is particularly important because Suricata is itself a security monitoring component. Exploitation can both elevate privileges and degrade defensive visibility.

### Blast Radius

The direct blast radius is local to the Windows host. Secondary blast radius depends on:

- credentials accessible from the compromised host;
- Suricata's role in network monitoring;
- trust relationships with SIEM, log collectors, and management systems;
- whether the host has access to packet capture interfaces or sensitive network segments.

## Remediation Guidance

### Code-Level Fix

The service binary path must quote the executable path separately from its arguments and must escape arguments according to Windows command-line parsing rules.

The service `ImagePath` should be constructed like:

```text
"C:\Program Files\Suricata\suricata.exe" -c "C:\Program Files\Suricata\suricata.yaml"
```

Recommended code changes:

1. Store the executable path separately from the argument string.
2. Quote the executable path unconditionally.
3. Escape embedded quotes and backslashes according to Windows command-line parsing rules.
4. Quote arguments that contain whitespace or shell-sensitive characters.
5. Apply the same helper to both `SCServiceInstall` and `SCServiceChangeParams`.
6. Fail closed if the final service command line would be truncated.
7. Prefer wide-character Windows APIs and a dedicated Windows argument-quoting helper instead of hand-building an ANSI command line with `strlcat`.
8. Add regression tests for paths with spaces, quoted arguments, long paths, Unicode paths, and service parameter updates.

### Deployment Mitigation

For deployed systems, inspect the current service path and quote the executable portion if needed:

```cmd
sc.exe qc Suricata
sc.exe config Suricata binPath= "\"C:\Program Files\Suricata\suricata.exe\" -c \"C:\Program Files\Suricata\suricata.yaml\""
sc.exe qc Suricata
```

### Hardening Recommendations

- Install Suricata under a protected directory with no standard-user write access.
- Avoid service executable paths under `C:\ProgramData`, `C:\Users\Public`, `%TEMP%`, shared writable directories, or network shares.
- Remove suspicious candidate executables such as `C:\Program.exe` or deployment-specific candidates like `C:\ProgramData\Suricata.exe`. For paths with later spaced components, also check candidates derived from those components, such as `C:\Program Files\Suricata Service.exe`.
- Audit ACLs on the Suricata installation directory and all parent directories.
- Restrict permissions to start, stop, and reconfigure the Suricata service.
- Use application control policies such as WDAC or AppLocker to prevent execution from writable directories.
- Add CI/static analysis checks for `CreateService` and `ChangeServiceConfig` call sites that construct service paths.

## Risk Classification

Recommended severity: High.

The vulnerability enables arbitrary code execution as LocalSystem when exploited against a vulnerable Windows service configuration. The impact is severe and can result in full host compromise and security monitoring degradation.

It is not classified as Critical because exploitation is local, requires write access to an earlier candidate path, and requires a service start or restart. Risk increases in deployments where Suricata is installed under commonly writable parents, or parents writable in the assessed environment, such as `C:\ProgramData` or `C:\Users\Public`, where service restarts are automated, or where Suricata hosts have privileged monitoring access.

OISF may assign a different severity under its own policy. Their Critical category is focused on remotely triggerable, traffic-based issues in default-enabled Tier 1 functionality, while command-line or unlikely-configuration issues may be rated lower. The recommended submission framing is therefore: High local privilege escalation, deployment-dependent, not remote RCE.

Typical protected Program Files deployment:

```text
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H = 7.0 High
```

Writable-parent deployment:

```text
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 7.8 High
```

Status: confirmed in the assessed source tree and validated with live SCM execution as LocalSystem. No public Suricata-specific CVE, GHSA, or advisory was identified during review.
