From f69aa18becbc724e40bb0a4759bfe7c91e842a19 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 20 Jul 2026 00:37:22 +0200 Subject: [PATCH] Add HIGH PRIORITY TODO: command injection in dyndns update scripts nsupdate.php/nsupdate_ipv4.php/nsupdate_ipv6.php pass unsanitized $_GET input straight into exec()'d shell commands. Gated by Apache Basic Auth on the dyndns vhost, so not anonymously exploitable, but a real OS command injection reachable by anyone with valid (or leaked) dyndns credentials. Documents the finding and the fix approach; not yet applied. --- TODO.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index f383a4d..e95e6a2 100644 --- a/TODO.md +++ b/TODO.md @@ -1,7 +1,39 @@ # alpha.jayfield.org — TODO Outstanding hardening items (see `README.md` for the full service overview). -Nothing here is urgent; all are low-risk, no-downtime changes. +Everything below is low-risk/no-downtime **except the item marked +HIGH PRIORITY**. + +- [ ] **HIGH PRIORITY — OS command injection in the dyndns update scripts** — found 2026-07-20 + `/var/www/dyndns/nsupdate.php`, `nsupdate_ipv4.php`, and + `nsupdate_ipv6.php` (used by both `index.php` and `nic/update.php`, + the router/ddclient-facing endpoint) build a shell command string by + directly interpolating unsanitized `$_GET` input (`hostname`/`myip`/ + `ip`, which become `$host`/`$ip`/`$ipv4`/`$ipv6`) and pass it to + PHP's `exec()`, which runs it via `/bin/sh -c` — no + `escapeshellarg()`/`escapeshellcmd()` anywhere. A request like + `hostname=$(id>/tmp/pwned).dyndns.jayfield.org` would execute + arbitrary shell commands as `www-data` (the domain-suffix check + still passes after the `.`-split). `nic/update.php` has no + application-level auth of its own at all. + Mitigating factor: the whole `dyndns.jayfield.org` vhost sits behind + Apache Basic Auth, so this needs valid (or leaked/brute-forced) + dyndns credentials to reach — not exploitable by an anonymous + visitor. Compounding factors: PHP is `7.4.33`, EOL since Nov 2022, + `disable_functions` is empty (`exec` fully available); `index.php`'s + `pass == '_NO_PASS_'` check is a hardcoded magic string, not real + per-user auth, so any valid dyndns credential can update *any* of + the zone's 5 hostnames (`jens`, `kack`, `test`, `uschi`, `vpn`), not + just its own; and the vhost's access logging turned out to be a + no-op (shared `access.log` has 0 lines), so past exploitation can't + be fully ruled out from logs alone — though file mtimes, `www-data` + processes/crontab, and current zone contents all look unremarkable, + consistent with unexploited. + Fix: switch `exec()` to pass the nsupdate command list via + `proc_open`/`popen` writing to stdin (no shell interpolation of + user input at all), or at minimum wrap every interpolated value in + `escapeshellarg()`; add strict `filter_var`/regex validation on the + hostname label and IP before use. Not yet applied. - [x] **Public static site (`jayfield.org`/`www.jayfield.org`) was 401'ing every visitor** — done 2026-07-20 Found while auditing which services are reachable without credentials: