Fedora Account System
Red Hat Associate
Red Hat Customer
**Component:** apcupsd (sub-package: apcupsd-cgi) **Version:** 3.14.14 (current/last upstream release; ships in Fedora, OpenWrt, Debian, etc.) **Affected file:** `src/cgi/upsfetch.c`, function `getupsvar()` **Type:** CWE-121 stack-based buffer overflow; CWE-457/CWE-908 uninitialized memory disclosure ## Summary The apcupsd CGI programs (`upsstats.cgi`, `multimon.cgi`, `upsfstats.cgi`) fetch UPS status from an apcupsd Network Information Server (NIS, TCP/3551) and render it as HTML. The shared helper `getupsvar()` copies field values from the NIS reply into fixed-size caller buffers **without honoring the destination size**, and on one code path returns "success" while leaving the destination buffer **uninitialized**. A NIS endpoint that the CGI is allowed to query (any host listed in `hosts.conf`, or any host when `hosts.conf` is absent — `checkhost()` then defaults to allow) can therefore: 1. **Overflow** a caller stack buffer with attacker-controlled data of attacker-chosen length, and 2. **Disclose** uninitialized stack memory back in the HTTP response. Threat model: apcupsd's documented multi-host monitoring lists remote UPS servers in `hosts.conf`; an operator who controls/spoofs one of those hosts (or a malicious UPS appliance, or an on-path attacker) can drive both bugs against the web server running the CGI. No authentication is required to the CGI itself. Note: `getupsvar()` was hardened in the *daemon-side* copy of the same routine (`src/drivers/net/net.c`, which bounds the copy with `anslen` and builds a width-limited `sscanf` format), but the CGI copy in `src/cgi/upsfetch.c` was never fixed. ## Bug 1 — unbounded copy (stack buffer overflow) `src/cgi/upsfetch.c`, `getupsvar(const char *host, const char *request, char *answer, size_t anslen)`: ```c if (nfields == 1) /* single-field values */ sscanf (find, "%*s %*s %s", answer); /* no width -> unbounded write */ else { /* free-form values */ i = 0; find += 11; /* skip label */ while (*find != '\n') /* no bound; stops only at '\n', */ answer[i++] = *find++; /* not at statbuf's NUL */ answer[i] = '\0'; } ``` `anslen` is accepted but never applied to the data copy. Callers pass small fixed buffers, e.g. `src/cgi/upsstats.c` `char answer[256]` and `src/cgi/multimon.c` `char status[64]`, `char model[256]`. A long field value (or one with no trailing newline) overflows them. The free-form path additionally over-reads past `statbuf[4096]` because the loop only stops at `'\n'`, not at the buffer's NUL terminator. ## Bug 2 — uninitialized stack disclosure Same function, `nfields == 1` path. If the matched STATUS line has fewer than three whitespace-separated tokens, `sscanf("%*s %*s %s", answer)` performs **zero** assignments and leaves `answer` untouched, yet the function then executes `strcmp(answer,"N/A")` and `return 1` (success). The caller prints the uninitialized buffer into the HTML response, disclosing live stack contents (varies per request). ## Reproduction Environment: any system with `apcupsd-cgi` installed and the CGI reachable via the web server (e.g. `http://<target>/cgi-bin/apcupsd/upsstats.cgi`). "Attacker host" = a machine the CGI is permitted to query; it does NOT run apcupsd but a 12-line fake NIS server. 1. Authorize the attacker host on the target (models the multi-host monitoring setup): add `MONITOR <attacker-ip>` to `/etc/apcupsd/hosts.conf`. (If `hosts.conf` does not exist, this step is unnecessary — `checkhost()` defaults to allow.) 2. On the attacker host, run a minimal NIS server that speaks apcupsd's framing (2-byte big-endian length prefix per packet; reply to a `status` request). For Bug 1, answer with a valid `DATE` line followed by a value with no whitespace and no newline, split into <=999-byte packets, e.g.: ``` DATE : 2026-01-01 00:00:00 OUTPUTV : AAAAAAAA... (≈800+ bytes of 'A', no spaces, no '\n') ``` 3. Trigger the CGI: ``` curl "http://<target>/cgi-bin/apcupsd/upsstats.cgi?host=<attacker-ip>&img1=4" ``` `send_image()` reads `OUTPUTV` via `getupsvar()` into `answer[256]` and overflows it. 4. Observe the crash. The CGI process dies; the kernel logs it, e.g.: ``` upsstats.cgi[NNNN]: segfault at 7ffffffff000 ip <getupsvar copy loop> ... ``` (With a controlled-length value the overflow corrupts the stack canary instead, yielding a `__stack_chk_fail` abort — still a remote crash of the CGI handler.) 5. For Bug 2 (info leak), instead answer step 2 with a STATUS whose last keyword has no value (e.g. a `HITRANS :` line with nothing after the colon), then request `...upsstats.cgi?host=<attacker-ip>&img1=3`. The `value3=` field in the returned HTML reflects uninitialized stack bytes that change between requests (observed: a raw 0x8c byte, a stray 't', or empty), confirming the disclosure. ## Impact - Remote, unauthenticated (to the CGI) **denial of service**: each request can crash the CGI handler. Reliable via the free-form path, which faults regardless of stack-protector. - **Information disclosure** of uninitialized stack memory in the HTTP response (Bug 2). - Potential **remote code execution**: Bug 1 is a classic stack overflow. On hardened builds it is gated by the stack canary, but Bug 2 provides a same-process stack leak that could recover the canary; binaries are commonly non-PIE, which eases ROP once the canary is known. RCE was not demonstrated end-to-end, but the primitives are present. ## Suggested fix Port the already-fixed daemon-side logic from `src/drivers/net/net.c` into `src/cgi/upsfetch.c`: - Build a width-limited format: `snprintf(fmt, sizeof fmt, "%%*s %%*s %%%zus", anslen-1);` then `sscanf(find, fmt, answer);` - Bound the free-form loop: `while (*find != '\n' && i < (int)anslen - 1)` and stop on NUL. - Initialize `answer` (e.g. `answer[0] = '\0';`) and/or treat a zero-assignment `sscanf` result as "not found" so an absent value never returns stale/uninitialized data. ## References - Vulnerable: `src/cgi/upsfetch.c:209` `getupsvar()` (lines ~232 and ~236-238). - Already-fixed twin: `src/drivers/net/net.c:153` `NetUpsDriver::getupsvar()` (bounded copy, width-limited sscanf). - Reachability/gate: `src/cgi/cgilib.c` `checkhost()` (prefix match; default-allow when `hosts.conf` is absent); `src/cgi/upsstats.c` takes the `host` parameter from QUERY_STRING. Reproducible: Always Steps to Reproduce: Special setup: a target with `apcupsd-cgi` installed and the CGI reachable over HTTP (e.g. `http://<target>/cgi-bin/apcupsd/upsstats.cgi`), plus one "attacker host" the CGI is permitted to query. The attacker host does NOT run apcupsd; it runs a ~12-line fake NIS server that speaks apcupsd's wire framing (each message = 2-byte big-endian length prefix followed by that many bytes; the server replies to a `status` request). 1. On the target, authorize the attacker host (models apcupsd's documented multi-host monitoring): add the line `MONITOR <attacker-ip>` to `/etc/apcupsd/hosts.conf`. (If `/etc/apcupsd/hosts.conf` does not exist, skip this step — `checkhost()` then defaults to "allow" and any host value is accepted.) 2. On the attacker host, start the fake NIS server listening on TCP/3551. On a `status` request it returns, split into packets of <=999 payload bytes each: - a valid `DATE` line so the CGI's "is the host up?" probe passes, then - the attack value for Bug 1 (overflow): ``` DATE : 2026-01-01 00:00:00 OUTPUTV : AAAAAAAA… (>= ~800 bytes of 'A', no spaces, no newline) ``` 3. From any web client, request the stats page pointed at the attacker host: ``` curl "http://<target>/cgi-bin/apcupsd/upsstats.cgi?host=<attacker-ip>&img1=4" ``` The CGI connects to the attacker host, sends `status`, and `send_image()` copies the `OUTPUTV` value via `getupsvar()` into a 256-byte stack buffer with no bounds check. 4. (Bug 2 — info leak) Repeat with the server instead returning a STATUS whose last keyword has no value, e.g. a line `HITRANS :` followed by nothing, and request: ``` curl "http://<target>/cgi-bin/apcupsd/upsstats.cgi?host=<attacker-ip>&img1=3" ``` then inspect the `value3=` field in the returned HTML across several requests. Actual Results: - Step 3: the `upsstats.cgi` process crashes. The kernel logs a fault inside the copy loop of `getupsvar()`, e.g.: ``` upsstats.cgi[NNNN]: segfault at 7ffffffff000 ip 000000000040315e … ``` With a controlled-length value the overflow instead corrupts the stack canary, and the process aborts via `__stack_chk_fail` (observed on hardened builds as a general protection fault in libc — musl `a_crash()`). Either way the CGI request handler dies; the overflow is attacker-controlled in both length and content. - Step 4: the `value3=` field of the HTML response contains uninitialized stack memory that differs from request to request (observed: a raw `0x8c` byte, a stray `t`, or empty), confirming disclosure of process memory to the requester. `getupsvar()` accepts a destination-size argument (`anslen`) but never applies it to the data copy; it is used only for the "N/A"/"Not found" fallback strings. Expected Results: - The field value should be copied into the destination buffer **bounded by `anslen`** (truncated, never overflowing), and the over-read should stop at the end of the source buffer (its NUL), not only at a newline. - When a value is absent, `getupsvar()` should report "not found" and leave a defined (empty) result — never return success with an uninitialized buffer that the caller then prints. - A malicious or malformed NIS reply must not be able to crash the CGI or leak memory. Additional Information: - Root cause is isolated to the CGI copy of `getupsvar()` in `src/cgi/upsfetch.c` (vulnerable lines ~232 and ~236-238). The **daemon-side** copy of the same routine in `src/drivers/net/net.c:153` (`NetUpsDriver::getupsvar`) is already correct: it builds a width-limited `sscanf` format (`"%*s %*s %<anslen>s"`) and bounds the free-form loop with `i < anslen - 1`. Backporting that logic to `upsfetch.c` fixes both bugs. - Affected callers / buffers: `src/cgi/upsstats.c` (`answer[256]`), `src/cgi/multimon.c` (`status[64]`, `model[256]`, `upstemp[64]`, …), `src/cgi/upsfstats.c`. - Reachability gate: `src/cgi/cgilib.c:checkhost()` is a prefix `strncmp` against `hosts.conf` `MONITOR` entries and returns "allow" if `hosts.conf` is absent; `src/cgi/upsstats.c` reads the `host` parameter from `QUERY_STRING`. - Upstream apcupsd has had no release since 3.14.14 (2016), so the fix will likely need to be carried as a distribution patch. - apcupsd is packaged identically across distributions (Fedora/EPEL, Debian/Ubuntu, OpenWrt, …); all builds of the `apcupsd-cgi` sub-package are affected. - Verified on an OpenWrt 25.12.4 x86_64 build of the unmodified upstream source (`apcupsd-cgi` 3.14.14), and independently reproduced under AddressSanitizer on a native build, which flags the overflow precisely at `cgi/upsfetch.c:237`: ``` ERROR: AddressSanitizer: stack-buffer-overflow … WRITE of size 1 #0 getupsvar src/cgi/upsfetch.c:237 #1 main src/cgi/upsstats.c:260 ```
This bug appears to have been reported against 'rawhide' during the Fedora Linux 45 development cycle. Changing version to 45.