
Reproduces ZendTo unauthenticated ClamAV RCE and root privilege escalation in an authorized lab, with pinned Docker target, fail-closed verification, nonce-bound output, and cleanup.
This directory is a standalone, authorized-lab reproduction bundle for the no-account ZendTo-to-ClamAV code-execution chain and its separate default- profile Smarty/root-cron continuation. It includes:
The initial PoC executes a caller-selected command as the stock clamav
service account. The optional variant continues that no-account foothold to a
caller-selected command as root. Both default to id, reject the literal
/root/flag path, and return nonce-bound output over HTTPS.
The vulnerable group, directory, Smarty, and root-cron relationships are
ZendTo Debian package/installer defaults. Root reachability is nevertheless
environment-conditioned: this exact positive fixture uses an ext4-backed
/var/zendto volume, functional PHP CLI FFI/POSIX/exec, and an unconfined
clamd process. Enforcing AppArmor/SELinux or incompatible filesystem semantics
can block the continuation on another otherwise stock installation.
Use this only on the included disposable fixture or another system you are explicitly authorized to test.
The authoritative exact target is a local, intentionally unversioned artifact at:
image/zendto-installer-systemd-debian12.tar.zst
Git ignores Docker image archives. Before exact regression, place an authorized
local copy at that path; scripts/setup.sh requires its recorded SHA-256 and
image ID. source-recipe/README.md documents how the installer-derived image
was built and captured. A new build from live package repositories is useful
for topology testing but is not presumed byte-identical to the pinned target.
The setup and container entrypoint reject any mismatch from the following profile:
| Component | Exact tested value |
|---|---|
| Docker image ID | sha256:de6d2f06ca04943362a9bdec5026e808448953728f31bd926343a4ed2ca2ef7c |
| ZendTo | 6.15-8 |
| ClamAV/libclamav package | 1.4.3+dfsg-1~deb12u2 |
| libclamav | libclamav.so.12.0.3, SHA-256 55e3cd94…027c |
| libclamav Build ID | e6427ab62146ee3001fe463d12e797e9d25bf81a |
| glibc | 2.36-9+deb12u14, SHA-256 6b4a4535…421 |
| main database | v63, SHA-256 0b2182d2…365 |
| daily database | v28082, SHA-256 cddbcccf…906 |
| bytecode database | v339, SHA-256 6d4aa01f…ffb |
| clamd | MaxThreads 12, IdleTimeout 30, Restart=no |
| lifecycle | enabled clamav-daemon.socket, systemd PID 1 |
| web runtime | Apache 2.4.68, PHP 8.2.32 |
| MAC posture | unconfined privileged Docker fixture |
| root bridge | stock clamav in www-data; /var/zendto root:www-data 0775 |
| root consumer | stock root cleanup cron, hourly at minute 25 |
| Smarty cache | deterministic Smarty 4.5.4 compiled zendto.conf |
| exchange filesystem | ext4-backed named /var/zendto volume |
The complete values are in PROFILE.json.
This is stronger than pinning only the ClamAV package version. The allocator
construction also depends on glibc, CVD-driven parser traffic, worker settings,
and the exact target library. FreshClam is disabled in the captured image, and
startup fails if daily.cvd has drifted or a daily.cld has appeared.
The currently long-running research container in the parent workspace is not the clean fixture: later testing disabled its external-sender form and updated its daily database. Those changes are intentionally excluded here. This bundle uses the clean baseline against which native execution was demonstrated.
/sys/fs/cgroupzstd, Python 3 with venv, a C compiler, file, and GNU readelfThe target shares the host kernel and ASLR implementation. Kernel-dependent mapping behavior therefore remains a portability variable.
Only one systemd fixture should share the host cgroup namespace at a time. On the original research machine, stop the older fixture without deleting it:
docker stop zendto-installer-systemd-native
It can later be restored with docker start zendto-installer-systemd-native.
From this directory:
./scripts/verify-bundle.sh
./scripts/setup.sh
The setup script:
.venv with the pinned PoC Python dependencies;/var/zendto volume and starts systemd; andrenameat2(RENAME_EXCHANGE) operation performed as clamav.The default endpoints are loopback-only:
http://127.0.0.1:18084/
https://127.0.0.1:18447/
The certificate is self-signed. The PoC deliberately uses verify=False for
every HTTP request. Different loopback ports can be selected before setup:
export ZENDTO_HTTP_PORT=19084
export ZENDTO_HTTPS_PORT=19447
./scripts/setup.sh
Use the same environment variables for later Compose/helper commands.
Check the target at any time:
./scripts/verify-target.sh
docker compose ps
The clean installer baseline has:
allowExternalUploads = TRUE
confirmExternalEmails = TRUE
captcha = google
Its CAPTCHA and mail values are installer placeholders, so the untouched
archive cannot actually deliver the public verification email. Native
regression testing represented only that completed application step with an
equivalent external-sender AuthData row. The target code, packages, scanner,
permissions, and exploit path were not patched.
Create that one lab row and a mode-0600 token file with:
./scripts/mint-lab-auth.py [email protected]
The token value is suppressed and stored at:
.lab/upload-auth-token.txt
This local helper is not a claim that an external-upload-disabled deployment can be exploited remotely. If the public external-sender form is disabled, the PoC correctly stops before ClamAV even when a token file is supplied.
On a normally configured authorized deployment, omit
--external-auth-token-file: manually open the verification page, solve the
CAPTCHA, receive the target-generated message at an attacker-controlled
mailbox, and paste its URL/token at the hidden prompt.
This mode sends no CAPTCHA, email, capability, upload, login POST, or scanner
content. --skip-clamd-tcp-probe also suppresses the optional read-only native
TCP/3310 check:
mkdir -p -m 700 work
.venv/bin/python poc/zendto_6_15_8_unauth_clamav_rce.py \
https://127.0.0.1:18447 \
--enumerate-only \
--skip-clamd-tcp-probe \
--fingerprint-json work/fingerprint.json
Enumeration still produces ordinary web access logs and can initialize normal application caches. Its Debian/Ubuntu Apache-layout result does not attest the native package, Build ID, libc, CVDs, socket policy, or MAC state. The local Docker verification script supplies that evidence for this fixture.