28 min readUpdated

CVE-2026-18759: Privilege Escalation in ASUSTOR Backup Plan

ASUSTOR Backup Plan’s Windows service ABP_VSS_Service treats encryption as authentication and checks paths with a substring test. Standard user to SYSTEM.

Contents

A note on disclosure. This finding was disclosed to ASUSTOR and is tracked as CVE-2026-18759 (ASUSTOR advisory AS-2026-021). It is fixed: upgrade Backup Plan to 2.0.8.7230 or later, and EZ Sync to 1.1.1.7230 or later. The advisory rates it CVSS v4.0 8.5 and covers both products. This write-up is published with the vendor’s agreement.

Introduction

Two patterns show up over and over in Windows software that runs with elevated privileges.

The first: treating “this message is encrypted” as proof that “this message is legitimate.” Encryption protects secrecy; it does not prove identity. The full argument is in §1.4, but the one-line version is enough: “it decrypts” proves only that the sender had the key.

The second: guarding a file path with a substring check instead of a containment check. dest.Contains("C:\\AllowedFolder") asks whether a string appears somewhere in the destination; it does not ask whether the resolved path actually lives inside that folder. The two questions have different answers, and ..\..\ walks between them.

Neither pattern, alone, is necessarily exploitable. Together, in a service running as NT AUTHORITY\SYSTEM, they compose into a clean standard-user → SYSTEM local privilege escalation — the strong case, the boundary that actually matters. This post is a case study of both, found in a real product and proven end-to-end on a live machine.

The product is ASUSTOR Backup Plan (version 2.0.7.10171), the Windows desktop client for ASUSTOR’s network-attached storage (NAS) devices. It installs one background service, ABP_VSS_Service, that runs as SYSTEM. That service will copy any file to or from anywhere on the machine for anyone who writes two lines to a text file — and when the write primitive is chained with how the service launches child processes, it escalates all the way to an interactive SYSTEM shell on the user’s desktop. You do not need to be an administrator. You need to know one key — and the key is readable by every user on the machine.

I want to be honest about the severity up front, because being honest about impact is a habit worth keeping. This is a genuine standard-user → SYSTEM escalation, reproduced in all three forms — reading a SYSTEM-only file, writing one, and launching a SYSTEM-privilege command shell — on a clean Windows 11 machine from an account that is a member of Users and nothing else. There is exactly one honest caveat, and I state it plainly in Part 8: the service’s copy step relies on wmic.exe, a tool Microsoft has been withdrawing from Windows in stages since 2021 and removed outright in the August 2026 update. On a machine that still carries it, the chain is fully reliable; Part 8 gives the timeline.

Here is the one idea to take away before any of the detail. The service treats “this request is encrypted” as if it meant “this request is legitimate.” Those are not the same thing, and the gap between them is where the vulnerability lives. Encryption keeps a message secret; it does not tell you who sent it. In this case, the key is stored in a file that every user on the machine is allowed to read — so “it decrypts” does not narrow down the sender.

So there are two stories here, and you can read either on its own:

  1. The vulnerability. A SYSTEM service that accepts an encrypted note as proof of identity, proven by reverse-engineering the .NET code and then by reproducing it on a live machine. → Parts 2–7.
  2. The patterns. Two design choices that recur across a lot of software — encryption is not authentication, and a substring test is not a path check — combine here into something far worse than either alone. → Key takeaways.

(Part 8 then states the one caveat honestly, and Part 9 turns the finding into detections and fixes — for the people who have to stop it.)

One more thing, for readers of the earlier posts here. The mechanical work — enumerating the attack surface, disassembling the IL, and driving the reproductions on the VM — was carried out by an AI agent I built and direct, following the methodology I wrote up in Rebuilding a Security Researcher’s Mind in an AI and used in RogueProvision. I chose the target, adjudicated every finding, and drew the line on impact (including the wmic caveat below); I keep that machinery in the background here so the bug stays in focus. I will explain every Windows term in plain words as it appears; you do not need to be a Windows internals person to follow along.


Part 1: Background — the mailbox, the key, and one idea

This part builds the three things the rest of the post stands on: the file “mailbox” the service listens on, the key that is supposed to lock it, and one sentence about what encryption does and does not do. Skim to Part 2 if none of that is new.

1.1 The product and its one SYSTEM service

The build analyzed here is version 2.0.7.10171. The product installs one background service, ABP_VSS_Service, that starts automatically at boot and runs as LocalSystem — the only persistent SYSTEM-level piece of the product, and so the single door worth attacking. Everything below is about how that service decides whose requests to honor.

1.2 The file “mailbox”

How does the ordinary, non-privileged app talk to its SYSTEM service? Through inter-process communication (IPC) — any mechanism by which two separate programs exchange messages. Windows offers IPC mechanisms that can verify the caller’s identity (named pipes and ALPC ports check who is calling). This service uses a simpler approach: a file drop. It watches a folder for a file called request.txt; whoever writes that file has sent it a message. A file in a shared folder has no built-in notion of who wrote it — and that missing notion is the seam the whole finding lives in.

Windows does guard every file and folder with an ACL (an access control list — the set of rules saying which accounts may read or write it). But an ACL governs access, not authorship: it can say “any user may write here” while recording nothing about which user actually did. Throughout this post, per-file rules appear as lines like Users:(RX) (read-only for the Users group) and Authenticated Users:(M) (modify/write for any signed-in account).

1.3 DPAPI, and the one setting that decides everything

The service does not accept just any text as a request — the two lines inside request.txt are encrypted. To encrypt and decrypt them it needs a key, and that key is itself stored on disk, protected by a built-in Windows feature called DPAPI (the Data Protection API — Windows’ standard way to encrypt a secret so it can be stored on disk and decrypted later without your program having to hold the master key).

DPAPI has one setting that matters enormously here: who is allowed to decrypt the secret. You choose it when you protect the data:

  • CurrentUser scope — only this one user account can decrypt it. A different user on the same machine cannot.
  • LocalMachine scope — any account on this machine can decrypt it. The secret is bound to the computer, not to a person.

Keep those two apart; the difference between them is, quite literally, the vulnerability. Software that stores a shared secret with CurrentUser under a dedicated service account keeps it private. Software that stores it with LocalMachine has published it to everyone who can log in.

1.4 “Encryption is not authentication”

The last idea is the one the whole post turns on, and it is worth saying slowly because the two concepts are easy to conflate.

Encryption answers “can an eavesdropper read this?” — it protects secrecy. Authentication answers “who actually sent this?” — it protects identity. They are different questions with different answers. If I hand you a locked box, the lock proves nobody read the contents in transit. It does not prove I am the one who packed it — anyone holding a copy of the key could have packed a box that opens with the same key. So “the message decrypts cleanly with our key” tells you the message was made by someone who had the key. That is only as strong as how few people have the key. When the key is readable by everyone (§1.3), “it decrypts” proves nothing about the sender at all.

This is exactly the situation in ABP_VSS_Service. The service does not check who wrote request.txt. Its only implicit “authentication” is that the message decrypts — and, as we will see, every user on the machine can produce a message that does.


Part 2: The mailbox anyone can post to

Here is the central image of the whole post. Plant it now, before any disassembly, and everything else is detail.

ABP_VSS_Service runs a loop, once every five seconds. Each tick it looks for C:\AsustorTempFiles\ABP\request.txt. If the file is there, it reads two lines, decrypts each into a source path and a destination path, and — as SYSTEM — copies the source file to the destination. Then it deletes the request and waits for the next one.

Think of the folder as a mailbox and the service as a SYSTEM-privileged errand-runner who checks it every five seconds and carries out whatever note is inside. For that to be safe, two things have to be true: only the app should be able to post a note (so strangers cannot send errands), and a note should only ever move files within the app’s own working area (so even a legitimate note cannot reach into Windows). The service has guards for both — but as the analysis will show, each one can be bypassed. Figure 1 shows the path a note takes.

The SYSTEM service that trusts a note standard user member of Users only C:\AsustorTempFiles\ABP the shared mailbox Authenticated Users = Modify ABP_VSS_Service SYSTEM · polls every 5s arbitrary file read + write as NT AUTHORITY\SYSTEM drops picks up Anyone may post a note; the service carries it out as SYSTEM.

Figure 1 — One SYSTEM service, one shared mailbox. The service copies files as SYSTEM for whoever drops a note. Whether a stranger can post one, and whether a note can reach outside the folder, are the two questions Parts 3 and 4 answer.

We can already write down the developer’s implicit assumption — the sentence they believed so completely they never wrote the check for it:

“Only Backup Plan itself posts requests, and a request only ever moves files inside the backup working folder.”

Neither clause holds in practice. The rest of the post is demonstrating that, first by reading the code, then by reproducing it on a live machine.


Part 3: The first lock — a key everyone can read

3.1 A one-paragraph primer on reverse engineering .NET

To understand the authentication model, you have to read the service’s code. ABP_VSS_Service is written in .NET (Microsoft’s managed runtime; the languages C# and VB.NET compile to it). .NET programs do not ship as raw machine code — they ship as IL (Intermediate Language), a compact stack-based bytecode. A free Microsoft tool called ILDASM turns that bytecode back into readable IL text, and from the IL you can faithfully reconstruct the original C#. That is the whole toolchain here: no fancy decompiler, just ILDASM and careful reading. The C# I show below is a faithful transcription of the shipped IL; where a single line is load-bearing, I quote the raw IL next to it.

3.2 The key is protected with LocalMachine scope

The request’s two lines are AES-encrypted, and the key comes from a helper called SecureKeyManager. The key file lives at C:\ProgramData\ASUSTOR\Backup Plan\key.bin. Here is what ReadKey does — the entire method:

public static byte[] ReadKey() {
    if (!File.Exists(KeyPath)) return null;
    return ProtectedData.Unprotect(
        File.ReadAllBytes(KeyPath),
        null,
        DataProtectionScope.LocalMachine);
}

The last argument is the security-relevant choice. LocalMachine binds the protected blob to the machine, not to a user — so any process, at any integrity level (the trust tier Windows stamps on each process; a standard-user or sandboxed process runs at a lower tier than an administrator), on any account, can call ProtectedData.Unprotect on key.bin and get the AES key back.

The load-bearing bytes in the shipped IL confirm this — the constant 1 pushed at IL_0019 is DataProtectionScope.LocalMachine:

IL_0013:  call    File::ReadAllBytes(string)
IL_0018:  ldnull
IL_0019:  ldc.i4.1
IL_001a:  call    ProtectedData::Unprotect(
            uint8[], uint8[],
            DataProtectionScope)

And it does not even take that much effort: the key file is also readable by ordinary users via its file permissions, and the mailbox folder lets ordinary users write. I confirmed both on the test host:

C:\ProgramData\ASUSTOR\Backup Plan\key.bin
    BUILTIN\Users:(RX)

C:\AsustorTempFiles\ABP
    Authenticated Users:(M)

Any user can read the key. Any user can write request.txt. The encryption does not restrict access to the IPC.

3.3 A strong lock is not the same as a secure one

Here is the part worth dwelling on, because it is the general lesson. The message cryptography itself is not weak. The key derivation runs PBKDF2-HMAC-SHA256 for 120,000 iterations (the IL literal is ldc.i4 0x1d4c0), producing 64 bytes that are split into an AES-256-CBC key and an HMAC-SHA256 integrity key. If you were trying to brute-force the ciphertext without the key, you would get nowhere. On its own, that is a textbook-correct scheme.

None of that strength matters, because the key is not secret from the attacker. This is the §1.4 distinction made concrete (Figure 2): a strong cipher, whose key is stored where every user can reach it. “The request decrypts with our key” is treated as proof the request came from Backup Plan. It proves only that the request came from someone who could read key.bin — which is everyone.

The lock is strong; the key is public What protects it: strong AES PBKDF2-HMAC-SHA256 × 120,000 sound, taken on its own Where the key lives: readable by all key.bin · DPAPI LocalMachine bound to the machine, not a user any local account can Unprotect() encryption ≠ authentication but its key:

Figure 2 — The lock is strong; the key is public. The AES scheme (green) is correct in isolation; the security-relevant consequence (red) is that its key is stored with LocalMachine scope and user-readable permissions, so every account can derive it. The message “decrypts,” but that does not identify the sender.

So the first lock is open. Any user can derive the key and produce a request the service will accept. Now, what will the service do with a request? That is the second lock.


Part 4: The second lock — a guard that only checks a substring

Even with a valid request, the service is supposed to keep the copy inside its own folder. The worker loop, Service1.DoWork, is where that guard lives. Here is the per-tick body, reconstructed from the IL:

string requestPath =
    @"C:\AsustorTempFiles\ABP\request.txt";
string allowedRoot =
    @"C:\AsustorTempFiles\ABP";
if (File.Exists(requestPath)) {
    var lines = File.ReadAllLines(requestPath);
    if (lines.Length >= 2) {
        string key = Convert.ToBase64String(
            SecureKeyManager.ReadKey());
        string source =
            AesEncrypt.Decrypt(key, lines[0]);
        string dest =
            AesEncrypt.Decrypt(key, lines[1]);
        if (File.Exists(source)
            && dest.Contains(allowedRoot)) {
            CpLockedFile(source, dest);
        }
    }
    File.Delete(requestPath);
}

Two security-relevant properties sit in that one if. The first we already have: the caller’s identity is not verified. There is no RpcImpersonateClient, no token lookup, no owner check on the file — the code does not ask who wrote request.txt. The only implicit gate is “the message decrypts,” which §3 showed is not restrictive.

The second is the destination guard: dest.Contains(allowedRoot). That asks “does the destination string contain the text C:\AsustorTempFiles\ABP somewhere in it?” — a substring test. It does not ask “does the destination, once resolved, actually live inside that folder?” — a path-containment test. Those are different questions, and a path traversal slips between them. .. is the filesystem’s “go up one level”; a path with enough .. segments climbs out of any folder. Watch what happens to a destination that keeps the folder name in the string but walks out with .. (Figure 3):

One substring check, two paths dest.Contains("…\ABP") passes for BOTH paths below intended destination C:\AsustorTempFiles\ABP\backup.tmp resolves inside the folder the traversal payload …\ABP\..\..\Windows\System32\evil.dll resolves into System32, as SYSTEM Contains sees the folder name; File.Copy follows the ..\ out of it.

Figure 3 — A substring check is not a path check. Both destinations satisfy Contains because both strings hold the folder name; only the left one actually stays inside. The right one — ...\ABP\..\..\Windows\System32\evil.dll — passes the same check, then File.Copy resolves the ..\..\ and writes into System32.

The correct check is one extra step the code never takes: resolve the destination to its real, absolute form with Path.GetFullPath, then confirm it starts with the allowed folder (plus a trailing separator). The service instead trusts the raw string, hands it to File.Copy, and the ..\..\ does the rest.


Part 5: The copy that runs as SYSTEM

Both locks are open. What remains is to see the actual copy, because the way it copies is what makes the read primitive so strong. The guarded call, CpLockedFile(source, dest), does two things:

private void CpLockedFile(
        string source, string dest) {
    string volume =
        Path.GetPathRoot(source).TrimEnd('\\');
    var psi = new ProcessStartInfo {
        FileName = "wmic",
        Arguments =
            "shadowcopy call create Volume="
            + volume + "\\",
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true
    };
    var p = Process.Start(psi);
    string stdout =
        p.StandardOutput.ReadToEnd();
    p.WaitForExit();

    string id = null, shadowPath = null;
    if (stdout.Contains("ReturnValue = 0")) {
        var m = Regex.Match(stdout,
                    "\\{([0-9A-Fa-f-]+)\\}");
        if (m.Success) {
            id = m.Groups[1].Value;
            shadowPath =
                GetShadowCopyPath("{" + id + "}");
        }
    }
    if (!string.IsNullOrEmpty(shadowPath)) {
        string shadowSource =
            shadowPath + source.Substring(2);
        File.Copy(shadowSource, dest, true);
        DeleteShadowCopy(id);
    }
}

Two notes on the listing before the analysis. The logging calls are removed for space; everything else follows the decompiled method. And GetShadowCopyPath() and DeleteShadowCopy() are the service’s own method names, not mine — both are worth opening, because each one spawns wmic again. GetShadowCopyPath() runs wmic shadowcopy get ID, DeviceObject and matches the \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyN device path against the identifier pulled out of the create output; DeleteShadowCopy() runs wmic shadowcopy where "ID='{...}'" delete after the copy. So one request drives three wmic launches, in that order, and the third one happens after File.Copy has already run. Part 7 turns on that ordering. (The assembly also contains an unused ExtractShadowCopyPath() that scans the create output for the device path inline — dead code, and not the path that runs.)

Two things matter here. First, FileName = "wmic" — a bare filename, no directory prefix, at all three launch sites. This detail will become critical in Part 7.

Second, and more immediately: the shadow copy. A Volume Shadow Copy (VSS) is a point-in-time, read-only snapshot of a whole drive that Windows can create even while files are open and locked. Backup software uses it legitimately to copy files that are in use. Here the SYSTEM service makes a shadow copy of the C: drive and reads the source through the snapshot — so files an ordinary user can never open, including the locked, in-use registry hives that hold Windows’ password data, are copied out without trouble. The destination is the raw attacker string from Part 4, and overwrite = true means it will clobber an existing file. Put the three parts together and the full chain is short (Figure 4):

The attacker writes two lines attacker standard user SYSTEM ABP service t0 · derive AES key key.bin → Unprotect t1 · drop request.txt enc(source) + enc(dest) t2 · decrypt the lines source, dest t3 · wmic VSS snapshot shadow copy of C: t4 · File.Copy shadowSource → dest read + write as SYSTEM next 5s poll The attacker is gone after t1. Steps t2–t4 are the vendor’s service.

Figure 4 — The attacker writes two lines; the SYSTEM service does the privileged part. The attacker’s whole job is steps t0–t1: derive the key, drop the two-line request. Everything privileged — the shadow copy, the copy into System32 or out of the hives — is done by the vendor’s SYSTEM service on the next poll.


Part 6: Did it actually work? — the proofs on a live machine

Reading a binary tells you what should happen. A finding is only worth something once you make it happen and watch it. So the work moved to a Windows 11 machine with the software installed and the service running, and — the important part — a genuine standard user, a member of Users and nothing else, whose token runs at Medium integrity (the normal level for a standard interactive user — below an elevated administrator’s High, and far below SYSTEM). No administrator anywhere in the exploit.

First, prove the wall is real. Before claiming an indirect path defeats a boundary, you check the boundary exists. As the standard user, a direct read of C:\Windows\System32\config\SAM (the file holding local account password hashes) is refused with access denied, and a direct write into C:\Windows\System32 is refused too. Good — those are real walls, so getting around them is a real finding.

6.1 Arbitrary read — steal the password hive as SYSTEM

The PoC derives the AES key from key.bin (Part 3), encrypts a request with source = C:\Windows\System32\config\SAM and a destination inside the mailbox folder, and drops the two-line request.txt. On the next five-second poll the service reads the request, makes a VSS shadow copy of the C: drive, and copies the SAM hive — through the shadow, so the file lock is irrelevant — to a location the standard user can read:

Shadow Copy created:
  \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyN
File copy:
  ...ShadowCopyN\Windows\System32\config\SAM
  to C:\AsustorTempFiles\ABP\exfiltrated_SAM
Successfully copied: ...\config\SAM

The result: the real SAM hive, 65,536 bytes, file magic 72 65 67 66 (regf — a genuine registry hive), now sitting in a folder the standard user can read. Combine it with the SYSTEM and SECURITY hives (fetched the same way) and an offline tool like secretsdump recovers the local Administrator’s NTLM hash (the password-derived credential Windows uses for network authentication) and the machine’s stored LSA secrets (cached service-account passwords and other credentials the OS keeps for internal use). With that hash, an attacker authenticates as the local Administrator via pass-the-hash (a technique where the hash itself is used to authenticate, bypassing the need for the plaintext password) — and the account boundary is gone.

6.2 Arbitrary write — plant a file in System32 as SYSTEM

The other direction. Stage content in a file the standard user can write (say C:\Users\Public\payload.txt), then send a request whose destination uses the Part 4 traversal:

source:
  C:\Users\Public\payload.txt
dest:
  C:\AsustorTempFiles\ABP\
  ..\..\Windows\System32\proof.txt

On the next poll the service copies it — as SYSTEM, with overwrite = true — and the file appears in System32, owned by BUILTIN\Administrators, with content authored by the low-privilege user. A path that the standard user cannot write to directly now contains attacker-controlled content, placed there by SYSTEM.

From an arbitrary SYSTEM write, code execution as SYSTEM is a short, well-worn step: drop a DLL where a SYSTEM process will load it, or overwrite one it already loads. Either primitive alone is a complete escalation; the machine has two independent ones (Figure 5).

Two primitives, one outcome arbitrary READ copy any file out, via VSS SAM / SYSTEM / SECURITY locked registry hives offline secretsdump admin hash + LSA secrets arbitrary WRITE plant any file, overwrite DLL into System32 a SYSTEM process loads it code runs as SYSTEM standard DLL-plant gadget SYSTEM full compromise Either primitive alone is enough: a standard-user → SYSTEM escalation.

Figure 5 — Two primitives, one outcome. The read primitive copies the locked password hives out and feeds an offline hash dump; the write primitive plants a DLL a SYSTEM process loads. Both land at SYSTEM, so either one alone is a full escalation.


Part 7: From arbitrary write to a SYSTEM shell

The read and write primitives from Part 6 are both complete escalation paths in theory — an offline hash dump from the read, a DLL plant from the write. But there is a shorter path that does not need a second tool or a reboot, because the vulnerable service itself provides the final stepping stone.

7.1 The unqualified process launch

Recall from Part 5 that servicing one request spawns wmic three times — create, query, delete — and that every one of them is Process.Start("wmic", ...). The filename is bare"wmic", not "C:\Windows\System32\wbem\wmic.exe". When Windows’ CreateProcess receives a bare name (no directory separator), it resolves it using a fixed search order (Microsoft’s documentation lists one more entry, the 16-bit system directory, which does not exist on a 64-bit target and is left out here):

  1. The directory the calling process was loaded from (the application directory)
  2. The current directory of the calling process
  3. The system directory (System32)
  4. The Windows directory
  5. The directories in PATH

For ABP_VSS_Service.exe, the application directory is C:\Program Files (x86)\ASUSTOR\Backup Plan\. A standard user cannot write there directly — its ACL is Users:(RX). But the SYSTEM arbitrary-write primitive from Parts 3–4 can — the service copies files as LocalSystem, which has full control on every directory. The write primitive turns a non-writable directory into a writable one, through the service itself.

7.2 The chain

The escalation is two IPC requests as I ran it, both sent by the standard user, both executed by the SYSTEM service (Figure 6) — and, as the next paragraphs show, the code reaches the planted executable before the first request is even finished:

Request 1 — plant. The attacker stages a small native x64 PE (Portable Executable — the format Windows uses for .exe files; 4 KB) at a user-writable path, then drops a request with a destination that traverses into the service’s own application directory: dest = C:\AsustorTempFiles\ABP\..\..\Program Files (x86)\ASUSTOR\Backup Plan\wmic.exe. The substring guard passes (the string contains the required folder name). File.Copy resolves the traversal and writes the payload as wmic.exe into the service’s own directory. The first two wmic launches of this request — create and query — used the real wmic.exe, because the planted file did not exist yet when CreateProcess ran for them.

The third launch, and why one request is enough. DeleteShadowCopy() runs after File.Copy, in the same call to CpLockedFile. By then the planted file is on disk, so that third CreateProcess finds it at search-order position 1 and stops there. The real wmic.exe is never consulted. Note where it actually lives: there is no wmic.exe in System32 itself, so position 3 does not find it either. It sits in System32\wbem, which the search reaches only at position 5, as one of the directories listed in PATH. The planted payload executes as NT AUTHORITY\SYSTEM.

Request 2 — trigger. The recorded proof of concept sends a second request anyway, and that is what the recording below shows. Sending any further valid request calls Process.Start("wmic", ...) again and reaches the planted executable from the very first launch, so the trigger does not depend on the delete step being reached at all. Two requests is the form I ran; one is the minimum the code allows.

Two requests to a SYSTEM shell CreateProcess search order (1) app dir · planted wmic.exe (5) PATH · System32\wbem real wmic.exe Request 1 · plant payload → service directory Request 2 · trigger any valid IPC request SYSTEM cmd.exe on the user’s desktop stops at (1) — real wmic skipped writes here resolves here Request 1 plants the payload; Request 2 makes the service run it.

Figure 6 — From a planted file to a SYSTEM shell. Request 1 uses the write primitive to plant a payload in the service’s application directory; Request 2 triggers the service to call Process.Start("wmic"), which now resolves to the planted executable. The real wmic.exe in System32\wbem is never reached.

7.3 The result

The payload opens a cmd.exe window on the user’s desktop, running as NT AUTHORITY\SYSTEM. A standard user who was denied access to the SAM hive and to System32 moments earlier now has an interactive SYSTEM shell — the highest privilege level Windows offers. The payload then deletes itself from the service directory. That removes the file, not the evidence: the request file, the copy into the application directory, and the SYSTEM service spawning wmic are all still visible to anything watching the endpoint. Part 9 lists what a defender should look for. The service itself continues running normally throughout.

Live capture — from a standard-user session, the PoC plants a payload via the service's own write primitive, then triggers it. A cmd.exe window appears running as NT AUTHORITY\SYSTEM.

7.4 Why this works on a default installation

This chain requires no non-default configuration. The service directory ACL is the standard Users:(RX) set by the installer — the attacker never writes there directly. The SYSTEM service does the write on the attacker’s behalf, through the same two bypassable guards from Parts 3 and 4. The only precondition is the presence of wmic.exe, which is the same precondition as the underlying file-copy vulnerability itself (Part 8).

This is worth contrasting with a related finding in the same vendor’s software. CVE-2025-13051 (a DLL-hijack vulnerability, scored CVSS v4.0 9.3 by ASUSTOR) requires the user to install the software into a non-default, user-writable directory. The chain here works on a completely default installation — the write primitive makes the otherwise non-writable service directory accessible by using the service’s own SYSTEM privileges against it.


Part 8: The one honest caveat — the wmic precondition

Every finding has a boundary condition, and hiding it would be dishonest, so here is this one in full.

The service’s copy step (Part 5) does not call the shadow-copy API directly. It shells out to wmic.exewmic shadowcopy call create ... — and parses that program’s text output to find the snapshot. The copy only proceeds if wmic returns a shadow device path. That makes the whole chain — the read, the write, and the SYSTEM code execution from Part 7 — depend on wmic.exe being present.

Microsoft has been withdrawing wmic (the WMI command-line tool) from Windows in stages, and where it stands has moved since this post first went up. So here is Microsoft’s own timeline rather than my summary of it. It was deprecated in Windows 10 version 21H2 in 2021. In Windows 11 version 22H2 it shipped as a Feature on Demand that was preinstalled and enabled by default. In 2024 it became disabled by default in versions 23H2 and 24H2, still installable on request. In 2025, upgrading to version 25H2 removed it where it was already installed, and it could still be added back afterwards. And in the August 2026 preview update for 24H2 and 25H2 it was removed outright and is no longer available as a Feature on Demand at all; the same is true of 26H1.

So the population where this chain fires is the population that still has the file: Windows 10, and any Windows 11 machine that carried wmic forward and has not yet taken that August 2026 update. That was a very large population when I tested, and it is shrinking. I ran the exploit against the real wmic, no substitution. Where wmic is absent, this particular copy step does not complete and the chain does not fire. Because the copy runs through the shadow copy in both directions, the precondition gates all three primitives equally: with wmic absent, neither the SAM read, the System32 write, nor the SYSTEM shell completes. That is a property of the vendor’s implementation choice (they chose to drive VSS by spawning wmic), not a limitation of the technique — a version of the service that called the VSS COM (Component Object Model) API directly would remove the dependency entirely (and, done right, the vulnerability with it).

One thing worth being explicit about: wmic’s removal breaks the copy mechanism, not the root cause. Even on a machine where wmic is gone and this particular chain does not fire, the unauthenticated IPC — the real vulnerability — survives. The file drop still accepts requests from any user; the path traversal still lets an attacker aim the service at arbitrary paths. What changes is only whether the service can execute the copy. If a future version replaces wmic with a direct COM call (or any other VSS driver), the full chain — read, write, and code execution — comes back unless the IPC itself is fixed.


Part 9: Detection and defense

Every offensive finding implies a defense; deriving it is half the value of the work.

If you run ASUSTOR Backup Plan, the fix is to upgrade: 2.0.8.7230 or later for Backup Plan, 1.1.1.7230 or later for EZ Sync. The mitigations below were written against the unpatched version and are what you can apply yourself if you cannot upgrade immediately:

  • Tighten the mailbox folder. Remove Authenticated Users: Modify from C:\AsustorTempFiles\ABP; restrict write access to SYSTEM and the service account. This alone stops a standard user from posting a request.
  • Lock down the key file. Remove Users:(RX) from C:\ProgramData\ASUSTOR\Backup Plan\key.bin — though note this does not fix the LocalMachine scope, so it is a partial measure.
  • If you do not use the backup service continuously, consider stopping/disabling ABP_VSS_Service when idle.

If you defend Windows endpoints, here is what to watch:

- a non-SYSTEM process writing
  C:\AsustorTempFiles\ABP\request.txt
- ABP_VSS_Service.exe (SYSTEM) spawning wmic
  outside a scheduled backup window
- a copy by ABP_VSS_Service.exe whose dest
  resolves OUTSIDE C:\AsustorTempFiles\ABP
- reads of ...\config\{SAM,SYSTEM,SECURITY}
  landing in a user-readable folder
- a new wmic.exe appearing in the Backup Plan
  app directory (the search-order tell)

Recommended fixes, in priority order:

  • Authenticate the caller, not the message. Replace the file drop with an authenticated channel — a named pipe or ALPC port that checks the client’s token/SID (Security Identifier — the unique ID Windows assigns to each account) — so “the message decrypts” cannot be confused with “the sender is trusted.”
  • If a shared secret is kept, keep it secret. Protect the key with CurrentUser scope under the service account (or a non-guessable optionalEntropy), in a location ACL’d to the service alone — never LocalMachine scope with Users:(RX).
  • Compare paths, not substrings. Canonicalize the destination with Path.GetFullPath, then confirm the result StartsWith the intended root plus a trailing separator — never Contains.
  • Use a fully qualified path for child processes. Replace Process.Start("wmic", ...) with C:\Windows\System32\wbem\wmic.exe (or better, call the VSS COM API directly and remove the wmic dependency).
  • Fix the shared library, not just one product. The same vulnerable code — same VSS_Service namespace, same SecureKeyManager with DPAPI LocalMachine, same Contains guard, same wmic shell-out — ships in at least one sibling product: ASUSTOR EZ Sync (AES_VSS_Service.exe), reachable through an identical IPC directory at C:\AsustorTempFiles\AES\. Patching Backup Plan alone would have left the sibling exploitable. ASUSTOR did fix both: CVE-2026-18759 names ABP 2.0 through 2.0.7.10171 and AES 1.0 through 1.1.1.3113 as affected, and the advisory shipped an AES release on 10 August 2026 and an ABP release on 12 August 2026.

Key takeaways

You do not need to remember the paths. Two ideas are worth keeping.

On the vulnerability. A SYSTEM backup service copies any file to or from anywhere for anyone who can post an encrypted note — because it relies on encryption as proof of identity, and the encryption key is stored where every user can read it, and the copy destination is validated with a substring test a ..\ walks straight out of. The write primitive then turns the service’s own privileges against it: a planted executable in its application directory is launched as SYSTEM on the next request, giving the attacker an interactive SYSTEM shell from a standard user account. Two common design choices, each survivable alone, compose into a clean standard-user → SYSTEM escalation, proven all three ways on a live machine.

On the two patterns, because they are everywhere. Encryption is not authentication — a lock proves secrecy, never identity, and it proves even that only as far as the key is actually secret; the moment the key is LocalMachine-scoped and world-readable, “it decrypts” does not identify the sender. And a substring is not a path — Contains("...\ABP") is not “inside ...\ABP”; canonicalize before you compare, always. If you write privileged software, these two lines are worth taping to the wall.

I hope this was useful. More to come.


A few notes, if you want to read more

Kazuma Matsumoto. “CVE-2026-18759: Privilege Escalation in ASUSTOR Backup Plan”. 637th Research Lab, 2026-08-12. https://y637f9qq2x.com/posts/asustor-lpe/