Skip to content

Remote Code Execution (RCE) via CivetWeb Configuration Injection

High
PromoFaux published GHSA-8j7w-m3cr-6q6x Jul 6, 2026

Package

Pi-Hole FTL

Affected versions

6.6.2

Patched versions

>=6.7

Description

Affected component: Pi-hole FTL v6.6.2, PATCH /api/config (config.c:1100-1106, webserver.c:750-791) and POST /api/teleporter (teleporter.c:340-367).

Required privileges: Authenticated admin session.

Summary

The webserver.advancedOpts configuration setting accepts arbitrary CivetWeb options with no filtering on option names or values. An admin can use the lua_background_script option pointing to a file they control, causing CivetWeb to execute arbitrary Lua code on the next FTL restart. The restart happens automatically when advancedOpts is changed.

The file write is achieved through a second flaw: the Teleporter import writes the dhcp.leases ZIP entry to disk with zero content validation. Together, these two weaknesses form a RCE chain.

Details

Unfiltered CivetWeb option injection

webserver.advancedOpts is defined as a JSON string array:

// src/config/config.c:1100-1106
conf->webserver.advancedOpts.k = "webserver.advancedOpts";
conf->webserver.advancedOpts.t = CONF_JSON_STRING_ARRAY;
conf->webserver.advancedOpts.f = FLAG_RESTART_FTL;
conf->webserver.advancedOpts.d.json = cJSON_CreateArray();
conf->webserver.advancedOpts.c = validate_stub; // Only type-based checking

At server startup, each array element is split on the first = and appended to CivetWeb's configuration options without any allowlist:

// src/webserver/webserver.c:750-791
cJSON *option = NULL;
cJSON_ArrayForEach(option, config.webserver.advancedOpts.v.json)
{
    // ...
    const char *opt = cJSON_GetStringValue(option);
    char *equal_sign = strchr(opt, '=');
    // ...
    size_t key_len = (size_t)(equal_sign - opt);
    char *key = calloc(key_len + 1, sizeof(char));
    strncpy(key, opt, key_len);
    char *value = strdup(equal_sign + 1);

    conf_opts[idx * 2] = key;
    conf_opts[idx * 2 + 1] = value;
    idx++;
}

// ...
init.configuration_options = (const char**)conf_opts;
if((ctx = mg_start2(&init, &error)) == NULL || !get_server_ports())

Pi-hole compiles CivetWeb with USE_LUA enabled and NO_CGI (src/webserver/civetweb/CMakeLists.txt:28-32). This means lua_background_script, lua_preload_file, lua_script_pattern, and lua_server_page_pattern are all live options that an attacker can inject to execute code.

Arbitrary file write via Teleporter dhcp.leases import

The Teleporter import handler writes the dhcp.leases ZIP entry to /etc/pihole/dhcp.leases with no content validation:

// src/zip/teleporter.c:340-367
static const char *import_dhcp_leases(const void *ptr, size_t size, char * const hint)
{
	// We do not check if the file is empty here, as an empty dhcp.leases file is valid

	// When we reach this point, we know that the file is a valid dhcp.leases file.
	// We can now safely overwrite the current dhcp.leases file with the one from the ZIP archive
	// Nevertheless, we rotate the current dhcp.leases file to keep a backup of the previous version

	// Rotate current dhcp.leases file
	rotate_files(DHCPLEASESFILE, NULL);

	// Write new dhcp.leases file to disk
	FILE *fp = fopen(DHCPLEASESFILE, "w");
	if(fp == NULL)
	{
		strncpy(hint, strerror(errno), ERRBUF_SIZE);
		return "Failed to open dhcp.leases file for writing";
	}
	if(fwrite(ptr, 1, size, fp) != size)
	{
		strncpy(hint, strerror(errno), ERRBUF_SIZE);
		fclose(fp);
		return "Failed to write to dhcp.leases file";
	}
	fclose(fp);

	return NULL;
}

The comment at line 344 claims "we know that the file is a valid dhcp.leases file", but no validation precedes this function.

Attack chain

  1. POST /api/teleporter with a ZIP containing etc/pihole/dhcp.leases whose content is a Lua script (e.g., os.execute("id > /tmp/proof.txt")). The import handler writes this verbatim to /etc/pihole/dhcp.leases.

  2. PATCH /api/config with {"config":{"webserver":{"advancedOpts":["lua_background_script=/etc/pihole/dhcp.leases"]}}}. Because FLAG_RESTART_FTL is set on this config item, FTL restarts automatically.

  3. On restart, CivetWeb reads the lua_background_script option, opens /etc/pihole/dhcp.leases, and executes it as a Lua script in a background thread (civetweb.c:20738-20742). The attacker's code runs.

PoC

Prerequisites: a Pi-hole v6 instance with a known admin password, and Python 3 with the requests library.

Script: advancedopts_rce_poc.py

$ python3 advancedopts_rce_poc.py http://192.168.16.132 admin_password "id"
[+] Authenticated
[+] Teleporter: ['etc/pihole/dhcp.leases']
[+] advancedOpts set, FTL restarting...
[+] Done. Verify: cat /tmp/pihole_rce_proof.txt

Proof file on the target:

$ docker exec pihole cat /tmp/pihole_rce_proof.txt
RCE_OK
uid=1000(pihole) gid=1000(pihole) groups=1000(pihole)

Impact

An authenticated admin can execute arbitrary commands on the Pi-hole server. The attack persists across restarts as the injected advancedOpts value is written to pihole.toml, and the Lua payload in dhcp.leases survives until overwritten. Every FTL restart re-executes the payload.

Remediation

Add an allowlist of permitted CivetWeb options in the advancedOpts processing loop and/or add content validation to import_dhcp_leases() in teleporter.c.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-65963

Weaknesses

External Control of System or Configuration Setting

One or more system settings or configuration elements can be externally controlled by a user. Learn more on MITRE.

Improper Input Validation

The product receives input or data, but it does not validate or incorrectly validates that the input has the properties that are required to process the data safely and correctly. Learn more on MITRE.

Credits