Skip to content

Instantly share code, notes, and snippets.

@brenns10
Created August 16, 2023 04:42
Show Gist options
  • Save brenns10/94167174cf32d6009518e1e241960e27 to your computer and use it in GitHub Desktop.
Save brenns10/94167174cf32d6009518e1e241960e27 to your computer and use it in GitHub Desktop.
Open a file with root privileges via sudo, a helper, and unix sockets
"""
Open a file via sudo and unix socket
If you only need root privileges to open one file, then you can spawn a helper
program with sudo to open the file, then pass the file descriptor back to your
original process via a Unix socket. This module imports a drop-in replacement
for os.open (except for the dir_fd argument) based on this idea:
stephen at wrath in ~/repos/opener
$ ls -l secret.txt
-rw------- 1 root root 24 Aug 15 21:11 secret.txt
stephen at wrath in ~/repos/opener
$ python
Python 3.11.3 (main, Jun 5 2023, 09:32:32) [GCC 13.1.1 20230429] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os, opener
>>> fd = opener.open_via_sudo("secret.txt")
[sudo] password for stephen:
>>> os.fdopen(fd, "rt").read()
os.fdopen(fd, "rt").read()
'the very secret contents'
>>>
There's some funny business I have yet to unwind with the terminal settings.
Sudo seems to change a number of settings around, and I had to run "stty sane"
after invoking sudo. It doesn't completely resolve the issue: see the extra copy
of the line reading "os.fdopen(...)"? That shouldn't be there...
"""
import argparse
import array
import os
import socket
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Union
def recv_fd(sock: socket.socket) -> int:
"""Receive a file descripter on the socket"""
# Borrows heavily from the Python docs for sock.recvmsg()
fds = array.array("i")
msg, ancdata, flags, addr = sock.recvmsg(4096, socket.CMSG_SPACE(fds.itemsize))
if msg == b"success":
level, typ, data = ancdata[0]
assert level == socket.SOL_SOCKET
assert typ == socket.SCM_RIGHTS
data = data[:fds.itemsize]
fds.frombytes(data)
return fds[0]
raise Exception(msg.decode())
def send_fd(sock: socket.socket, fd: int) -> None:
"""Send a file descripter on the socket"""
# Borrows heavily from the Python docs for sock.sendmsg()
fds = array.array("i", [fd])
sock.sendmsg(
[b"success"],
[(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)],
)
def open_via_sudo(
path: Union[Path, str],
flags: int = os.O_RDONLY,
mode: int = 0o777,
) -> int:
"""Implements os.open() using sudo to get permissions"""
# Currently does not support dir_fd argument
path = str(path)
with tempfile.TemporaryDirectory() as td:
sockpath = Path(td) / "sock"
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.bind(str(sockpath))
try:
proc = subprocess.Popen(
[
"sudo",
sys.executable,
"-B",
__file__,
str(sockpath),
path,
str(flags),
str(mode),
],
)
# TODO: handle errors / timeout / wait
return recv_fd(sock)
finally:
# sudo tends to mess up the terminal, get it into a sane state
subprocess.run(["stty", "sane"])
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"socket",
type=Path,
help="path to unix domain socket",
)
parser.add_argument(
"file",
type=Path,
help="filename to open",
)
parser.add_argument(
"flags",
type=int,
help="numeric flags",
)
parser.add_argument(
"mode",
type=int,
help="numeric mode",
)
args = parser.parse_args()
fd = os.open(args.file, args.flags, args.mode)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.connect(str(args.socket))
send_fd(sock, fd)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment