← all walkthroughs

Reddish

Linux· Insane· Privilege Escalation
owned
2026-07-12
time to own
37m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The attacker found a Node-RED administrative interface on TCP 1880 with no authentication whatsoever.

Querying the Node-RED REST API exposed an active flow ID, and a follow-up POST deployed a malicious flow containing an exec node wired to an HTTP endpoint, granting immediate root-level command execution inside the Node-RED Docker container.

From that foothold, TCP probes across the Docker bridge subnets revealed two additional internal-only peers: an unauthenticated Redis 4.0.9 instance and an Apache/PHP container that shared a data volume with Redis.

Redis's CONFIG SET and SAVE primitives were used over a raw TCP tunnel to write a PHP webshell directly into the shared web-container document root, yielding a second foothold as www-data.

A root-owned backup cron running rsync over a www-data-writable staging directory was then identified; staging filenames that began with '-e sh' caused rsync's wildcard expansion to treat those filenames as command-line options, executing an attacker-controlled script as root inside the web container.

Finally, an rsync daemon module on the backup host exposed the underlying host's filesystem — including /etc/cron.d — to the container network with write access.

Pushing a malicious root cron entry via that module caused the host to copy root.txt to a web-readable path, completing full host compromise.

Attack path — how the box was taken

Port scan revealed a single exposed service: Node-RED on TCP 1880, then Discovered unauthenticated Node-RED Admin API and retrieved the active flow ID, then Deployed a malicious Node-RED flow for persistent root-in-container command execution, then Enumerated internal Docker networks and discovered Redis and Apache/PHP peers, then Abused unauthenticated Redis to write a PHP webshell into the web container's document root, then Exploited rsync wildcard option injection in a root backup cron to execute code as root, then Wrote a root cron entry to the host via an exposed rsync daemon module, capturing root.txt.

1EnumerationNetwork service enumeration (T1046)
Port scan revealed a single exposed service: Node-RED on TCP 1880
A full TCP port scan of <retired-instance-ip> returned exactly one open port: 1880/tcp, running a Node.js/Express application identified as Node-RED. No SSH, HTTP on 80/443, or any other service was reachable from the internet, focusing the entire attack surface on the Node-RED admin interface.
Exact commands 2
Full TCP port scan with service and version detection.
nmap -sV -sC -p- --min-rate 5000 -oA reddish_full $TARGET
Confirm Node-RED responds on port 1880.
curl -sS http://$TARGET:1880/
2EnumerationUnauthenticated API enumeration / information disclosure (T1592)
Discovered unauthenticated Node-RED Admin API and retrieved the active flow ID
Node-RED's Admin HTTP API was exposed with no credentials required. A POST to the root endpoint and a GET to /flows both returned flow data immediately, yielding the active flow ID [REDACTED: sensitive value]. This ID gates all further API calls to deploy or modify flows.
GET /flows returned flow list with id [REDACTED: sensitive value]; no HTTP 401 or redirect to a login page
Exact commands 2
Probe the admin API root; a 200 response with no auth challenge confirms the issue.
curl -sS -X POST http://$TARGET:1880/ -H 'Content-Type: application/json' -d '{}'
Retrieve all deployed flows and note the flow id field.
curl -sS http://$TARGET:1880/flows
3ExploitationNode-RED unauthenticated Admin API remote code execution
Deployed a malicious Node-RED flow for persistent root-in-container command execution
Using the unauthenticated Admin API, a replacement flow was POSTed containing an 'http in' node wired through a function node to an exec node, exposing a persistent command endpoint at /api/[REDACTED: sensitive value]/cmd. A base64-encoded OS command passed as query parameter 'b' was decoded and executed as the Node-RED process owner, which proved to be uid=0 (root) inside the nodered Docker container.
id; hostname → uid=0(root) gid=0(root) groups=0(root); hostname: nodered
4Lateral MovementInternal network discovery from container foothold (T1018, T1046)
Enumerated internal Docker networks and discovered Redis and Apache/PHP peers
From the root shell inside the nodered container, network interface and ARP table inspection revealed two Docker bridge networks: <retired-internal-ip>/24 (the Node-RED container) and <retired-internal-ip>/24. TCP probes via bash /dev/tcp confirmed two peers unreachable from the internet: <retired-internal-ip> with port 6379 open (Redis 4.0.9) and <retired-internal-ip> with port 80 open (Apache 2.4/PHP).
ip a showed eth0 (172.18.0.x) and eth1 (172.19.0.x); /dev/tcp probes confirmed <retired-internal-ip>:6379 and <retired-internal-ip>:80 open
5Lateral MovementUnauthenticated Redis CONFIG SET / SAVE arbitrary file write (T1505.003)
Abused unauthenticated Redis to write a PHP webshell into the web container's document root
Redis 4.0.9 at <retired-internal-ip>:6379 required no password. Redis's CONFIG SET commands allow changing the RDB save directory and filename at runtime. Because the Redis container and the Apache/PHP container shared a data volume mounted at /var/www/html/[REDACTED: sensitive value]/, redirecting the RDB save path there and saving a key whose value was a PHP webshell caused Redis's dump file to contain executable PHP. Requesting the file via Apache returned command execution as www-data.
GET http://<retired-internal-ip>/[REDACTED: sensitive value]/shell.php?x=id → uid=33(www-data)
6Privilege EscalationRsync wildcard argument injection via root cron (T1053.003, T1574)
Exploited rsync wildcard option injection in a root backup cron to execute code as root
From the www-data foothold on the Apache/PHP container, cron inspection revealed a root-owned rsync backup job that read all files from the www-data-writable staging directory /var/www/html/[REDACTED: sensitive value]/ using a shell glob (*). Because rsync expands wildcards before parsing arguments, creating a file literally named '-e sh proof.rdb' alongside an executable script 'proof.rdb' caused rsync — running as root — to interpret the filename as options: '-e sh' (use sh as the remote-shell executor) applied to 'proof.rdb', executing that script as root. Proof of root execution was confirmed by finding /var/tmp/rsync_wildcard_proof written after one cron interval.
/var/tmp/rsync_wildcard_proof present and owned by root after ~3-minute cron interval; contained uid=0(root) output
7Privilege EscalationRsync daemon module unauthenticated write → host filesystem arbitrary file write (T1105, T1222)
Wrote a root cron entry to the host via an exposed rsync daemon module, capturing root.txt
The same backup infrastructure exposed an rsync daemon module named 'backup' reachable at <retired-internal-ip> from the web container. This module mapped the host's filesystem including /etc/cron.d, /root/, and /backup/. With unauthenticated write access, a malicious cron entry was pushed to the host's /etc/cron.d/ via rsync. When the host's cron daemon processed it, root copied /root/root.txt into a www-data-readable path inside the web container, from which the flag was retrieved.
rsync backup::src/etc/hostname confirmed host identity; /var/www/html/[REDACTED: sensitive value]/rootflag populated with root.txt content after one cron minute

Attack patterns used

The transferable techniques behind the compromise.

Unauthenticated API enumeration / information disclosureEnumerationT1592

What it is

Node-RED's Admin HTTP API was exposed with no credentials required. A POST to the root endpoint and a GET to /flows both returned flow data immediately, yielding the active flow ID [REDACTED: sensitive value]. This ID gates all further API calls to deploy or modify flows.

Why it works

Enable the built-in adminAuth block in settings.js (type: 'credentials', users: [{...hashed password...}]) or place a reverse proxy enforcing HTTP Basic or Bearer token authentication in front of port 1880. Never run the Node-RED runtime as root; use a dedicated low-privilege service account. In production, disable the editor UI entirely and expose only the runtime HTTP nodes the application requires. If internet exposure is not needed, bind Node-RED to 127.0.0.1 and access it only via a management VPN.

Unauthenticated Redis CONFIG SET / SAVE arbitrary file writeLateral MovementT1505.003

What it is

Redis 4.0.9 at <retired-internal-ip>:6379 required no password. Redis's CONFIG SET commands allow changing the RDB save directory and filename at runtime. Because the Redis container and the Apache/PHP container shared a data volume mounted at /var/www/html/[REDACTED: sensitive value]/, redirecting the RDB save path there and saving a key whose value was a PHP webshell caused Redis's dump file to contain executable PHP. Requesting the file via Apache returned command execution as www-data.

Why it works

Set a strong requirepass in redis.conf (Redis 6+ preferably with ACL users). Disable dangerous administrative commands that are not operationally needed by renaming them to empty strings in redis.conf: CONFIG, SAVE, DEBUG, SLAVEOF, REPLICAOF. Never mount a Redis data directory and an Apache document root to the same volume. Bind Redis to a dedicated backend Docker network that does not include the web container, and enforce network-level isolation in docker-compose or Docker firewall rules.

Rsync wildcard argument injection via root cronPrivilege EscalationT1053.003

What it is

From the www-data foothold on the Apache/PHP container, cron inspection revealed a root-owned rsync backup job that read all files from the www-data-writable staging directory /var/www/html/[REDACTED: sensitive value]/ using a shell glob (*). Because rsync expands wildcards before parsing arguments, creating a file literally named '-e sh proof.rdb' alongside an executable script 'proof.rdb' caused rsync — running as root — to interpret the filename as options: '-e sh' (use sh as the remote-shell executor) applied to 'proof.rdb', executing that script as root. Proof of root execution was confirmed by finding /var/tmp/rsync_wildcard_proof written after one cron interval.

Why it works

Replace the shell glob with an explicit, hardcoded source path or a manifest file that web-facing accounts cannot modify. Pass '--' before the source path in the rsync command to prevent option injection (rsync [options] -- /exact/source/ /dest/). Run the backup cron as a dedicated non-root backup service account and ensure the rsync binary is not SUID. Apply AppArmor or SELinux confinement to the rsync process so that even if arguments are injected, the resulting shell cannot write to sensitive paths.

Rsync daemon module unauthenticated write → host filesystem arbitrary file writePrivilege EscalationT1105

What it is

The same backup infrastructure exposed an rsync daemon module named 'backup' reachable at <retired-internal-ip> from the web container. This module mapped the host's filesystem including /etc/cron.d, /root/, and /backup/. With unauthenticated write access, a malicious cron entry was pushed to the host's /etc/cron.d/ via rsync. When the host's cron daemon processed it, root copied /root/root.txt into a www-data-readable path inside the web container, from which the flag was retrieved.

Why it works

Disable the rsync daemon entirely if it is not operationally required. If it must run: restrict each module with 'hosts allow' to specific, trusted management IPs only; set 'read-only mode' for all modules that do not require inbound writes; add explicit 'exclude = /etc/cron* /root /home' entries to every module definition; run rsyncd as a dedicated unprivileged uid (not root). Use Docker network segmentation (separate bridge networks, explicit deny rules) so that backup containers cannot reach production web or database containers.

Findings

Enable authentication on the Node-RED Admin HTTP APICritical
The Node-RED Admin HTTP API — including /flows, /red/<id>/flows, and all downstream exec endpoints — was reachable from the internet without any credentials, allowing anyone to enumerate deployed flows, replace them with malicious ones containing exec nodes, and run arbitrary operating-system commands as the Node-RED process owner (root in this case).
Require authentication on Redis and remove it from any shared volume with a web serverCritical
The Redis instance accepted connections from any container on the Docker network with no password, and its CONFIG command was unrestricted. This allowed the attacker to redirect the RDB save path to the Apache container's document root (shared via a Docker volume) and write an executable PHP webshell as the Redis data file, achieving code execution on the web container.
Eliminate rsync wildcard option injection in the root backup cronCritical
The root-owned backup cron job passed a shell glob (*) directly to rsync over a directory that the www-data account could write to. Because rsync expands wildcards before argument parsing, filenames beginning with '-' are treated as command-line flags, allowing any process that can create files in that directory to inject arbitrary rsync options — including '-e sh <script>' — executed as root.
Restrict rsync daemon module access and prevent container-to-host filesystem writesHigh
An rsync daemon module named 'backup' exposed the host's filesystem — including /etc/cron.d and /root/ — to the Docker container network with both read and write access and no authentication. This allowed an attacker with web-container access to push a malicious root cron entry directly onto the host, leading to full host compromise.

Exposed services

External surface