{"repro_id":"REPRO-2026-00229","version":6,"title":"CivetWeb PUT + SSI #exec RCE","repro_type":"security","status":"published","severity":"high","description":"CivetWeb combines an authenticated HTTP PUT upload feature with Server-Side Include (SSI) #exec processing. When an administrator enables put_delete_auth_file (Digest authentication for PUT/DELETE), any authenticated user can upload a file ending in .shtml via PUT. Because the default ssi_pattern includes **.shtml$ and **.shtm$, a subsequent GET request causes do_ssi_exec() to pass the command to popen(). The result is authenticated remote code execution. Reproduction: build CivetWeb from https://github.com/civetweb/civetweb at commit 588860e3, start with listening_ports, document_root, and put_delete_auth_file, create a digest password file, then PUT a file such as /pwn.shtml containing <!--#exec \"id; uname -a\" -->, and GET /pwn.shtml to see the command output. The issue is enabled by the interaction of two default/documented features; no out-of-band interaction is required.","root_cause":"# RCA Report: CivetWeb Authenticated PUT + SSI `#exec` RCE\n\n## Summary\n\nCivetWeb’s default `ssi_pattern` causes files ending in `.shtml` or `.shtm` to be processed as Server Side Includes (SSI). When the `put_delete_auth_file` option is enabled, CivetWeb accepts authenticated HTTP PUT uploads. Because files uploaded via PUT live in the configured `document_root`, a subsequent GET request for a `.shtml` upload is handled by the SSI engine. The SSI parser calls `do_ssi_exec()` on `<!--#exec \"...\"-->` tags, which passes the command string directly to `popen()`. An authenticated attacker can therefore upload a `.shtml` file containing an SSI `#exec` directive and execute arbitrary shell commands on the server by requesting that file.\n\n## Impact\n\n- **Package/component affected:** CivetWeb HTTP server (`civetweb` executable, `src/civetweb.c`).\n- **Affected versions:** The ticket names commit `588860e3`. That specific commit has a build-breaking syntax error in `get_request()` and does not compile. The immediately preceding parent commit (`588860e3^1`, also known as `3309a6c`) is the last working master revision and still contains the vulnerable `do_ssi_exec` / `handle_put_file` code path. The same behavior is present in `588860e3` once the unrelated syntax error is corrected.\n- **Risk level and consequences:** High. Any user with a valid digest credential for the `put_delete_auth_file` realm can upload and execute arbitrary shell commands as the CivetWeb process user, leading to full host compromise.\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Authenticated remote code execution via PUT upload of `.shtml` followed by GET.\n- **Reproduced impact from this run:** The reproduction script authenticated with digest credentials, PUT an `.shtml` payload containing `<!--#exec \"id; uname -a\" -->`, and a subsequent GET returned the output of `id` and `uname -a` in the HTTP response body. This proves the server executed attacker-controlled shell commands through the real HTTP API.\n- **Parity:** `full`\n- **Not demonstrated:** N/A — the claimed code-execution path was demonstrated end-to-end through the real HTTP API.\n\n## Root Cause\n\nThe root cause is the interaction of two default/documented features without sufficient isolation:\n\n1. **Authenticated PUT/DELETE support.** When `put_delete_auth_file` is configured, CivetWeb allows any user with a valid digest credential to write arbitrary files into the `document_root` (`handle_put_file()` in `src/civetweb.c`).\n2. **SSI `#exec` processing.** The default `ssi_pattern` is `**.shtml$|**.shtm$`. When a requested file matches this pattern, `send_ssi_file()` scans the file for `<!--#...-->` tags. If the tag starts with `exec` and `NO_POPEN` is not defined, it calls `do_ssi_exec(conn, buf + 9)`:\n\n```c\n#if !defined(NO_POPEN)\nstatic void\ndo_ssi_exec(struct mg_connection *conn, char *tag)\n{\n    char cmd[1024] = \"\";\n    struct mg_file file = STRUCT_FILE_INITIALIZER;\n\n    if (sscanf(tag, \" \\\"%1023[^\\\"]\\\"\", cmd) != 1) {\n        mg_cry_internal(conn, \"Bad SSI #exec: [%s]\", tag);\n    } else {\n        cmd[1023] = 0;\n        if ((file.access.fp = popen(cmd, \"r\")) == NULL) {\n            mg_cry_internal(conn,\n                            \"Cannot SSI #exec: [%s]: %s\",\n                            cmd,\n                            strerror(ERRNO));\n        } else {\n            send_file_data(conn, &file, 0, INT64_MAX, 0); /* send static file */\n            pclose(file.access.fp);\n        }\n    }\n}\n#endif /* !NO_POPEN */\n```\n\nThe command string is extracted from the SSI tag and passed unmodified to `popen()`, which runs it with the privileges of the CivetWeb process. There is no additional authorization check for `#exec` beyond the SSI file pattern match, and no restriction on which files created via PUT may be treated as SSI.\n\n- **Link to fix commit:** No upstream fix commit was identified in the repository at the time of this run. The current master (`588860e3`) still contains the `do_ssi_exec` code path and is only prevented from compiling by an unrelated syntax error in `get_request()`.\n\n## Reproduction Steps\n\n1. **Build the vulnerable server:** Run `bundle/repro/reproduction_steps.sh`. The script reads `bundle/project_cache_context.json`, clones or reuses the CivetWeb repository from the project cache, resolves the ticket-named commit `588860e3`, and uses its working parent `588860e3^1` (`3309a6c`) because the named commit does not compile. It builds two binaries: one default (vulnerable) and one with `-DNO_POPEN` (control).\n2. **Start the server:** The script creates a digest password file (`admin:mydomain.com:<md5>`) and starts CivetWeb with `listening_ports`, `document_root`, `put_delete_auth_file`, and `authentication_domain`.\n3. **Upload the payload:** The script performs an authenticated HTTP PUT to `/pwn.shtml` with the body `<!--#exec \"id; uname -a\" -->`.\n4. **Trigger execution:** The script sends an authenticated HTTP GET to `/pwn.shtml`. On the vulnerable build, the response body contains the output of the executed commands. On the `-DNO_POPEN` build, the response body is empty because the `#exec` directive is not implemented.\n5. **Expected evidence:**\n   - `bundle/artifacts/vulnerable-attempt1/get_body.txt` contains `uid=...` and `Linux ...`.\n   - `bundle/artifacts/fixed-attempt1/get_body.txt` does not contain either string.\n   - `bundle/logs/reproduction_steps.log` shows two vulnerable attempts returning command output and two fixed attempts returning no command output.\n\n## Evidence\n\n- **Log file:** `bundle/logs/reproduction_steps.log`\n- **Vulnerable GET response body:** `bundle/artifacts/vulnerable-attempt1/get_body.txt`:\n  ```\n  uid=1000(vscode) gid=1000(vscode) groups=1000(vscode),962(962)\n  Linux d778bdddc001 7.0.14-arch1-1 #1 SMP PREEMPT_DYNAMIC Sat, 27 Jun 2026 16:15:10 +0000 x86_64 GNU/Linux\n  ```\n- **Vulnerable GET response headers:** `bundle/artifacts/vulnerable-attempt1/get_headers.txt` shows `HTTP/1.1 200 OK` and `Content-Type: text/html`.\n- **Fixed GET response body:** `bundle/artifacts/fixed-attempt1/get_body.txt` is empty, confirming the `#exec` path is disabled when `NO_POPEN` is defined.\n- **HTTP headers:** `bundle/artifacts/vulnerable-attempt1/put_headers.txt` and `get_headers.txt` show `HTTP/1.1 200 OK` for both PUT and GET.\n- **Server logs:** `bundle/artifacts/vulnerable-attempt1/server.log` and `bundle/artifacts/fixed-attempt1/server.log` show the CivetWeb startup and shutdown.\n- **Environment:** Reproduced on Linux x86_64 with gcc, using CivetWeb built from source at commit `3309a6c` (`588860e3^1`).\n\n## Recommendations / Next Steps\n\n- **Short-term mitigation:** Build CivetWeb with `-DNO_POPEN` to disable SSI `#exec` entirely, or disable PUT/DELETE by not setting `put_delete_auth_file`. Alternatively, restrict `ssi_pattern` so that untrusted upload directories cannot match it.\n- **Proper fix:** Remove or gate the SSI `#exec` directive behind an explicit opt-in option (e.g., `ssi_exec_enabled yes`) that defaults to `no`. When enabled, restrict it to a dedicated, non-upload directory and/or require additional authorization. Alternatively, do not allow files uploaded via PUT to match `ssi_pattern` unless explicitly whitelisted.\n- **Upgrade guidance:** There is no known upstream fixed version at the time of this report. Users should apply the `-DNO_POPEN` build flag or disable PUT/DELETE until a patched release is available.\n- **Testing recommendations:** Add an integration test that uploads an `.shtml` file with `#exec` and verifies that no command is executed, and that the response body does not contain shell command output.\n\n## Additional Notes\n\n- **Idempotency:** The reproduction script was run twice consecutively from the same project-cache state and produced identical confirmation results in both runs.\n- **Commit discrepancy:** The ticket names commit `588860e3` as the vulnerable version. That commit has a build-breaking syntax error in `src/civetweb.c` (`get_request()`: `if (h_chunk != NULL) && ...` instead of `if ((h_chunk != NULL) && ...)` plus an undeclared `cl` variable). The reproduction therefore uses the immediately preceding working commit `588860e3^1` (`3309a6c`), which is the parent of `588860e3` and contains the same vulnerable SSI code path.\n- **Limitations:** The reproduction demonstrates authenticated RCE against a locally running CivetWeb instance. Actual deployments may differ in user privilege, authentication domain, or file-system layout, but the underlying SSI `#exec` mechanism is the same.\n","cve_id":"CVE-VINEXT-CIVETWEB-PUT-SSI-RCE","source_url":"https://github.com/civetweb/civetweb","package":{"name":"civetweb/civetweb","ecosystem":"github"},"reproduced_at":"2026-07-04T22:08:20.436351+00:00","duration_secs":2067.0,"tool_calls":374,"handoffs":6,"total_cost_usd":3.1713070800000023,"agent_costs":{"coding":0.62160311,"hypothesis_generator":0.03269134,"judge":0.602703,"repro":0.7078323500000002,"support":0.07204799999999999,"vuln_variant":1.1344292799999998},"cost_breakdown":{"coding":{"accounts/fireworks/models/kimi-k2p7-code":0.62160311},"hypothesis_generator":{"accounts/fireworks/models/kimi-k2p7-code":0.03269134},"judge":{"gpt-5.5":0.602703},"repro":{"accounts/fireworks/models/kimi-k2p7-code":0.7078323500000002},"support":{"accounts/fireworks/models/kimi-k2p7-code":0.07204799999999999},"vuln_variant":{"accounts/fireworks/models/kimi-k2p7-code":1.1344292799999998}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-04T22:08:52.579339+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":10167,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":8535,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":11321,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":9716,"category":"analysis"},{"path":"bundle/coding/proposed_fix.diff","filename":"proposed_fix.diff","size":1181,"category":"patch"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":788,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":1130,"category":"other"},{"path":"bundle/coding/artifacts/patched_get_headers.txt","filename":"patched_get_headers.txt","size":268,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":784,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":1164,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":4981,"category":"log"},{"path":"bundle/logs/vuln_variant.log","filename":"vuln_variant.log","size":8996,"category":"log"},{"path":"bundle/vuln_variant/artifacts/vulnerable-chunked/get_body.txt","filename":"get_body.txt","size":169,"category":"other"},{"path":"bundle/vuln_variant/artifacts/vulnerable-webdav/get_body.txt","filename":"get_body.txt","size":169,"category":"other"},{"path":"bundle/vuln_variant/artifacts/vulnerable-shtm/get_body.txt","filename":"get_body.txt","size":169,"category":"other"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2536,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":907,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":5831,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":1196,"category":"other"},{"path":"bundle/vuln_variant/artifacts/vulnerable-chunked/get_headers.txt","filename":"get_headers.txt","size":268,"category":"other"},{"path":"bundle/vuln_variant/artifacts/vulnerable-webdav/get_headers.txt","filename":"get_headers.txt","size":268,"category":"other"},{"path":"bundle/vuln_variant/artifacts/vulnerable-shtm/get_headers.txt","filename":"get_headers.txt","size":268,"category":"other"},{"path":"bundle/vuln_variant/artifacts/fixed-chunked/get_body.txt","filename":"get_body.txt","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/fixed-chunked/get_headers.txt","filename":"get_headers.txt","size":268,"category":"other"},{"path":"bundle/vuln_variant/artifacts/fixed-webdav/get_body.txt","filename":"get_body.txt","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/fixed-webdav/get_headers.txt","filename":"get_headers.txt","size":268,"category":"other"},{"path":"bundle/vuln_variant/artifacts/fixed-shtm/get_body.txt","filename":"get_body.txt","size":0,"category":"other"},{"path":"bundle/vuln_variant/artifacts/fixed-shtm/get_headers.txt","filename":"get_headers.txt","size":268,"category":"other"},{"path":"bundle/coding/verify_fix.sh","filename":"verify_fix.sh","size":4353,"category":"other"},{"path":"bundle/coding/summary_report.md","filename":"summary_report.md","size":4466,"category":"documentation"},{"path":"bundle/logs/verify_fix.log","filename":"verify_fix.log","size":5783,"category":"log"},{"path":"bundle/coding/artifacts/patched_get_body.txt","filename":"patched_get_body.txt","size":0,"category":"other"}]}