Bug 2493140

Summary: apcupsd-cgi: remote buffer overflow and uninitialized-stack disclosure in getupsvar()
Product: [Fedora] Fedora Reporter: Antonio Nappa <lm45>
Component: apcupsdAssignee: Gwyn Ciesla <gwync>
Status: NEW --- QA Contact: Fedora Extras Quality Assurance <extras-qa>
Severity: medium Docs Contact:
Priority: unspecified    
Version: 45CC: esandeen, germano.massullo, gwync
Target Milestone: ---   
Target Release: ---   
Hardware: x86_64   
OS: Linux   
Whiteboard:
Fixed In Version: Doc Type: ---
Doc Text:
Story Points: ---
Clone Of: Environment:
Last Closed: Type: ---
Regression: --- Mount Type: ---
Documentation: --- CRM:
Verified Versions: Category: ---
oVirt Team: --- RHEL 7.3 requirements from Atomic Host:
Cloudforms Team: --- Target Upstream Version:
Embargoed:

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.