Skip to content

CVE-VINEXT-CIVETWEB-PUT-SSI-RCE: Verified Repro With Script Download

CVE-VINEXT-CIVETWEB-PUT-SSI-RCE: CivetWeb PUT + SSI exec RCE

CVE-VINEXT-CIVETWEB-PUT-SSI-RCE is verified against civetweb/civetweb · github. Vulnerability class: RCE. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00229.

REPRO-2026-00229 civetweb/civetweb · github RCE Variant found Jul 4, 2026 CVE entry ↗ .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
34m 27s
Tool calls
374
Spend
$3.17
01 · Overview

What Is CVE-VINEXT-CIVETWEB-PUT-SSI-RCE?

It is a high-severity authenticated remote code execution in CivetWeb that combines its HTTP PUT upload feature with Server-Side Include (SSI) #exec processing. An authenticated user can upload a .shtml file that runs a shell command when fetched. Pruva reproduced it (reproduction REPRO-2026-00229).

02 · Severity & CVSS

CVE-VINEXT-CIVETWEB-PUT-SSI-RCE Severity

CVE-VINEXT-CIVETWEB-PUT-SSI-RCE is rated high severity.

HIGH threat level

High — serious impact or readily exploitable. Prioritize remediation.

How to Reproduce CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

$ pruva-verify REPRO-2026-00229
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00229/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

<!--#exec "id; uname -a" --> uploaded via authenticated HTTP PUT as /pwn.shtml

Attack chain
  1. HTTP PUT /pwn.shtml followed by HTTP GET /pwn.shtml on a CivetWeb instance with put_delete_auth_file and default ssi_pattern
Variants tested

Alternate triggers for CivetWeb authenticated PUT + SSI #exec RCE: chunked PUT, WebDAV MOVE, and .shtm extension

How the agent worked 893 events · 374 tool calls · 34 min
34 minDuration
374Tool calls
254Reasoning steps
893Events
1Dead-ends
Agent activity over 34 min
Support
39
Hypothesis
4
Repro
221
Judge
52
Variant
325
Coding
243
0:0034:27

Root Cause and Exploit Chain for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

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.

CivetWeb’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.

  • Package/component affected: CivetWeb HTTP server (civetweb executable, src/civetweb.c).
  • 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.
  • 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.

Impact Parity

  • Disclosed/claimed maximum impact: Authenticated remote code execution via PUT upload of .shtml followed by GET.
  • 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.
  • Parity: full
  • Not demonstrated: N/A — the claimed code-execution path was demonstrated end-to-end through the real HTTP API.

Root Cause

The root cause is the interaction of two default/documented features without sufficient isolation:

  1. 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).
  2. 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):
#if !defined(NO_POPEN)
static void
do_ssi_exec(struct mg_connection *conn, char *tag)
{
    char cmd[1024] = "";
    struct mg_file file = STRUCT_FILE_INITIALIZER;

    if (sscanf(tag, " \"%1023[^\"]\"", cmd) != 1) {
        mg_cry_internal(conn, "Bad SSI #exec: [%s]", tag);
    } else {
        cmd[1023] = 0;
        if ((file.access.fp = popen(cmd, "r")) == NULL) {
            mg_cry_internal(conn,
                            "Cannot SSI #exec: [%s]: %s",
                            cmd,
                            strerror(ERRNO));
        } else {
            send_file_data(conn, &file, 0, INT64_MAX, 0); /* send static file */
            pclose(file.access.fp);
        }
    }
}
#endif /* !NO_POPEN */

The 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.

  • 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().

Reproduction Steps

  1. 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).
  2. 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.
  3. Upload the payload: The script performs an authenticated HTTP PUT to /pwn.shtml with the body <!--#exec "id; uname -a" -->.
  4. 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.
  5. Expected evidence:
    • bundle/artifacts/vulnerable-attempt1/get_body.txt contains uid=... and Linux ....
    • bundle/artifacts/fixed-attempt1/get_body.txt does not contain either string.
    • bundle/logs/reproduction_steps.log shows two vulnerable attempts returning command output and two fixed attempts returning no command output.

Evidence

  • Log file: bundle/logs/reproduction_steps.log
  • Vulnerable GET response body: bundle/artifacts/vulnerable-attempt1/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
    
  • Vulnerable GET response headers: bundle/artifacts/vulnerable-attempt1/get_headers.txt shows HTTP/1.1 200 OK and Content-Type: text/html.
  • Fixed GET response body: bundle/artifacts/fixed-attempt1/get_body.txt is empty, confirming the #exec path is disabled when NO_POPEN is defined.
  • HTTP headers: bundle/artifacts/vulnerable-attempt1/put_headers.txt and get_headers.txt show HTTP/1.1 200 OK for both PUT and GET.
  • Server logs: bundle/artifacts/vulnerable-attempt1/server.log and bundle/artifacts/fixed-attempt1/server.log show the CivetWeb startup and shutdown.
  • Environment: Reproduced on Linux x86_64 with gcc, using CivetWeb built from source at commit 3309a6c (588860e3^1).

Recommendations / Next Steps

  • 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.
  • 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.
  • 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.
  • 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.

Additional Notes

  • Idempotency: The reproduction script was run twice consecutively from the same project-cache state and produced identical confirmation results in both runs.
  • 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.
  • 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.

Variant Analysis & Alternative Triggers for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

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.

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_requestput_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.

  • 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 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.

CVE-VINEXT-CIVETWEB-PUT-SSI-RCE Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:001:44
0:00
session startedaccounts/fireworks/models/kimi-k2p7-code · CVE-VINEXT-CIVETWEB · REPRO-20
0:05
0:07
web search
0:08
0:10
web search
0:12
0:14
0:17
0:18
web search
0:38
0:38
extract_facts
no facts extracted
0:39
0:39
0:39
supportrepro
1:20
1:22
1:22
1:22
1:24
1:24
1:24
1:31
1:32
1:37
1:37
1:39
1:39
$git --version && which curl && which jq && which make && which gcc && which cmake && which openssl
git version 2.55.0
/usr/bin/curl
/usr/bin/jq
/usr/bin/make
/usr/bin/gcc
/usr/bin/cmake
/usr/bin/openssl
1:44

Artifacts and Evidence for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

Scripts, logs, diffs, and output captured during the reproduction.

bundle/vuln_variant/source_identity.json0.8 KB
bundle/vuln_variant/root_cause_equivalence.json1.1 KB
bundle/coding/artifacts/patched_get_headers.txt0.3 KB
bundle/repro/reproduction_steps.sh9.9 KB
bundle/repro/rca_report.md8.3 KB
bundle/repro/validation_verdict.json0.8 KB
bundle/repro/runtime_manifest.json1.1 KB
bundle/logs/reproduction_steps.log4.9 KB
bundle/logs/vuln_variant.log8.8 KB
bundle/vuln_variant/artifacts/vulnerable-chunked/get_body.txt0.2 KB
bundle/vuln_variant/artifacts/vulnerable-webdav/get_body.txt0.2 KB
bundle/vuln_variant/artifacts/vulnerable-shtm/get_body.txt0.2 KB
bundle/vuln_variant/reproduction_steps.sh11.1 KB
bundle/vuln_variant/variant_manifest.json2.5 KB
bundle/vuln_variant/validation_verdict.json0.9 KB
bundle/vuln_variant/rca_report.md9.5 KB
bundle/vuln_variant/patch_analysis.md5.7 KB
bundle/vuln_variant/runtime_manifest.json1.2 KB
bundle/vuln_variant/artifacts/vulnerable-chunked/get_headers.txt0.3 KB
bundle/vuln_variant/artifacts/vulnerable-webdav/get_headers.txt0.3 KB
bundle/vuln_variant/artifacts/vulnerable-shtm/get_headers.txt0.3 KB
bundle/vuln_variant/artifacts/fixed-chunked/get_body.txt0.0 KB
bundle/vuln_variant/artifacts/fixed-chunked/get_headers.txt0.3 KB
bundle/vuln_variant/artifacts/fixed-webdav/get_body.txt0.0 KB
bundle/vuln_variant/artifacts/fixed-webdav/get_headers.txt0.3 KB
bundle/vuln_variant/artifacts/fixed-shtm/get_body.txt0.0 KB
bundle/vuln_variant/artifacts/fixed-shtm/get_headers.txt0.3 KB
bundle/coding/proposed_fix.diff1.2 KB
bundle/coding/verify_fix.sh4.3 KB
bundle/coding/summary_report.md4.4 KB
bundle/logs/verify_fix.log5.6 KB
bundle/coding/artifacts/patched_get_body.txt0.0 KB
08 · How to Fix

How to Fix CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

Coming soon

Step-by-step mitigation and hardening guidance for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

What configuration makes CivetWeb exploitable here?

The server must have put_delete_auth_file enabled (Digest authentication for PUT/DELETE) so authenticated PUT uploads are accepted, and the default SSI handling for .shtml files must be active — so an uploaded .shtml with an #exec directive is executed on retrieval.

How severe is the CivetWeb PUT + SSI #exec issue?

It is rated high severity: any authenticated user permitted to PUT files can achieve command execution on the server via a crafted .shtml upload and a follow-up GET.

How can I reproduce it?

Download the verified script from this page and run it in an isolated environment against a CivetWeb instance with put_delete_auth_file enabled. It PUTs a .shtml file containing an #exec directive, then GETs it, showing the injected command run by do_ssi_exec().
11 · References

References for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE

Authoritative sources for CVE-VINEXT-CIVETWEB-PUT-SSI-RCE — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.