GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

PYSEC-2026-3682

Vulnerability from pysec - Published: 2026-08-19 11:56 - Updated: 2026-08-19 12:16
VLAI
Details

Summary

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.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.
  • 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='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 root open()/read of the attacker-named path. Disclosure degree varies by each plugin's 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).

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

  1. Entry: sudo /usr/lib64/nagios/plugins/deb-updates --test=/etc/shadow,,0
  2. Action: the nagios user invokes the whitelisted plugin as root with a --test CSV whose element[0] is the target path and retc=0.
  3. Guard: sudoers (Debian.sudoers:3) lists the binary only; --test is not gated to test builds.
  4. Bypass proof: CONTRIBUTING.md documents --test as centrally mapped to argparse.SUPPRESS — hidden from --help but still accepted on the command line; lib.args.csv splits /etc/shadow,,0 into ['/etc/shadow','','0'].
  5. Sink: lib.lftest.test(args.TEST) (deb-updates:143) reads element[0] as a file, as root.
  6. Guard: none — no path confinement on element[0].
  7. 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 early cu() abort.
  8. Store + query: each line → lib.db_sqlite.insert(conn, {'package': item}, ...); default QUERY='1'SELECT * FROM deb_updates WHERE 1.
  9. Guard: --only-critical or a restrictive --query would filter, but both default to permissive (ONLY_CRITICAL=False, QUERY='1').
  10. Bypass proof: attacker passes neither → all rows selected.
  11. Disclosure: msg += '\n* '.join([row['package'] for row in result])lib.base.oao(...) → stdout.
  12. Guard: none.
  13. Bypass proof: with len(result) > 0 the branch prints every row (every file line).
  14. Impact: full contents of any root-readable file disclosed to the nagios user → root. On the ~22 other --test plugins 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) when os.path.isfile(stdout)).
  • --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='1' confirmed.
  • No path-confinement guard exists on the --test path element in either the plugin or lib.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)

Impacted products
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)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…