# Variant Root Cause Analysis: CVE-2026-4480 — spoolss RPC StartDocPrinter Alternate Trigger

## Summary

This variant analysis identified a materially different entry point for CVE-2026-4480: the **spoolss RPC `StartDocPrinter(pDocName)` path** (MS-RPRN opnum 17 over `\pipe\spoolss`), as opposed to the original repro's SMB file open path on a printable share. Both paths converge at the same vulnerable sink (`generic_job_submit()` → `print_run_command()` → `smbrun_no_sanitize()` → `system()`), but the spoolss RPC path passes the client-controlled document name directly as the job name without SMB filename character restrictions. The variant was **confirmed as an alternate trigger on the vulnerable version (4.22.9)** — the `&&` injection in the document name caused `id` and `touch` to execute as `nobody`. However, testing on the **fixed version (4.22.10)** confirmed that the fix **covers this path** — the `replace_print_cmd_J()` function at the sink masks unsafe characters and wraps the jobname in single quotes regardless of entry point. This is an **alternate trigger, NOT a bypass**.

## Fix Coverage / Assumptions

### What invariant the original fix relies on

The fix (commit `b80131fcf582`) modifies `generic_job_submit()` in `source3/printing/print_generic.c` — the **sink** where all non-CUPS/non-iPrint print job submissions converge. The fix's invariant is: **regardless of how the print job was created, the `%J` substitution in the `print command` must use a masked, single-quoted jobname.**

### What code path(s) it explicitly covers

The fix pre-substitutes `%J` in the print command via `replace_print_cmd_J()` before calling `print_run_command()`. This covers ALL paths that reach `generic_job_submit()`:

1. **SMB file open on printable share** (original repro): `smbd/open.c` → `print_spool_open(fsp, filename)` → `print_job_start(docname)` → `print_job_end()` → `generic_job_submit()`
2. **spoolss RPC StartDocPrinter** (this variant): `\pipe\spoolss` → `_spoolss_StartDocPrinter()` → `print_job_start(info_1->document_name)` → `print_job_end()` → `generic_job_submit()`
3. **SMB2 Create on printable share**: `smbd/smb2_create.c` → `print_spool_open(state->result, in_name)` → same path
4. **SMB1 Open Print File** (limited): `smbd/smb1_reply.c` → `print_spool_open(fsp, NULL)` → same path (but uses hardcoded docname `"Remote Downlevel Document"`)

### What the fix does NOT cover (if applicable)

The fix **does cover** the spoolss RPC path. No gap was found. The fix is at the sink, not at any specific entry point, so it correctly handles all paths.

The only theoretical gap is if a completely separate code path could reach `smbrun_no_sanitize()` with client-controlled input WITHOUT going through `generic_job_submit()`. A thorough search of all `print_run_command()` callers showed that only `generic_job_submit()` passes `%J` (the job name). The other callers (`generic_job_delete`, `generic_job_pause`, `generic_job_resume`, `generic_queue_get`, `generic_queue_pause`, `generic_queue_resume`) pass only `%j` (numeric job ID), `%p` (printer name from config), or no substitutions. None of these are injectable.

## Variant / Alternate Trigger

### Description

The **spoolss RPC `StartDocPrinter` path** is a materially different entry point to CVE-2026-4480:

- **Original repro entry point**: SMB2/SMB1 file open on a printable share → the client-supplied **filename** becomes the document name → stored as `pjob->jobname` → substituted as `%J` in `print command`. The filename is subject to SMB filename character restrictions (no `/ \ : * ? " < > |`).

- **Variant entry point**: spoolss RPC `StartDocPrinter(pDocName)` (MS-RPRN opnum 17) over `\pipe\spoolss` → the client-supplied **document_name** field in `spoolss_DocumentInfo1` becomes the job name → substituted as `%J` in `print command`. The document name is a UTF-16 string in the RPC request and is **NOT subject to SMB filename character restrictions** — it can contain `;`, `$`, `` ` ``, `(`, `)`, `#`, and other characters forbidden in SMB filenames.

### Exact entry point

- **Protocol**: DCERPC over SMB, pipe `\pipe\spoolss`, UUID `12345678-1234-ABCD-EF00-0123456789AB` v1.0
- **RPC call**: `RpcStartDocPrinter` (opnum 17), level=1, `pDocName` field in `DOC_INFO_1`
- **Authentication**: Guest/anonymous (no credentials required with `map to guest = Bad User`)
- **Transport**: TCP 445

### Code path (files/functions)

1. `source3/rpc_server/spoolss/srv_spoolss_nt.c:5959` — `_spoolss_StartDocPrinter()` receives `info_1->document_name` from RPC
2. → `print_job_start(session_info, ..., info_1->document_name, ...)` at line 6035
3. → `print_job_end()` → `generic_job_submit()` in `source3/printing/print_generic.c:261`
4. → `replace_print_cmd_J()` (fixed version) or `talloc_string_sub("%J", jobname)` (vulnerable version)
5. → `print_run_command()` → `smbrun_no_sanitize()` → `execl("/bin/sh", "sh", "-c", cmd, NULL)` — **shell execution with unescaped jobname**

## Impact

- **Package/component affected:** Samba `smbd` printing subsystem — `source3/printing/print_generic.c`, function `generic_job_submit()` → `print_run_command()` → `smbrun_no_sanitize()` → `system()`
- **Affected versions:** All Samba versions prior to 4.22.10, 4.23.8, and 4.24.3. Verified on 4.22.9 (vulnerable) and 4.22.10 (fixed).
- **Risk level:** Critical (CVSS 3.1 base 10.0 — AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H). Unauthenticated, remote, guest print access via spoolss RPC.
- **Consequences:** Arbitrary OS command execution with the privileges of the Samba service (typically `nobody` or the configured `guest account`).

## Impact Parity

- **Disclosed/claimed maximum impact:** Remote code execution (unauthenticated, network-reachable) — same as parent CVE
- **Reproduced impact from this variant run:** Remote code execution confirmed via spoolss RPC path — the `id` command executed on the Samba server (`uid=65534(nobody)`) and a `spoolss_marker` file was created by the injected `touch` command. All via an unauthenticated guest DCERPC/SMB connection over TCP 445.
- **Parity:** `full` — same impact as the parent CVE, achieved through a different entry point
- **Not demonstrated:** A full reverse shell or privilege escalation to root was not demonstrated (the Samba guest account runs as `nobody`).

## Root Cause

### Why the same underlying bug can be reached

The root cause is that `generic_job_submit()` substitutes the client-controlled job name into the `print command` string via `%J`, and the result is passed to `smbrun_no_sanitize()` → `execl("/bin/sh", "sh", "-c", cmd, NULL)` — shell execution without proper escaping.

On the **vulnerable version**, `talloc_string_sub2()` with `remove_unsafe_characters=true` only masks `$ \` " ' ; % \r \n`, missing `& | < > ( ) #` etc. The `&&` operator survives sanitization and chains arbitrary commands.

The spoolss RPC `StartDocPrinter` path reaches the same `generic_job_submit()` sink with a client-controlled `pDocName` that has even fewer character restrictions than SMB filenames (no `/ \ : * ? " < > |` restriction), making it a more flexible injection vector.

### Fix commit

Fix commit: `b80131fcf582ecc8e8c1b97e6051bb324bb8bef8` (Samba master), backported to 4.22.10, 4.23.8, 4.24.3.

The fix adds `replace_print_cmd_J()` which:
- Masks ALL unsafe characters (`$ \` " ' ; % | & < > / \`) plus control characters to `_`
- Wraps the `%J` substitution in single quotes (preventing shell interpretation)
- Falls back to a fixed `__CVE-2026-4480_FallbackJobname__` string for mixed-quoting configurations
- Pre-substitutes `%J` before `print_run_command()`, removing the raw `%J` → jobname path

Because the fix is at the **sink** (`generic_job_submit()`), it covers BOTH the SMB file open entry point and the spoolss RPC entry point. The variant is an **alternate trigger**, not a bypass.

## Reproduction Steps

1. **Reference:** `bundle/vuln_variant/reproduction_steps.sh`
2. **What the script does:**
   - Starts vulnerable Samba 4.22.9 `smbd` on port 14445 with `printing = sysv` and `print command = (echo %J) > /tmp/samba_printlog_vuln_spoolss 2>&1`
   - Runs the spoolss RPC exploit (`bundle/vuln_variant/spoolss_exploit.py`) which:
     - Binds to `\pipe\spoolss` via DCERPC over SMB (guest/anonymous)
     - Calls `RpcOpenPrinter` to get a printer handle for `\\127.0.0.1\testprn`
     - Calls `RpcStartDocPrinter` with `pDocName = 'PWN&&id&&touch spoolss_marker&&END'`
     - Calls `RpcWritePrinter` to write print data
     - Calls `RpcEndDocPrinter` to trigger job submission
   - Checks the print log for `uid=` (RCE evidence) and the marker file
   - Stops the vulnerable smbd, starts fixed Samba 4.22.10 on port 14446
   - Repeats the same exploit against the fixed version (negative control)
   - Checks that the fixed version blocks the injection (no `uid=`, no marker)
3. **Expected evidence of reproduction:**
   - **Vulnerable (4.22.9):** Print log contains `uid=65534(nobody)` (output of injected `id`), `spoolss_marker` file created, smbd log shows `Running the command '(echo PWN&&id&&touch spoolss_marker&&END) > ...' gave 127`
   - **Fixed (4.22.10):** Print log contains `PWN__id__touch spoolss_marker__END` (masked, no command execution), no marker file, smbd log shows `Running the command '(echo 'PWN__id__touch spoolss_marker__END') > ...' gave 0`

## Evidence

### Vulnerable Samba 4.22.9 — RCE confirmed via spoolss RPC path

**Print log** (`bundle/artifacts/smb-vuln-spoolss/printlog.txt`):
```
PWN
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
sh: 1: END: not found
```
The `uid=65534(nobody)` line is the output of the injected `id` command — **arbitrary code execution proven via spoolss RPC path**.

**Marker file** (`bundle/artifacts/smb-vuln-spoolss/marker_check.txt`):
```
-rw-rw-rw- 1 nobody nogroup 0 Jul 12 22:00 /tmp/sambatest-vuln-spoolss/spool/spoolss_marker
```
Created by the injected `touch spoolss_marker` command running as `nobody`.

**smbd log** (`bundle/artifacts/smb-vuln-spoolss/smbd_command_log.txt`):
```
Running the command `(echo PWN&&id&&touch spoolss_marker&&END) > /tmp/samba_printlog_vuln_spoolss 2>&1' gave 127
```
The `&&` is intact in the executed command — no shell escaping applied. This confirms the spoolss RPC `pDocName` reaches the same vulnerable `%J` substitution.

### Fixed Samba 4.22.10 — injection blocked (negative control)

**Print log** (`bundle/artifacts/smb-fixed-spoolss/printlog.txt`):
```
PWN__id__touch spoolss_marker__END
```
The `&&` was masked to `__` and the jobname was wrapped in single quotes — **no command execution**.

**Marker file** (`bundle/artifacts/smb-fixed-spoolss/marker_check.txt`):
```
no marker (correct)
```

**smbd log** (`bundle/artifacts/smb-fixed-spoolss/smbd_command_log.txt`):
```
Running the command `(echo 'PWN__id__touch spoolss_marker__END') > /tmp/samba_printlog_fixed_spoolss 2>&1' gave 0
```
The jobname is single-quoted and masked — safe execution.

### Log locations
- `bundle/logs/vuln_variant/variant_repro.log` — full reproduction log
- `bundle/logs/vuln_variant/vulnerable_version.txt` — vulnerable version info
- `bundle/logs/vuln_variant/fixed_version.txt` — fixed version info
- `bundle/artifacts/smb-vuln-spoolss/` — vulnerable version evidence
- `bundle/artifacts/smb-fixed-spoolss/` — fixed version evidence

### Environment
- Samba built from source: `samba-4.22.9` (vulnerable, commit `ff3dd69`) and `samba-4.22.10` (fixed, commit `0abface`)
- Build: `--bundled-libraries=ALL --without-ad-dc --disable-python` (standalone file/print server)
- Client: impacket 0.13.1+ DCERPC/spoolss RPC client (custom `StartDocPrinter`/`WritePrinter`/`EndDocPrinter` NDR structures)
- OS: Ubuntu 26.04 LTS

## Recommendations / Next Steps

1. **The fix is complete for this variant.** The `replace_print_cmd_J()` function at the sink correctly handles the spoolss RPC path. No additional code changes are needed.

2. **Defense in depth:** Consider also sanitizing the document name at the entry point (`_spoolss_StartDocPrinter()` in `srv_spoolss_nt.c`) before it enters the print job pipeline. This would provide defense in depth even if the sink-level fix is accidentally regressed.

3. **Audit other `%` substitution variables:** While `%J` is the only client-controlled substitution variable that reaches the shell, administrators should be aware that `print command`, `lprm command`, `lppause command`, etc. all execute via `smbrun_no_sanitize()`. The fix correctly only modifies the `%J` path (the only injectable one), but future changes should ensure no new client-controlled substitutions are added.

4. **Disable guest printer access:** Require authentication for print shares and spoolss RPC access as a defense in depth measure.

## Additional Notes

- **Idempotency:** The reproduction script was run twice with identical results. Each run cleans all state directories and uses separate ports (14445 for vulnerable, 14446 for fixed) to avoid conflicts.

- **NDR encoding note:** Impacket's `rprn.py` module does not implement `RpcStartDocPrinter` (opnum 17), `RpcWritePrinter` (opnum 19), or `RpcEndDocPrinter` (opnum 23). These were implemented as custom NDR structures in `bundle/vuln_variant/spoolss_exploit.py`. A key finding: impacket's `LPWSTR` does not include the null terminator in the conformant array count, but Samba's NDR parser (`ndr_check_string_terminator`) requires it. Strings must be set with an explicit `\x00` suffix (e.g., `'doc_name\x00'`) for Samba compatibility.

- **EndDocPrinter error on vulnerable version:** The `EndDocPrinter` call returns `ERROR_PRINT_CANCELLED` on the vulnerable version because the print command exits with code 127 (the `END` command is not found). This is expected — the print job was already submitted and the command was already executed before EndDocPrinter returns. The RCE evidence (print log + marker) confirms the command executed regardless of the EndDocPrinter error.

- **SMB1 path limitation:** The SMB1 `SMBsplopen` command (`reply_printopen` in `smb1_reply.c`) passes `NULL` to `print_spool_open()`, resulting in a hardcoded docname `"Remote Downlevel Document"` with no client-controlled component. This path is NOT injectable via the filename.
