Bug 2493140 - apcupsd-cgi: remote buffer overflow and uninitialized-stack disclosure in getupsvar()
Summary: apcupsd-cgi: remote buffer overflow and uninitialized-stack disclosure in get...
Keywords:
Status: NEW
Alias: None
Product: Fedora
Classification: Fedora
Component: apcupsd
Version: 45
Hardware: x86_64
OS: Linux
unspecified
medium
Target Milestone: ---
Assignee: Gwyn Ciesla
QA Contact: Fedora Extras Quality Assurance
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-06-25 19:07 UTC by Antonio Nappa
Modified: 2026-08-17 14:59 UTC (History)
3 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Type: ---
Embargoed:


Attachments (Terms of Use)

Description Antonio Nappa 2026-06-25 19:07:46 UTC
**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
  ```

Comment 1 Aoife Moloney 2026-08-17 14:59:35 UTC
This bug appears to have been reported against 'rawhide' during the Fedora Linux 45 development cycle.
Changing version to 45.


Note You need to log in before you can comment on or make changes to this bug.