Share
## https://sploitus.com/exploit?id=59920917-A8BC-5B76-924B-0398244FD7A3
# TOCTOU: HMAC Checks Disk, Executes from Memory

**Notepad++ v8.9.6.2 โ€” Attacker-Controlled UserCommand Execution via HMAC Race Condition**

## Advisory

| | |
|---|---|
| **GHSA** | [GHSA-qm4c-qg8p-qfcr](https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-qm4c-qg8p-qfcr) |
| **CVE** | CVE-2026-52885 |
| **Severity** | High |
| **Affected** | v8.9.6.2 |
| **Patched** | v8.9.6.4 |
| **Patch Commit** | [4f7563c](https://github.com/notepad-plus-plus/notepad-plus-plus/commit/4f7563c) |
| **Affected File** | `PowerEditor/src/NppCommands.cpp` :4328โ€“4359 |
| **Platform** | Windows 11 Pro x64 |
| **Privilege Required** | Standard user (write access to `shortcuts.xml`) |
| **Researcher** | v3s9er |
| **Disclosed** | 2026-06-05 |

---

## Summary

Notepad++ v8.9.6.2 introduced an HMAC-SHA256 integrity check on `shortcuts.xml` to prevent injection of arbitrary `UserCommand` entries.

The check has a TOCTOU flaw: the HMAC is verified against the **on-disk file** at trigger time, but the command that executes is read from the **in-memory `_userCommands` vector** loaded at startup. These two sources are never re-synchronized.

An attacker who can write to `shortcuts.xml` can plant a malicious command before launch, let Notepad++ load it into memory, restore the legitimate file on disk, then trigger the command. The HMAC check passes (disk is clean), but the malicious payload executes (memory is stale).

---

## Technical Overview

The integrity mechanism validates the current on-disk `shortcuts.xml`, while command execution uses a previously loaded in-memory representation. Because the two data sources are independent, an attacker can alter the file before startup, allow it to be parsed into memory, then restore the original file before command execution โ€” causing the HMAC check to pass while the malicious payload runs.

---

## Root Cause

`NppCommands.cpp` lines 4328โ€“4359:

```cpp
// TIME OF CHECK โ€” reads on-disk file
std::string currentHMAC = computeHMAC(
    getMachineGUID(),
    getFileContent(nppParams.getShortcutsPath().c_str())
);
if (currentHMAC != nppGUI._shortcutsXmlHmacInConfig) { return; }
// validated against DISK

// TIME OF USE โ€” reads in-memory vector (loaded at startup, never refreshed)
int i = id - ID_USER_CMD;
const vector& cmds = nppParams.getUserCommandList();
UserCommand ucmd = cmds[i];
Command cmd(string2wstring(ucmd.getCmd(), CP_UTF8));
cmd.run(_pPublicInterface->getHSelf()); // executes payload from MEMORY
```

| Operation | Source | Timing |
|-----------|--------|--------|
| CHECK (HMAC) | Disk file | At trigger time |
| USE (exec) | Memory vector | Loaded at startup |
| GAP | Attacker-controlled | Startup โ†’ Trigger |

---

## Attack Scenario

**Pre-condition:** write access to `shortcuts.xml`
- Portable install: same directory as `notepad++.exe`
- Standard install: `%AppData%\Notepad++\`

**Steps:**

1. Overwrite `shortcuts.xml` on disk with a malicious version (`cmd = calc.exe`)
2. Launch Notepad++ โ€” `_userCommands` is populated from the malicious file
3. Restore the legitimate `shortcuts.xml` to disk โ€” HMAC now matches `config.xml`
4. Send `WM_COMMAND(21000)` (`ID_USER_CMD+0`) or press `Ctrl+Alt+P`
5. HMAC check reads the legitimate file from disk โ†’ **PASSES**
6. Execution reads `_userCommands[0]` from memory โ†’ **calc.exe runs**

**Result:** execution of attacker-controlled commands under the privileges of the current user. No admin, no UAC, no user warning.

---

## Execution Timeline

| Time | Event | State |
|------|-------|-------|
| T+0.0s | Attacker writes MALICIOUS shortcuts.xml | disk=MALICIOUS |
| T+0.0s | Attacker launches Notepad++ | disk=MALICIOUS |
| T+0.1s | Notepad++ reads shortcuts.xml โ†’ `_userCommands[0]="calc.exe"` | mem=MALICIOUS |
| T+4.2s | Notepad++ fully loaded | mem=MALICIOUS |
| T+4.2s | Attacker restores LEGIT shortcuts.xml | disk=LEGIT / mem=MAL |
| T+4.7s | Attacker sends WM_COMMAND(21000) | |
| T+4.7s | HMAC check: reads LEGIT file from disk โ†’ PASSES | CHECK OK |
| T+4.7s | Exec: reads `_userCommands[0]` from MEMORY โ†’ calc.exe | **EXPLOIT** |
| T+4.7s | calc.exe spawned (PID 35932) | CONFIRMED |

---

## Impact

| | |
|---|---|
| **Type** | Arbitrary Command Execution |
| **Admin Required** | No |
| **UAC Prompt** | No |
| **Detectability** | Low โ€” disk shows legitimate file throughout; payload is memory-only |
| **CVSS (est.)** | AV:L / AC:H / PR:L / UI:N / S:U / C:H / I:H / A:H |

---

## Proof of Concept

See [`poc_8962_toctou.ps1`](poc_8962_toctou.ps1).

**Requirements:**
- Notepad++ v8.9.6.2 portable x64
- `doLocalConf.xml` present in the Notepad++ directory (portable mode)
- `config.xml` with a valid HMAC for the legitimate `shortcuts.xml`

```
.\poc_8962_toctou.ps1 -NppPath "C:\path\to\notepad++.exe"
```

**Verified:** calc.exe spawned (PID 35932), disk shows legitimate `shortcuts.xml` throughout.

---

## Patch Analysis

The fix (commit [4f7563c](https://github.com/notepad-plus-plus/notepad-plus-plus/commit/4f7563c)) moves HMAC validation to startup, alongside the initial file parse. Check and Use now operate on the same object at the same time, eliminating the TOCTOU window.

| | v8.9.6.2 (vulnerable) | v8.9.6.4 (patched) |
|---|---|---|
| HMAC check | At trigger time, re-reads disk | At startup, same bytes as parse |
| Command source | Memory loaded at startup | Memory validated at startup |
| TOCTOU window | Startup โ†’ Trigger | Eliminated |

---

## References

- [GHSA-qm4c-qg8p-qfcr](https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-qm4c-qg8p-qfcr)
- [Patch commit 4f7563c](https://github.com/notepad-plus-plus/notepad-plus-plus/commit/4f7563c)
- [Notepad++ v8.9.6.4 release](https://github.com/notepad-plus-plus/notepad-plus-plus/releases/tag/v8.9.6.4)