June 16, 2026
Reading Files You Shouldn’t: The ZKTeco BioTime Path-Traversal Chain (CVE-2023–38950 → 38952)
A walkthrough of how a single unauthenticated path-traversal bug in a workforce-management platform unravels into full credential…
Nathan Budhu
3 min read
A walkthrough of how a single unauthenticated path-traversal bug in a workforce-management platform unravels into full credential disclosure — and how to shut it down.
Disclaimer & scope. This article describes publicly disclosed vulnerabilities (Claroty Team82, 2023) for defensive and educational purposes. All target identifiers, hostnames, customer codes, and any credentials encountered during testing have been redacted. Only test systems you are explicitly authorized to assess.
Why attendance systems are a juicy target
Biometric attendance and access-control platforms sit in an awkward spot on the network. They're often:
- Internet-exposed (so remote sites and devices can phone home),
- Installed once and forgotten (rarely patched),
- Running with high local privileges (Windows SYSTEM is common),
- Holding sensitive PII such as names, fingerprints, face templates, schedules.
ZKTeco's BioTime is one of the most widely deployed of these platforms. In 2023, Claroty's Team82 published a cluster of vulnerabilities against it. Chained together, they take an attacker from "no credentials" to "read any file on disk" to "code execution as SYSTEM." This writeup focuses on the unauthenticated path traversal and how it cascades.
The vulnerability chain at a glance
CVE-2023–38950 -> CVE-2023–38951 -> CVE-2023–38952
Step 0 — Fingerprinting the version
BioTime exposes an unauthenticated /license/ endpoint that returns build and deployment metadata. That's enough to confirm the exact version and whether it falls in the vulnerable range (8.5.5 and earlier).
curl -s https://target.example/license/
Step 1 — Unauthenticated path traversal (CVE-2023–38950)
The iclock API — the channel devices use to sync andaccepts a url parameter that is concatenated into a filesystem path without normalization. Classic ../ traversal applies, and the server base64-encodes the file it returns.
The canonical public proof-of-concept reads a harmless Windows file to confirm the bug:
curl -s "https://target.example/iclock/file?url=/../../../../../../../../windows/win.ini" | base64 -d
A successful response confirms arbitrary file read with directory traversal.
Step 2 — Pivot to the application config
The payoff isn't win.ini. It's the application's own settings file (attsite.ini), which lives in a predictable install path. Read it with the same traversal primitive and you get the full backend configuration.
Here's the catch that makes this a textbook finding: the config's secrets are encrypted, but with hardcoded keys shipped inside the product and documented in the public disclosure. Hardcoded, universal cryptographic keys provide roughly the security of base64, as anyone with the binary (or the writeup) has them.
There is a Proof-of-Concept script provided by https://github.com/omair2084/biotime-rce-8.5.5 that exploits this vulnerability automatically.
python3 biotime_enum.py http://target:port
These fields contain live PostgreSQL/SQL Server/Oracle credentials, a Redis password, and FTP logins. Furthermore, the path which contained database backup files was discovered and accessible.
Step 3 — Where it goes from here
Once an attacker holds backend credentials and confirmed file-read, the rest of the chain is mechanical:
- CVE-2023–38951 turns the traversal into an authenticated arbitrary write, dropping a webshell or scheduled task and yielding execution as SYSTEM.
- CVE-2023–38952 means the "authenticated" requirement is weak: session handling doesn't properly distinguish user roles, so a low-privilege session can reach admin functionality.
- Direct database access exposes employee PII and biometric data, and any reused credential becomes a lateral-movement lever into the rest of the environment.
Remediation
For operators running BioTime:
- Patch. Upgrade to a fixed release (BioTime 9.0.1 / Build 20240617.19506 or later). This is the only real fix.
- Get it off the internet. There is no good reason for the iclock/management interface to be publicly reachable. Put it behind a VPN or restrict to device subnets via firewall.
- Rotate everything. If the host was ever exposed, assume the config was read. Rotate all database, Redis, and FTP credentials — and anywhere those credentials were reused.
- Kill default creds. Replace any sa/ROOT, demo/demo, and similar defaults.
- Monitor. Alert on ../ sequences and base64-heavy responses from the iclock endpoints; watch for unexpected writes to web-accessible directories.
Closing thoughts
This chain is a tidy case study in how unglamorous bugs compound. There's no exotic memory corruption here — just a path that wasn't normalized, secrets "protected" by a shared key, and an access-control check that trusted the client. Each is a one-line oversight; together they're a full compromise of a system holding people's biometric data.
References