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

前置知识:PHP函数缺陷

测试环境:MetInfo CMS

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

漏洞URL:/include/thumb.php?dir=

漏洞文件位置:MetInfo6.0.0\app\system\include\module\old_thumb.class.php

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 复制代码
<?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\ 目录不存在。
相关推荐
大方子14 小时前
【PolarCTF】rce1
网络安全·polarctf
枷锁—sha16 小时前
Burp Suite 抓包全流程与 Xray 联动自动挖洞指南
网络·安全·网络安全
聚铭网络17 小时前
聚铭网络再度入选2026年度扬州市网络和数据安全服务资源池单位
网络安全
darkb1rd19 小时前
八、PHP SAPI与运行环境差异
开发语言·网络安全·php·webshell
世界尽头与你1 天前
(修复方案)基础目录枚举漏洞
安全·网络安全·渗透测试
枷锁—sha2 天前
【SRC】SQL注入快速判定与应对策略(一)
网络·数据库·sql·安全·网络安全·系统安全
liann1192 天前
3.1_网络——基础
网络·安全·web安全·http·网络安全
ESBK20252 天前
第四届移动互联网、云计算与信息安全国际会议(MICCIS 2026)二轮征稿启动,诚邀全球学者共赴学术盛宴
大数据·网络·物联网·网络安全·云计算·密码学·信息与通信
旺仔Sec2 天前
一文带你看懂免费开源 WAF 天花板!雷池 (SafeLine) 部署与实战全解析
web安全·网络安全·开源·waf
七牛云行业应用2 天前
Moltbook一夜崩盘:150万密钥泄露背后的架构“死穴”与重构实战
网络安全·postgresql·架构·高并发·七牛云