# Variant RCA Report: CivetWeb PUT + SSI #exec RCE — Alternate Triggers

## Summary

The original reproduction confirmed that an authenticated CivetWeb user can upload a `.shtml` file containing `<!--#exec "command"-->` via HTTP PUT and then execute arbitrary shell commands by retrieving the file. This variant stage demonstrates that the same root cause (the SSI `#exec` sink calling `popen()`) is reachable through **multiple alternate triggers**: (1) `Transfer-Encoding: chunked` PUT framing, (2) WebDAV `MOVE` of an uploaded file to an SSI-matched extension, and (3) the `.shtm` extension matched by the default `ssi_pattern`. All three variants reproduced the same authenticated RCE on the vulnerable build. None of them bypassed the `NO_POPEN` control build, because the `NO_POPEN` flag removes the only command-execution sink. Therefore, this stage finds **confirmed alternate triggers but no true bypass**.

## Fix Coverage / Assumptions

The reproduction stage did not use an upstream fix; it used a **control build** of the same vulnerable commit (`3309a6c`) compiled with `-DNO_POPEN`. That flag removes `do_ssi_exec()` and the `#exec` dispatch in `send_ssi_file()` in `src/civetweb.c`.

The control fix relies on this invariant:
> The only way an attacker-controlled file can cause arbitrary command execution is through the SSI `#exec` directive, which calls `popen()`. If `popen()` is removed at compile time, the RCE cannot occur.

What the control fix covers:
- All `.shtml` / `.shtm` files served by the SSI engine, regardless of how they were uploaded.
- All HTTP body framing styles for PUT (`Content-Length` and `Transfer-Encoding: chunked`).
- All WebDAV `MOVE`/`COPY` operations that rename files into SSI-matched extensions.

What the control fix does NOT cover (and what a real patch should address):
- The underlying trust boundary: authenticated users can still write arbitrary files that are later served by the same process.
- Other script-handling paths (CGI, Lua server pages, Duktape) that could execute uploaded content if the corresponding feature/interpreter is enabled.
- Path traversal in `SSI #include` (a separate LFI concern, not RCE).

There is no upstream fix at the time of this analysis. The repository HEAD is at the same vulnerable commit `3309a6c`, and the next commit `588860e3` ("Refactor request handling: don't allow chunked encoding and content length") has a syntax error in `get_request()` and does not compile.

## Variant / Alternate Trigger

Three distinct triggers were tested against both the vulnerable and the `NO_POPEN` control builds.

### Variant 1 — Chunked PUT framing
- **Entry point:** `PUT /pwn.shtml` with `Transfer-Encoding: chunked` and a body of `<!--#exec "id; uname -a" -->`.
- **Code path:** `handle_request()` → `is_put_or_delete_request` → `put_file()` → `forward_body_data()` (handles chunked) → GET triggers `send_ssi_file()` → `do_ssi_exec()` → `popen()`.
- **Why it is a variant:** The original reproduction used a `Content-Length` PUT. The vulnerable `forward_body_data()` supports chunked bodies, so the same malicious file reaches the same sink via a different HTTP framing.

### Variant 2 — WebDAV MOVE
- **Entry point:** `PUT /pwn.txt` with the `#exec` payload, then `MOVE /pwn.txt` with `Destination: /pwn.shtml`. Requires `enable_webdav yes`.
- **Code path:** `PUT` goes through `put_file()`; `MOVE` is handled by `dav_move_file()` in `src/civetweb.c`. The target path matches the default `ssi_pattern`, so the subsequent GET calls `send_ssi_file()` → `do_ssi_exec()` → `popen()`.
- **Why it is a variant:** It uses a different HTTP method (`MOVE`) and a two-step workflow to place the payload in an SSI-matched file.

### Variant 3 — `.shtm` extension
- **Entry point:** `PUT /pwn.shtm` with the `#exec` payload, then `GET /pwn.shtm`.
- **Code path:** The default `ssi_pattern` is `**.shtml$|**.shtm$`, so `send_ssi_file()` processes the file and reaches the same `do_ssi_exec()` sink.
- **Why it is a variant:** The same sink is reached through a different extension that is enabled by default.

## Impact

- **Package/component affected:** CivetWeb HTTP server (`src/civetweb.c`, `civetweb` executable).
- **Affected versions:** The tested vulnerable revision is `3309a6c` (master at the time of analysis). The next commit `588860e3` does not compile. No upstream fix exists yet.
- **Risk level and consequences:** High. Any authenticated user who can write files (via `PUT` or WebDAV `MOVE`/`COPY`) can plant an SSI-matched file and execute arbitrary shell commands as the CivetWeb process user.

## Impact Parity

- **Disclosed/claimed maximum impact:** Authenticated remote code execution via PUT upload of `.shtml` followed by GET.
- **Reproduced impact from this variant run:** All three alternate triggers produced a `GET` response body containing the output of `id` and `uname -a` on the vulnerable build, proving command execution.
- **Parity:** `full` for the alternate triggers (same RCE impact as the original claim).
- **Not demonstrated:** Persistence, lateral movement, or privilege escalation. No true bypass of the `NO_POPEN` control was found.

## Root Cause

The root cause is unchanged from the original reproduction: the default `ssi_pattern` causes files ending in `.shtml` or `.shtm` to be processed by `send_ssi_file()`, which unconditionally dispatches `<!--#exec ...-->` directives to `do_ssi_exec()`. That function passes the attacker-controlled command string directly to `popen()`. Because CivetWeb's `put_file()` accepts any file content (including SSI directives) when `put_delete_auth_file` is configured, the same process later serves and executes that content.

The `NO_POPEN` control mitigates this by compiling out the entire `do_ssi_exec()` function, so the SSI engine silently drops `#exec` directives. This is a compile-time removal of the sink, not a runtime fix for the trust-boundary issue.

## Reproduction Steps

Run the stage script:

```bash
bash bundle/vuln_variant/reproduction_steps.sh
```

The script:
1. Resolves or builds the vulnerable `civetweb` binary (commit `3309a6c`).
2. Resolves or builds the `NO_POPEN` control binary from the same commit.
3. For each variant, starts a fresh CivetWeb instance on an isolated port, performs the authenticated upload, and then performs the GET.
4. Checks whether the GET response body contains the output of `id; uname -a`.
5. Repeats the same tests against the `NO_POPEN` binary.
6. Exits `1` (no bypass) because all variants work on the vulnerable build and are blocked by the control build.

Expected evidence on the vulnerable build:
- `vuln_variant/artifacts/vulnerable-chunked/get_body.txt` contains the command output.
- `vuln_variant/artifacts/vulnerable-webdav/get_body.txt` contains the command output.
- `vuln_variant/artifacts/vulnerable-shtm/get_body.txt` contains the command output.

Expected evidence on the fixed/control build:
- The corresponding `fixed-*/get_body.txt` files are empty (the `#exec` directive is ignored).

## Evidence

- **Log:** `bundle/logs/vuln_variant.log`
- **Runtime manifest:** `bundle/vuln_variant/runtime_manifest.json`
- **Variant 1 (chunked PUT) evidence:**
  - `bundle/vuln_variant/artifacts/vulnerable-chunked/get_body.txt`:
    ```
    uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),962(962)
    Linux d778bdddc001 7.0.14-arch1-1 #1 SMP PREEMPT_DYNAMIC Sat, 27 Jun 2026 16:15:10 +0000 x86_64 GNU/Linux
    ```
  - `bundle/vuln_variant/artifacts/fixed-chunked/get_body.txt` is empty.
- **Variant 2 (WebDAV MOVE) evidence:**
  - `bundle/vuln_variant/artifacts/vulnerable-webdav/get_body.txt` contains the same `id`/`uname -a` output.
  - `bundle/vuln_variant/artifacts/fixed-webdav/get_body.txt` is empty.
- **Variant 3 (`.shtm`) evidence:**
  - `bundle/vuln_variant/artifacts/vulnerable-shtm/get_body.txt` contains the same `id`/`uname -a` output.
  - `bundle/vuln_variant/artifacts/fixed-shtm/get_body.txt` is empty.

Environment details captured:
- Vulnerable source commit: `3309a6cac05335aa4371a0c3750b42fbe05d3cb4`
- Fixed/control build: same commit, compiled with `-DNO_POPEN`
- CivetWeb version string: `CivetWeb V1.17`

## Recommendations / Next Steps

To produce a complete fix for the underlying issue, not just the `NO_POPEN` control:

1. **Disable SSI `#exec` by default** or guard it behind a dedicated runtime option (e.g., `enable_ssi_exec`) that defaults to `no`. This preserves `#include` for static includes while removing the command-execution surface.
2. **Decouple upload destinations from script/SSI paths.** If a directory is writable via PUT, do not serve SSI or CGI from that directory unless explicitly enabled.
3. **Apply the same authorization to GET of SSI/CGI files as to PUT.** The current code authorizes PUT via `put_delete_auth_file`, but GET of the resulting `.shtml` relies on other auth mechanisms that may not be configured.
4. **Harden `do_ssi_include`** by applying `is_in_script_path` or similar checks to prevent path traversal via `file=` and `virtual=` (a separate LFI issue).

## Additional Notes

- **Idempotency:** The script was run twice in a row and produced the same results both times (exit code `1`, all variants reproduced on vulnerable, none on fixed).
- **Trust boundary:** The attack requires valid credentials for the `put_delete_auth_file` realm. The WebDAV variant additionally requires the administrator to set `enable_webdav yes`, which is disabled by default.
- **No bypass:** The `NO_POPEN` control is sufficient to stop the SSI `#exec` RCE. A true bypass would require an alternative command-execution sink reachable from an uploaded file, none of which are enabled in the default build.
