squirrelly@glykon:~/390r

            .-'` `}
    _./)   /       }
  .'o   \ |       }
  '.___.'`.\    {`
  /`\_/  , `.    }
  \=' .-'   _`\  {
  `'`;/      `,  }
      _\       ;  }
      /__`;-...'--'

[05/21/2024] 390R Final Project

by August Huber, Leon Ge, Karthik Shankar, and Vien Tran

Introduction

This project was aimed at implementing Common Vulnerabilities and Exposures (CVEs) of PHP programming language concentrating on the security risks and possible exploits of stack-based and heap-based vulnerabilities. For this project, we deployed the application of these vulnerabilities in simulated environments to understand and demonstrate how CVEs can be exploited in real-world situations.

PHP (PHP Hypertext Processor), a general purpose programming language, is one of the world’s most popular web development languages. It is an interpreted language whose interpreter is mostly written in the C programming language. Because of this, PHP is prone to vulnerabilities. For instance, there is often a lack of proper input sanitation within the source code. In 2006, at the height of PHP usage, there were over 3,000 vulnerabilities in the programming language.


Vulnerability Analysis

The vulnerability is described by the National Vulnerability Database as follows:

“In PHP version 8.0.* before 8.0.30, 8.1.* before 8.1.22, and 8.2.* before 8.2.8, when loading phar file, while reading PHAR directory entries, insufficient length checking may lead to a stack buffer overflow, leading potentially to memory corruption or RCE.”

To break this down further, a PHAR (PHP Archive) is PHP’s equivalent to Java’s JAR file – an archive file that allows simple transmission of external libraries. Like a JAR or ZIP file, a PHAR file may contain any number of files or directories. Each directory name is limited to 4096 characters, which is related to the same limit in Linux.

However, in the vulnerable PHP versions (PHP versions 8.0.29, 8.1.21, 8.2.7 and prior), there is a bug with respect to how directory names are handled when initially reading from PHAR files.

When reading directories from the PHAR file, there is an incorrect length check which causes a buffer overflow in the edge case that the length of directory name is equal to 4096. This overflow writes a null byte two bytes past the buffer’s end and leaks the byte in between the buffer and the null byte.

For instance, given the directory name, represented as bytes in hexadecimal, “AABB…EEFF” (where the ellipsis represents some 4092 bytes), the null-byte should occur at position 4097 in the internal buffer. However, we see a result more like this:


  AABB...EEFF XX \00
        

Where XX is some byte belonging to a separate item on the stack, and \00 is the directory buffer’s null terminator. Instead of being allocated to 4097 bytes (4096 characters + null terminator), the internal buffer for the directory is allocated to 4096 bytes. Then, due to a bad check, the null-byte is written two bytes outside of the buffer’s limit at byte 4096. This causes a leak of the byte at position 4097, and an overwrite of the null-byte to position 4098, which may interfere with other data.


Vulnerability Internals

The bug is located in the p_dir_read() function. Here is its source code for reference:


static ssize_t phar_dir_read(php_stream *stream, char *buf, size_t count) /* {{{ */
  {
      size_t to_read;
      HashTable *data = (HashTable *)stream->abstract;
      zend_string *str_key;
      zend_ulong unused;
  
      if (HASH_KEY_NON_EXISTENT == zend_hash_get_current_key(data, &str_key, &unused)) {
        return 0;
      }
  
      zend_hash_move_forward(data);
      to_read = MIN(ZSTR_LEN(str_key), count);
  
      if (to_read == 0 || count < ZSTR_LEN(str_key)) {
        return 0;
      }
  
      memset(buf, 0, sizeof(php_stream_dirent));
      memcpy(((php_stream_dirent *) buf)->d_name, ZSTR_VAL(str_key), to_read);
      ((php_stream_dirent *) buf)->d_name[to_read + 1] = '\0';
      return sizeof(php_stream_dirent);
  }
    

The p_dir_read() function loads the directory name as an object string str_key. The p_dir_read() function passes in a parameter count which equals 4096 (which is to say, sizeof(buffer)).

The number of bytes (“to_read”) it writes to the buffer equals len(str_key). There is a check to see if len(str_key) >= count as to prevent a buffer overflow, so if len(str_key) == count (ie. the length of the directory name is 4096), this check passes.

The 4096 (or fewer) bytes are written to the to_read variable. If the string was non-null terminated (terminated, for instance, by a size variable), this would not be a bugged implementation. However, directory entry strings are null terminated. Since the length of the buffer is exactly equal to the length of the directory string, there exists no place in the buffer (which is called d_name, for directory name) for a null terminator.

A less bugged solution would place the null terminator at d_name[to_read] (ie. d_name[4096] – remember that buffers are 0 indexed, so the proper final byte is d_name[4095]), which would still be an overwrite. However, the null terminator is actually written at d_name[to_read + 1] (ie. d_name[4097]), which causes both a one byte overwrite and the byte between the end of the buffer (d_name[4095]) and the null terminator (d_name[4097]).


Proof of Concept

In our malicious PHAR file, the length of the vulnerable directory entry is exactly 4096 bytes. Note that the directory entry itself only reads “AAAA…AAB.” We will find that our victim doesn’t think the same thing. This is the malicious PHAR file:


<?php
$phar = new Phar('myarchive.phar');
$phar->startBuffering();
$phar->addFromString(str_repeat('A', PHP_MAXPATHLEN - 1).'B', 'This is the content of the file.');
$phar->stopBuffering();
?>

This is the victim file. It simply reads:


  <?php
  $handle = opendir("phar://./myarchive.phar");
  $x = readdir($handle);
  closedir($handle);
  #$test = "hello";
  var_dump($x);
  #var_dump($x);
  ?>

When we dump the variable, we see some strange output. Note that this exploit is running on a patched version of PHP that we developed that moves the null terminator several bytes out in order to better illustrate the exploit:

AAAA...AAAAAAAAAAAAAAB @!2\xf3w

Remember how our malicious directory name ended with B? We now see a stack leak of, in our patched version, six bytes (including the space!). This is actually a leak in stack of the zend_cli output function, when printing. In the original vulnerability, this leak only would have been the space character – hexadecimal 0x20.


Findings & Conclusions

The overflow was non-exploitable for advanced exploits like canary leakage due to self-null-termination of the buffer; however, it could still leak potentially useful information if paired with other exploits or leaked information. If the leak was larger, this exploit would be useful for canary exploitation. The one byte overwrite of the buffer’s of the null-byte may be useful in advanced applications, for instance to overwrite data or an address.

Following this research, more vulnerability chaining could be done for a full exploit, and fuzzing techniques could be enhanced to detect more exploitable scenarios and robust defenses against types of vulnerabilities in PHP.

This vulnerability would have been prevented with proper bounds checking and secure memory management. This stresses the necessary role of secure code development in PHP, especially since it is such a widely used language. While we were unable to achieve arbitrary code execution (although it is thought that law enforcement used this vulnerability to take down the ransomware group LockBit), the leak nonetheless demonstrates the necessity of rigorous security testing, particularly through the usage tools like fuzzers.


go back..