How to Prevent Shell Attack in Image File Upload System in PHP

The Command Injection Vulnerabilities are one of the most common types of vulnerabilities in our PHP Web Applications. The main purpose of the command injection attack or shell attacks is to inject and execute commands specified by the attacker by uploading malicious shells like C99 in our websites. The attacker mainly focused on insecure file upload system on our websites. In this tutorial, I will show you how to prevent malicious shell uploading like C99 in your image upload system.

Before I show you the most secure way of image uploading, I will show you the some of the most common mistakes we make in our image upload system.

Case 1: Extremely Vulnerable

<?php if( isset( $_POST[ 'Upload' ] ) ) { $target_path = "/uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { echo '<pre>Your image was not uploaded.</pre>'; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>"; } } ?>

As you can see the above system can accept any files so it is extremely vulnerable. Some of you may even think that who use this coding style. But in reality, many of us because of ignorance still make image upload system like above. Even I personally use it in my first Website 3 years back.

Case 2: Still Vulnerable

<?php if( isset( $_POST[ 'Upload' ] ) ) { $target_path = "/uploads/"; $target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] ); // File information $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; // Is it an image? if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) && ( $uploaded_size < 100000 ) ) { if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) { echo '<pre>Your image was not uploaded.</pre>'; } else { echo "<pre>{$target_path} succesfully uploaded!</pre>"; } } else { // Invalid file echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } } ?>

You may think that above code is secure because it checks whether uploaded file is the image or not and also the size of file uploaded. But in reality, it is also very vulnerable. Using tools like Burp Suite you can easily mimic our server to think that the file uploaded is an image file. I was able to successfully uploaded a PHP file using Burp Suite very easily. So above code is also vulnerable.

The Most Secure Way of Image Uploading

The complete code for your index.php file is given below.

<?php // Start the session session_start(); require 'functions.php'; if( isset( $_POST[ 'Upload' ] ) ) { // Check Anti-CSRF token if ($_POST['_token'] === $_SESSION['_token']) { // File information $uploaded_name = $_FILES[ 'uploaded' ][ 'name' ]; $uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); $uploaded_size = $_FILES[ 'uploaded' ][ 'size' ]; $uploaded_type = $_FILES[ 'uploaded' ][ 'type' ]; $uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ]; // Where are we going to be writing to? $target_path = 'uploads/'; $target_file = md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; $temp_file = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) ); $temp_file .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext; // Is it an image? if( ( strtolower( $uploaded_ext ) == 'jpg' || strtolower( $uploaded_ext ) == 'jpeg' || strtolower( $uploaded_ext ) == 'png' ) && ( $uploaded_size < 100000 ) && ( $uploaded_type == 'image/jpeg' || $uploaded_type == 'image/png' ) && getimagesize( $uploaded_tmp ) ) { // Strip any metadata, by re-encoding image (Note, using php-Imagick is recommended over php-GD) if( $uploaded_type == 'image/jpeg' ) { $img = imagecreatefromjpeg( $uploaded_tmp ); imagejpeg( $img, $temp_file, 100); } else { $img = imagecreatefrompng( $uploaded_tmp ); imagepng( $img, $temp_file, 9); } imagedestroy( $img ); // Can we move the file to the web root from the temp folder? if( rename( $temp_file, ( getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file ) ) ) { // Yes! $target = getcwd() . DIRECTORY_SEPARATOR . $target_path . $target_file; echo "<pre><a href='${target_path}${target_file}'>${target_file}</a> succesfully uploaded!</pre>"; } else { // No echo '<pre>Your image was not uploaded.</pre>'; } // Delete any temp files if( file_exists( $temp_file ) ) unlink( $temp_file ); } else { // Invalid file echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>'; } } else{ die("CSRF TOKEN MATCH FAILED!..."); } } generateSessionToken(); ?> <!DOCTYPE html> <html lang="en"> <head> <title>Bootstrap Example</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <div class="container"> <div class="row"> <p> </p> <div class="col-sm-4"> <div class="panel panel-default"> <div class="panel-heading text-center"><b>Secure Image Upload System</b></div> <div class="panel-body"> <form enctype="multipart/form-data" action="" method="POST"> <div class="form-group"> <label for="email">Choose an image to upload:</label> <input name="uploaded" type="file" > </div> <input type="hidden" name="_token" value="<?php echo $_SESSION['_token']; ?>" /> <button type="submit" name="Upload" class="btn btn-success btn-block">Upload</button> </form> </div> </div> </div> </div> </div> </body> </html>

Now the code for generating CSRF token in your functions.php file

<?php function generateSessionToken() { $data['_token'] = md5(uniqid(rand(), true)); $_SESSION['_token'] = $data['_token']; }

To ensure maximum protection we are checking CSRF token in our image upload system. A 32 character token is generated each time page is loaded and hidden field token is compared with the value of csrf token is the session to ensure that the post request is actually coming from our website and not from malicious websites.

After the token successfully matched we can start our security measures. First, we change the name of the file with a random unique md5 hashed string and also extracted the extension of the file. Then we make a temporary image file in system temporary directory or temporary directory specified by our PHP init file.

Now we check whether the file is an image or not. This time we also check the extension and also use a high secure image validation method called getimagesize(). The getimagesize function will determine the size of any supported given image file and return the dimensions along with the file type and a height/width text string to be used inside a normal HTML IMG tag and the correspondent HTTP content type. The sample output of this function is like this.

Array ( [0] => 496 [1] => 476 [2] => 2 [3] => width="496" height="476" [bits] => 8 [channels] => 3 [mime] => image/jpeg )

The above code will ensure that only jpeg and png image file is uploaded into our file system. But still, we need to ensure maximum security right?. So in case, a highly expert attacker mimics above validation rule we need a backup. So we Create a new image from uploaded the image using imagecreatefromjpeg() or imagecreatefrompng() function and then store that image to our temporary file using imagejpeg() and imagepng() function. This ensures that even if the attacker was able to upload his image it will we completely corrupted by our conversion method.

Now everything is ok and we can move our temporary file to our desired file system by using rename() method. The getcwd() function gives us current working directory. The DIRECTORY_SEPARATOR is a predefined PHP constant. Now we can delete the temporary file in our temporary directory if in case it exits using the unlink() method. So now you are successfully uploaded your image file.

In case you want to try PHP shell by yourself you can download it by visiting r57.gen.tr. Please try this at your own risk and don't test it on your production server as it is extremely dangerous. Now you can customise the HTML form as you wish. If anybody has any suggestions or doubts or need any help comment below.