GitHub Advisoryadvisory · PyPI

GHSA-vx5f-957p-qpvm Glances Central Browser Autodiscovery Leaks Reusable Credentials to Zeroconf-Spoofed Servers

HIGH Published 2026-03-16 Modified 2026-03-18 Official page ↗
Aliases
CVE-2026-32634
Ecosystem
PyPI
Packages
Glances
Severity
High
CVSS vector
CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Weaknesses
CWE-346, CWE-522
Reviewed
2026-03-16

Affected packages

• PyPI Glances: < 4.5.2 → fixed in 4.5.2

Description

Summary In Central Browser mode, Glances stores both the Zeroconf-advertised server name and the discovered IP address for dynamic servers, but later builds connection URIs from the untrusted advertised name instead of the discovered IP. When a dynamic server reports itself as protected, Glances also uses that same untrusted name as the lookup key for saved passwords and the global `[passwords] default` credential. An attacker on the same local network can advertise a fake Glances service over Zeroconf and cause the browser to automatically send a reusable Glances authentication secret to an attacker-controlled host. This affects the background polling path and the REST/WebUI click-through path in Central Browser mode. Details Dynamic server discovery keeps both a short `name` and a separate `ip`: ```python glances/servers_list_dynamic.py:56-61 def add_server(self, name, ip, port, protocol='rpc'): new_server = { 'key': name, 'name': name.split(':')[0], # Short name 'ip': ip, # IP address seen by the client 'port': port, ... 'type': 'DYNAMIC', } ``` The Zeroconf listener populates those fields directly from the service advertisement: ```python glances/servers_list_dynamic.py:112-121 new_server_ip = socket.inet_ntoa(address) new_server_port = info.port ... self.servers.add_server( srv_name, new_server_ip, new_server_port, protocol=new_server_protocol, ) ``` However, the Central Browser connection logic ignores `server['ip']` and instead uses the untrusted advertised `server['name']` for both password lookup and the destination URI: ```python glances/servers_list.py:119-130 def get_uri(self, server): if server['password'] != "": if server['status'] == 'PROTECTED': clear_password = self.password.get_password(server['name']) if clear_password is not None: server['password'] = self.password.get_hash(clear_password) uri = 'http://{}:{}@{}:{}'.format( server['username'], server['password'], server['name'], server['port'], ) else: uri = 'http://{}:{}'.format(server['name'], server['port']) return uri ``` That URI is used automatically by the background polling thread: ```python glances/servers_list.py:141-143 def __update_stats(self, server): server['uri'] = self.get_uri(server) ``` The password lookup itself falls back to the global default password when there is no exact match: ```python glances/password_list.py:45-58 def get_password(self, host=None): ... try: return self._password_dict[host] except (KeyError, TypeError): try: return self._password_dict['default'] except (KeyError, TypeError): return None ``` The sample configuration explicitly supports that `default` credential reuse: ```ini conf/glances.conf:656-663 [passwords] Define the passwords list related to the [serverlist] section ... default=defaultpassword ``` The secret sent over the network is not the cleartext password, but it is still a reusable Glances authentication credential. The client hashes the configured password and sends that hash over HTTP Basic authentication: ```python glances/password.py:72-74,94 For Glances client, get the password (confirm=False, clear=True): 2) the password is hashed with SHA-pbkdf2_hmac (only SHA string transit password = password_hash ``` ```python glances/client.py:55-57 if args.password != "": self.uri = f'http://{args.username}:{args.password}@{args.client}:{args.port}' ``` There is an inconsistent trust boundary in the interactive browser code as well: • `glances/client_browser.py:44` opens the REST/WebUI target via `webbrowser.open(self.servers_list.get_uri(server))`, which again trusts `server['name']` • `glances/client_browser.py:55` fetches saved passwords with `self.servers_list.password.get_password(server['name'])` • `glances/client_browser.py:76` uses `server['ip']` for the RPC client connection That asymmetry shows the intended safe destination (`ip`) is already available, but the credential-bearing URI and password binding still use the attacker-controlled Zeroconf name. Exploit Flow 1. The victim runs Glances in Central Browser mode with autodiscovery enabled and has a saved Glances password in `[passwords]` (especially `default=...`). 2. An attacker on the same multicast domain advertises a fake `_glances._tcp.local.` service with an attacker-controlled service name. 3. Glances stores the discovered server as `{'name': , 'ip': , ...}`. 4. The background stats refresh calls `get_uri(server)`. 5. Once the fake server causes the entry to become `PROTECTED`, `get_uri()` looks up a saved password by the attacker-controlled `name`, falls back to `default` if present, hashes it, and builds `http://username:hash@:`. 6. The attacker receives a reusable Glances authentication secret and can replay it against Glances servers using the same credential. PoC Step 1: Verified local logic proof The following command executes the real `glances/servers_list.py` `get_uri()` implementation (with unrelated imports stubbed out) and demonstrates that: • password lookup happens against `server['name']`, not `server['ip']` • the generated credential-bearing URI uses `server['name']`, not `server['ip']` ```bash cd D:\bugcrowd\glances\repo @' import importlib.util import sys import types from pathlib import Path pkg = types.ModuleType('glances') pkg.__apiversion__ = '4' sys.modules['glances'] = pkg client_mod = types.ModuleType('glances.client') class GlancesClientTransport: pass client_mod.GlancesClientTransport = GlancesClientTransport sys.modules['glances.client'] = client_mod globals_mod = types.ModuleType('glances.globals') globals_mod.json_loads = lambda x: x sys.modules['glances.globals'] = globals_mod logger_mod = types.ModuleType('glances.logger') logger_mod.logger = types.SimpleNamespace( debug=lambda *a, **k: None, warning=lambda *a, **k: None, info=lambda *a, **k: None, error=lambda *a, **k: None, ) sys.modules['glances.logger'] = logger_mod password_list_mod = types.ModuleType('glances.password_list') class GlancesPasswordList: pass password_list_mod.GlancesPasswordList = GlancesPasswordList sys.modules['glances.password_list'] = password_list_mod dynamic_mod = types.ModuleType('glances.servers_list_dynamic') class GlancesAutoDiscoverServer: pass dynamic_mod.GlancesAutoDiscoverServer = GlancesAutoDiscoverServer sys.modules['glances.servers_list_dynamic'] = dynamic_mod static_mod = types.ModuleType('glances.servers_list_static') class GlancesStaticServer: pass static_mod.GlancesStaticServer = GlancesStaticServer sys.modules['glances.servers_list_static'] = static_mod spec = importlib.util.spec_from_file_location('tested_servers_list', Path('glances/servers_list.py')) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) GlancesServersList = mod.GlancesServersList class FakePassword: def get_password(self, host=None): print(f'lookup:{host}') return 'defaultpassword' def get_hash(self, password): return f'hash({password})' sl = GlancesServersList.__new__(GlancesServersList) sl.password = FakePassword() server = { 'name': 'trusted-host', 'ip': '203.0.113.77', 'port': 61209, 'username': 'glances', 'password': None, 'status': 'PROTECTED', 'type': 'DYNAMIC', } print(sl.get_uri(server)) print(server) '@ | python - ``` Verified output: ```text lookup:trusted-host http://glances:hash(defaultpassword)@trusted-host:61209 {'name': 'trusted-host', 'ip': '203.0.113.77', 'port': 61209, 'username': 'glances', 'password': 'hash(defaultpassword)', 'status': 'PROTECTED', 'type': 'DYNAMIC'} ``` This confirms the code path binds credentials to the advertised `name` and ignores the discovered `ip`. Step 2: Live netw…

Related

Alias · CVE (1)

  • CVE-2026-32634 Glances is an open-source system cross-platform monitoring tool. vulnerability

Weakness · CWE (2)

  • CWE-346 Origin Validation Error weakness
  • CWE-522 Insufficiently Protected Credentials weakness

References