PHP Null Byte Poisoning

30 November -0001

PHP null byte injection, or null byte poisoning, is a pervasive problem. Although null byte injection has been a known problem back to the 1980s (http://artofhacking.com/files/phrack/phrack55/P55-07.TXT) it has made a resurgence with it's presence in PHP. PHP is implemented in C, and therefore suffers from many of the same hassles that C does, including the handling of string terminators (http://php.net/manual/en/security.filesystem.nullbytes.php).

Null Byte Handling in C

In order to fully understand the PHP null byte issue we have to examine how C handles strings. In C, strings are actually a construct and don't exist as a distinct primitive data type like they do in many more recent languages. To create a "string" in C you have to utilize an array of characters. In other words, to create a string in a language like PHP you can simply utilize:

$string = 'foo';

Whereas in C you have to use the following:

char string[4];

int main() {
  string[0] = 'f';
  string[1] = 'o';
  string[2] = 'o';
  string[3] = '\0';
}

Alternatively you can use other functions to populate the array, but the idea is the same. Because C handles strings as character arrays it needs a way to delimit the last character of a string. This is done using a null byte (\0). For instance, if you populate a string like so:

#include <string.h>
char string[10];
int main() {
  strcpy(string, "foo");
}

The actual contents of the array are:

string[0] = 'f';
string[1] = 'o';
string[2] = 'o';
string[3] = '\0';
string[4] = null;
string[5] = null;
string[6] = null;
string[7] = null;
string[8] = null;
string[9] = null;

What "null" means in this case is that we haven't explicitly written any data value to these memory locations. That doesn't exactly mean they don't contain any values though. The memory is at no time explicitly zeroed out. For instance, if we run the following program:

#include <stdio.h>
#include <string.h>
char string[10];
int main() {
  printf("%c\n", string[4]); // print nothing
  strcpy(string, "foobar");
  strcpy(string, "foo");
  printf("%c\n", string[4]);  // print 'a'
  printf("%s\n", string);  // print 'foo'
}

We get the output:

$ ./test 

a
foo

The reason this happened is because when we assigned values to the memory locations reserved for the character array they start out initialized as (null) but then once values are assigned to those memory locations they are never zeroed (or nulled) out. In other words, at the end of the program run our array space looking like this:

string[0] = 'f';
string[1] = 'o';
string[2] = 'o';
string[3] = '\0';
string[4] = 'a';
string[5] = 'r';
string[6] = '\0';
string[7] = null;
string[8] = null;
string[9] = null;

As you can see there is data after in the array that is not accessible to string functions because in C the string is read from the first character until a null byte is reached. This creates a problem because the extra 'a', 'r', and null byte at positions 4,5, and 6 are still in memory.

In practical PHP terms this can become an issue because some functions might handle an input string that contains a null byte by interpreting the string as though it terminated with the null byte. Several functions handle the null byte in different, and not always expected, ways. For instance, running the following PHP script:

<?php
$input = "foo\0bar";

if ($input == "foo") echo "$input is 'foo'\n";
else echo "$input is not 'foo'\n";

echo "including " . $input . ".php\n";

include ($input . ".php");
?>

Produces the following output:

$ php nullbyte.php 
foobar is not 'foo'
including foobar.php
PHP Warning:  include(foo): failed to open stream: No such file or directory in /var/www/html/nullbyte.php on line 9

As you can see the comparison operator does not have any problem with the null byte (\0) but the include function does. In addition to the include, include_once, require, and require_once functions several other commonly use functions suffer from problems handling null byte characters. In the above example you can see that the file extension added (".php") was ignored by PHP when executing the include() function.

Several functions utilized for input handling and validation suffer from this null bypte problem. The following is a partial list of functions that suffer from null byte handling problems and example usage:

copy()

This is a particularly nasty one because the null byte can be used to copy files that should not be copied. For instance:

$ cat nullbyte.php 
<?php
$input = "image.php\0.jpg";
copy($input, 'i.should.not.be');
?>
$ ls
image.php  nullbyte.php
$ php nullbyte.php 
$ ls
image.php  i.should.not.be  nullbyte.php

is_file()

This function can be bypassed by inserting a null byte to prematurely terminate the string for comparison. For example:

$ ls
image.php  nullbyte.php
$ cat nullbyte.php 
<?php
$input = "image.php\0.jpg";
echo is_file($input);
echo "\n";
?>
$ php nullbyte.php 
1

The function finds the file 'image.php', which does exist on the filesystem, rather than the specified 'image.php\0.jpg'. If the file JPEG file extension had been added with a script an attacker could evade this specification utilizing null byte injection.

file_get_contents()

The null byte poisoning of this function can reveal or include the contents of incorrect files:

$ ls
file.php  nullbyte.php
$ cat file.php 
I am the contents.
$ cat nullbyte.php 
<?php
$input = "file.php\0.jpg";
echo file_get_contents($input);
echo "\n";
?>
$ php nullbyte.php 
I am the contents.

file_put_contents()

Similarly, by way of example:

$ ls
file.php  nullbyte.php
$ cat file.php 
I am the contents.
$ cat nullbyte.php 
<?php
$input = "file.php\0.jpg";
echo file_put_contents($input, 'The is new content');
echo "\n";
?>
$ php nullbyte.php 
18
$ cat file.php 
The is new content

file()

Once again, the usage is rather straightforward:

$ ls
file.php  nullbyte.php
$ cat file.php 
The is new content
$ cat nullbyte.php 
<?php
$input = "file.php\0.jpg";
print_r(file($input));
echo "\n";
?>
$ php nullbyte.php 
Array
(
    [0] => The is new content
)

glob()

This is an interesting one because it allows a bypass that reads all files in the filepath specified:

$ ls
file.php  nullbyte.php
$ cat nullbyte.php 
<?php
$input = ".php\0.jpg";
foreach (glob("*" . $input) as $filename) {
    echo "$filename size " . filesize($filename) . "\n";
}
?>
$ php nullbyte.php 
file.php size 18
nullbyte.php size 135

is_dir()

$ ls -latdr '/etc/ssh\0foo'
ls: cannot access /etc/ssh\0foo: No such file or directory
$ cat nullbyte.php 
<?php
$input = "/etc/ssh\0foo";
echo is_dir($input) . "\n";
?>
$ php nullbyte.php 
1

Other Vulnerable Functions

file_exists(), fileatime(), filectime(), filegroup(), fileinode(), filemtime(), fileowner(), fileperms(), filesize(), filetype(), fopen(), is_executable(), is_link(), is_readable(), is_writable(), lchgrp(), lchown(), link(), linkinfo(), lstat(), mkdir(), pathinfo(), popen(), readfile(), realpath(), rename(), rmdir(), stat(), symlink(), touch(), unlink

tempnam() takes two input strings, both of which are vulnerable to null byte poisoning.

Conclusion

Null byte poisoning can be an extremely dangerous problem in PHP applications. Null byte poisoning is often used as a technique to exploit arbitrary local and remote file include vulnerabilities, information disclosure vulnerabilities, and arbitrary filesystem manipulation vulnerabilities. Many of the functions vulnerable to null byte poisoning in PHP operate on the filesystem and can be the source of severe problems if misused. In order to counteract the threat of null byte injection all user supplied input should be sanitized. Simply using the following snippit will strip null bytes out of input:

$input = str_replace(chr(0), '', $input);

This is quite irritating to have to do manually. It is rather baffling that some PHP functions strip out null bytes without explicit string manipulation while others do not. However, as long as this problem persists developers have to be extremely vigilant about input validation and sanity.