PYSEC-2026-3682
Vulnerability from pysec - Published: 2026-08-19 11:56 - Updated: 2026-08-19 12:16Summary
Every Linuxfabrik check plugin that supports the shared --test argument (routed through lib.lftest.test()) will, when --test is supplied, treat the first CSV element as a filesystem path and read its full contents as the plugin's simulated STDOUT — running as root when the plugin is invoked through the shipped nagios/icinga sudoers allowlist. --test is a live production argument (centrally mapped to argparse.SUPPRESS, so it is hidden from --help but still accepted on the command line), not a build-time-only gate. This yields an arbitrary root file-read primitive (full disclosure on deb-updates; filtered disclosure / existence-and-readability oracle on ~22 other whitelisted plugins), i.e. local privilege escalation from the nagios account to root.
Root Cause
lib.lftest.test(args)(lftest.pylines 659-664):stdout = args[0];if stdout and os.path.isfile(stdout): _, stdout = disk.read_file(stdout). Element[1] (stderr channel) is read the same way. There is no path confinement on the supplied path.check-plugins/deb-updates/deb-updates:--testis registered withtype=lib.args.csv(lines 78-82). When supplied, control flows tostdout, _, retc = lib.lftest.test(args.TEST)(line 143), bypassing the apt path (if args.TEST is None:at 121). Each returned line is stored as apackagerow and, under the default--query='1'(WHERE 1, matches all rows), every row is printed via'\n* '.join([row['package'] ...])→lib.base.oao(...).- The same
--test/lib.lftest.test()mechanism exists identically on ~22 whitelisted plugins (e.g.docker-info), each performing a rootopen()/read of the attacker-named path. Disclosure degree varies by each plugin's downstream parser: full (deb-updates), filtered (docker-infoechoes lines containingwarning:/error:;openvpn-client-listechoesCLIENT_LISTlines), or existence/readability oracle (JSON parsers).
Impact
An attacker controlling the low-privilege nagios/icinga account (the documented threat model for the shipped sudoers file — same precondition as CVE-2026-52817) obtains the full contents of any root-readable file via deb-updates (e.g. /etc/shadow, /root/.ssh/id_*, TLS keys, cloud credentials), plus a fleet-wide root file existence/readability oracle and filtered content leak via the other plugins → local privilege escalation to root.
Proof of Concept
Full disclosure (deb-updates):
sudo /usr/lib64/nagios/plugins/deb-updates --test=/etc/shadow,,0
Filtered disclosure / oracle (docker-info, target routed to the stderr channel that gets echoed):
sudo /usr/lib64/nagios/plugins/docker-info --test="dummy,/etc/shadow,0"
Attack Chain
- Entry:
sudo /usr/lib64/nagios/plugins/deb-updates --test=/etc/shadow,,0 - Action: the nagios user invokes the whitelisted plugin as root with a
--testCSV whose element[0] is the target path and retc=0. - Guard: sudoers (Debian.sudoers:3) lists the binary only;
--testis not gated to test builds. - Bypass proof: CONTRIBUTING.md documents
--testas centrally mapped toargparse.SUPPRESS— hidden from--helpbut still accepted on the command line;lib.args.csvsplits/etc/shadow,,0into['/etc/shadow','','0']. - Sink:
lib.lftest.test(args.TEST)(deb-updates:143) reads element[0] as a file, as root. - Guard: none — no path confinement on element[0].
- Bypass proof (from lib source):
lftest.py:661-664:stdout = args[0]; if stdout and os.path.isfile(stdout): _, stdout = disk.read_file(stdout)— element[0], if it exists on disk, is opened and its contents returned as stdout.retc=0(element[2]) so there is no earlycu()abort. - Store + query: each line →
lib.db_sqlite.insert(conn, {'package': item}, ...); defaultQUERY='1'→SELECT * FROM deb_updates WHERE 1. - Guard:
--only-criticalor a restrictive--querywould filter, but both default to permissive (ONLY_CRITICAL=False,QUERY='1'). - Bypass proof: attacker passes neither → all rows selected.
- Disclosure:
msg += '\n* '.join([row['package'] for row in result])→lib.base.oao(...)→ stdout. - Guard: none.
- Bypass proof: with
len(result) > 0the branch prints every row (every file line). - Impact: full contents of any root-readable file disclosed to the nagios user → root. On the ~22 other
--testplugins the same primitive yields a filtered leak / universal root file existence-and-readability oracle.
Bypass Evidence
lib.lftest.test()file-read behavior verified directly from linuxfabrik-lib source (lftest.py:659-664,disk.read_file(stdout)whenos.path.isfile(stdout)).--testregistration (type=lib.args.csv) and thestdout, _, retc = lib.lftest.test(args.TEST)call verified on the latest release tag v6.0.0 atcheck-plugins/deb-updates/deb-updates:143(GitHub contents API); defaultQUERY='1'confirmed.- No path-confinement guard exists on the
--testpath element in either the plugin orlib.lftest.
Affected Versions
<= 6.0.0 (latest release; --test/lib.lftest.test() flow present on tag v6.0.0). Not covered by any existing advisory (none reference --test or arbitrary file read).
Suggested Fix
Compile --test out of production builds (or gate it behind an explicit build/dev flag so it is not accepted at runtime), OR confine the --test path element(s) to a dedicated fixtures directory via realpath() + containment check before disk.read_file(). As defense-in-depth, constrain the sudoers entries to specific argument values so --test cannot be supplied to a root-run plugin.
Reported by zx (Jace)
| Name | purl | linuxfabrik-lib | pkg:pypi/linuxfabrik-lib |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "linuxfabrik-lib",
"purl": "pkg:pypi/linuxfabrik-lib"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "6.1.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"2.0.0.0",
"2.0.0.7",
"2.1.0.0",
"2.1.0.4",
"2.1.1.15",
"2.1.1.5",
"2.1.1.7",
"2.2.0",
"2.2.1",
"2.3.0",
"2.4.0",
"3.0.0",
"3.1.0",
"3.1.1",
"3.2.0",
"3.3.0",
"3.4.0",
"3.4.1",
"4.0.0",
"4.0.1",
"4.0.2",
"4.1.0",
"4.2.0",
"4.3.0",
"4.4.0",
"5.0.0",
"5.1.0",
"6.0.0"
]
}
],
"aliases": [
"CVE-2026-73974",
"GHSA-rh9c-rqvg-f7pr"
],
"details": "## Summary\nEvery Linuxfabrik check plugin that supports the shared `--test` argument (routed through `lib.lftest.test()`) will, when `--test` is supplied, treat the first CSV element as a filesystem path and read its full contents as the plugin\u0027s simulated STDOUT \u2014 running as root when the plugin is invoked through the shipped `nagios`/`icinga` sudoers allowlist. `--test` is a **live production argument** (centrally mapped to `argparse.SUPPRESS`, so it is hidden from `--help` but still accepted on the command line), not a build-time-only gate. This yields an arbitrary root file-read primitive (full disclosure on `deb-updates`; filtered disclosure / existence-and-readability oracle on ~22 other whitelisted plugins), i.e. local privilege escalation from the `nagios` account to root.\n\n## Root Cause\n- `lib.lftest.test(args)` (`lftest.py` lines 659-664): `stdout = args[0]`; `if stdout and os.path.isfile(stdout): _, stdout = disk.read_file(stdout)`. Element[1] (stderr channel) is read the same way. There is **no path confinement** on the supplied path.\n- `check-plugins/deb-updates/deb-updates`: `--test` is registered with `type=lib.args.csv` (lines 78-82). When supplied, control flows to `stdout, _, retc = lib.lftest.test(args.TEST)` (line 143), bypassing the apt path (`if args.TEST is None:` at 121). Each returned line is stored as a `package` row and, under the default `--query=\u00271\u0027` (`WHERE 1`, matches all rows), every row is printed via `\u0027\\n* \u0027.join([row[\u0027package\u0027] ...])` \u2192 `lib.base.oao(...)`.\n- The same `--test`/`lib.lftest.test()` mechanism exists identically on ~22 whitelisted plugins (e.g. `docker-info`), each performing a root `open()`/read of the attacker-named path. Disclosure degree varies by each plugin\u0027s downstream parser: full (`deb-updates`), filtered (`docker-info` echoes lines containing `warning:`/`error:`; `openvpn-client-list` echoes `CLIENT_LIST` lines), or existence/readability oracle (JSON parsers).\n\n## Impact\nAn attacker controlling the low-privilege `nagios`/`icinga` account (the documented threat model for the shipped sudoers file \u2014 same precondition as CVE-2026-52817) obtains the full contents of any root-readable file via `deb-updates` (e.g. `/etc/shadow`, `/root/.ssh/id_*`, TLS keys, cloud credentials), plus a fleet-wide root file existence/readability oracle and filtered content leak via the other plugins \u2192 local privilege escalation to root.\n\n## Proof of Concept\nFull disclosure (deb-updates):\n```\nsudo /usr/lib64/nagios/plugins/deb-updates --test=/etc/shadow,,0\n```\nFiltered disclosure / oracle (docker-info, target routed to the stderr channel that gets echoed):\n```\nsudo /usr/lib64/nagios/plugins/docker-info --test=\"dummy,/etc/shadow,0\"\n```\n\n## Attack Chain\n1. **Entry:** `sudo /usr/lib64/nagios/plugins/deb-updates --test=/etc/shadow,,0`\n - **Action:** the nagios user invokes the whitelisted plugin as root with a `--test` CSV whose element[0] is the target path and retc=0.\n - **Guard:** sudoers (Debian.sudoers:3) lists the binary only; `--test` is not gated to test builds.\n - **Bypass proof:** CONTRIBUTING.md documents `--test` as centrally mapped to `argparse.SUPPRESS` \u2014 hidden from `--help` but still accepted on the command line; `lib.args.csv` splits `/etc/shadow,,0` into `[\u0027/etc/shadow\u0027,\u0027\u0027,\u00270\u0027]`.\n2. **Sink:** `lib.lftest.test(args.TEST)` (deb-updates:143) reads element[0] as a file, as root.\n - **Guard:** none \u2014 no path confinement on element[0].\n - **Bypass proof (from lib source):** `lftest.py:661-664`: `stdout = args[0]; if stdout and os.path.isfile(stdout): _, stdout = disk.read_file(stdout)` \u2014 element[0], if it exists on disk, is opened and its contents returned as stdout. `retc=0` (element[2]) so there is no early `cu()` abort.\n3. **Store + query:** each line \u2192 `lib.db_sqlite.insert(conn, {\u0027package\u0027: item}, ...)`; default `QUERY=\u00271\u0027` \u2192 `SELECT * FROM deb_updates WHERE 1`.\n - **Guard:** `--only-critical` or a restrictive `--query` would filter, but both default to permissive (`ONLY_CRITICAL=False`, `QUERY=\u00271\u0027`).\n - **Bypass proof:** attacker passes neither \u2192 all rows selected.\n4. **Disclosure:** `msg += \u0027\\n* \u0027.join([row[\u0027package\u0027] for row in result])` \u2192 `lib.base.oao(...)` \u2192 stdout.\n - **Guard:** none.\n - **Bypass proof:** with `len(result) \u003e 0` the branch prints every row (every file line).\n5. **Impact:** full contents of any root-readable file disclosed to the nagios user \u2192 root. On the ~22 other `--test` plugins the same primitive yields a filtered leak / universal root file existence-and-readability oracle.\n\n## Bypass Evidence\n- `lib.lftest.test()` file-read behavior verified directly from linuxfabrik-lib source (`lftest.py:659-664`, `disk.read_file(stdout)` when `os.path.isfile(stdout)`).\n- `--test` registration (`type=lib.args.csv`) and the `stdout, _, retc = lib.lftest.test(args.TEST)` call verified on the latest release tag **v6.0.0** at `check-plugins/deb-updates/deb-updates:143` (GitHub contents API); default `QUERY=\u00271\u0027` confirmed.\n- No path-confinement guard exists on the `--test` path element in either the plugin or `lib.lftest`.\n\n## Affected Versions\n`\u003c= 6.0.0` (latest release; `--test`/`lib.lftest.test()` flow present on tag v6.0.0). Not covered by any existing advisory (none reference `--test` or arbitrary file read).\n\n## Suggested Fix\nCompile `--test` out of production builds (or gate it behind an explicit build/dev flag so it is not accepted at runtime), OR confine the `--test` path element(s) to a dedicated fixtures directory via `realpath()` + containment check before `disk.read_file()`. As defense-in-depth, constrain the sudoers entries to specific argument values so `--test` cannot be supplied to a root-run plugin.\n\n---\nReported by **zx (Jace)**",
"id": "PYSEC-2026-3682",
"modified": "2026-08-19T12:16:28.037522Z",
"published": "2026-08-19T11:56:28.745844Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Linuxfabrik/monitoring-plugins/security/advisories/GHSA-rh9c-rqvg-f7pr"
},
{
"type": "WEB",
"url": "https://github.com/Linuxfabrik/lib/commit/d665042c55fae83a295ebc0023e8b77f6c473a28"
},
{
"type": "WEB",
"url": "https://github.com/Linuxfabrik/lib/releases/tag/v6.1.0"
},
{
"type": "PACKAGE",
"url": "https://github.com/Linuxfabrik/monitoring-plugins"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/linuxfabrik-lib"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-rh9c-rqvg-f7pr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73974"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "linuxfabrik-lib: Arbitrary root file read via live --test argument (lib.lftest) across sudoers-whitelisted plugins (LPE)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.