拓展知识六:MetInfo6.0.0目录遍历漏洞原理分析
所需进行代码审计的文件路径:
C:\phpStudy\WWW\MetInfo6.0.0\include\thumb.php
C:\phpStudy\WWW\MetInfo6.0.0\app\system\entrance.php
C:\phpStudy\WWW\MetInfo6.0.0\app\system\include\class\load.class.php
C:\phpStudy\WWW\MetInfo6.0.0\app\system\include\moduleold_thumb.class.php
文件间的关联与作用
thumb.php:
<?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. ?>此文件定义了 4 个常量。
M_NAME和M_MODULE被设为include,虽在此漏洞利用过程中未直接起关键作用,但它们是系统模块标识的一部分。重点是M_CLASS被定义为old_thumb,M_ACTION被定义为doshow,这两个常量为系统后续确定执行的类和方法提供了核心标识。同时,通过require_once '../app/system/entrance.php';引入系统入口文件 entrance.php,启动整个系统执行流程。重点代码:
<?php define('M_CLASS', 'old_thumb'); define('M_ACTION', 'doshow'); require_once '../app/system/entrance.php'; ?>
define函数:
define('M_CLASS', 'old_thumb');:这行代码使用define函数定义了一个名为M_CLASS的常量,并将其值设置为old_thumb。常量在整个脚本执行过程中值是固定不变的,不能被重新赋值。
define('M_ACTION', 'doshow');:同样使用define函数定义了名为M_ACTION的常量,值为doshow。这些常量后续会被用来确定系统要执行的类和类中的方法。
require_once语句:
require_once '../app/system/entrance.php';:require_once是 PHP 中用于引入外部文件的语句。它会尝试引入../app/system/entrance.php文件。如果该文件已经被引入过,require_once不会再次引入,以避免重复引入导致的问题。在这里,它的作用是启动系统的执行流程,进入entrance.php文件继续执行。
entrance.php:
//当前访问的主机名 define ('HTTP_HOST', isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']); //来源页面 define('HTTP_REFERER', $_SERVER['HTTP_REFERER']); //来源页面 define('REQUEST_URI', $_SERVER['REQUEST_URI']);//脚本路径 $phpfile = basename(__FILE__); $_SERVER['PHP_SELF']=htmlentities($_SERVER['PHP_SELF']); define ('PHP_SELF', $_SERVER['PHP_SELF']=="" ? $_SERVER['SCRIPT_NAME'] : $_SERVER['PHP_SELF']);if (!preg_match('/^[A-Za-z0-9_]+$/', M_TYPE.M_NAME.M_MODULE.M_CLASS.M_ACTION)) {echo 'Constants must be numbers or letters or underlined';die(); }require_once PATH_SYS_CLASS.'load.class.php';load::module();# 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.该文件首先定义了与 HTTP 请求和服务器相关的常量,如
HTTP_HOST用于获取当前访问的主机名,HTTP_REFERER记录来源页面,REQUEST_URI表示当前请求的 URI 等。这些常量主要用于系统对请求环境的识别和记录,但与本次路径穿越漏洞利用无直接关联。接着,它对M_TYPE.M_NAME.M_MODULE.M_CLASS.M_ACTION进行正则匹配检查,确保其由数字、字母或下划线组成,目的是保证系统常量的规范性。关键在于require_once PATH_SYS_CLASS.'load.class.php';引入了load.class.php文件,并调用load::module();方法。在调用load::module()方法时,默认会依据 thumb.php 中定义的M_CLASS和M_ACTION常量来确定要加载的类和执行的动作,由此建立起与 thumb.php 和load.class.php的紧密联系。重点代码:
require_once PATH_SYS_CLASS.'load.class.php'; load::module();
require_once语句:
require_once PATH_SYS_CLASS.'load.class.php';:这行代码引入了load.class.php文件,PATH_SYS_CLASS是一个预定义的路径常量(在提供的代码中虽然没有看到它的定义,但从代码逻辑推测应该是存在的),它和'load.class.php'拼接成完整的文件路径。引入这个文件是为了使用其中定义的load类及其相关方法。
load::module()方法调用:
load::module();:这里调用了load类的静态方法module。load类定义在load.class.php文件中,module方法的作用是根据传入的参数(如果没有传入参数则使用默认值)来加载相应的类并执行类中的方法。在这个调用中,由于没有传入参数,它会使用默认的参数值,而默认值就与thumb.php中定义的M_CLASS和M_ACTION常量相关。
load.class.php:
class load {// ...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);}// ...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;}// ... }该文件提供了一系列加载类、函数库、模块等的方法。其中
load::module方法至关重要,在没有传入特定参数时,它会使用默认值。这里if (!$modulename) $modulename = M_CLASS;和if (!$action) $action = M_ACTION;,意味着会使用 thumb.php 中定义的M_CLASS(即old_thumb)和M_ACTION(即doshow)。然后调用self::_load_class方法。_load_class方法会先检查要加载的类文件是否存在,若存在则引入。当$action存在且符合条件(以do开头)时,会实例化类并调用类中的对应方法。由于load::module方法在 entrance.php 中被调用,且根据 thumb.php 的常量设置,最终会调用old_thumb类的doshow方法。重点代码:
class load {public static function module($path = '', $modulename = '', $action = '') {if (!$modulename) $modulename = M_CLASS;if (!$action) $action = M_ACTION;return self::_load_class($path, $modulename, $action);}private static function _load_class($path, $classname, $action = '') {if ($action) {if (!class_exists($classname)) {die($classname . ' ' . $action . ' class\'s file is not exists!!!');}$newclass = new $classname;if ($action!='new') {if(substr($action, 0, 2) == 'do' && method_exists($newclass, $action)) {call_user_func(array($newclass, $action));}}return $newclass;}return true;} }
module方法:
public static function module($path = '', $modulename = '', $action = ''):这是load类的一个公共静态方法,接受三个参数$path(文件路径)、$modulename(类名)和$action(要执行的方法名)。
if (!$modulename) $modulename = M_CLASS;:如果没有传入$modulename参数,就使用M_CLASS常量的值作为类名。
if (!$action) $action = M_ACTION;:同理,如果没有传入$action参数,就使用M_ACTION常量的值作为要执行的方法名。
return self::_load_class($path, $modulename, $action);:调用_load_class方法,并将当前的参数传递过去,_load_class方法会负责加载类并执行相应的方法。
_load_class方法:
private static function _load_class($path, $classname, $action = ''):这是一个私有静态方法,用于实际加载类和执行方法。
if ($action):检查是否传入了$action参数,如果有传入才会继续执行后续逻辑。
if (!class_exists($classname)):检查指定的类名$classname是否存在,如果不存在就输出错误信息并终止脚本执行。
$newclass = new $classname;:如果类存在,就实例化这个类。
if ($action!='new'):如果$action不是new,则继续检查。
if(substr($action, 0, 2) == 'do' && method_exists($newclass, $action)):检查$action是否以do开头,并且类中是否存在这个方法,如果满足条件,就使用call_user_func函数调用类中的方法。
old_thumb.class.php:
<?php # MetInfo Enterprise Content Management System # Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved.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']);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();}} } # 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.此文件定义了
old_thumb类,其中doshow方法存在路径穿越漏洞。在doshow方法中,$dir = str_replace(array('../','./'), '', $_GET['dir']);从$_GET中获取dir参数,并尝试通过str_replace函数移除../和./来进行简单过滤。但这种过滤方式存在严重缺陷,攻击者可构造特殊输入如....//绕过过滤。随后,若满足一定条件,会使用readfile($dir);函数读取文件。若攻击者成功绕过过滤,就能利用该函数读取服务器上的任意文件,例如敏感文件/etc/passwd,从而导致敏感信息泄露。重点代码:
class old_thumb {public function doshow() {$dir = str_replace(array('../','./'), '', $_GET['dir']);if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false){readfile($dir);}// ... 其他逻辑if ($img) {readfile(PATH_WEB.str_replace($_M['url']['site'], '', $img));}} }
old_thumb类:
class old_thumb:定义了一个名为old_thumb的类。
doshow方法:
public function doshow():这是old_thumb类的一个公共方法。
$dir = str_replace(array('../','./'), '', $_GET['dir']);:从$_GET超全局变量中获取名为dir的参数值,然后使用str_replace函数尝试移除字符串中的../和./。这里的意图是对用户输入的路径进行简单过滤,防止目录穿越。
if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false):进一步检查处理后的$dir参数,如果它以http开头并且不包含./,则执行readfile($dir);。readfile函数会读取指定路径的文件内容并输出。
if ($img):在后续逻辑中,如果$img存在,也会使用readfile函数读取经过处理的路径对应的文件内容。这里的问题在于,str_replace函数的过滤方式不够严格,攻击者可以通过构造特殊的输入(比如....//)绕过过滤,从而实现路径穿越,读取服务器上的任意文件。
利用过程及方式
- 攻击者构造请求:攻击者精心构造一个包含恶意
dir参数的 HTTP 请求,假设系统访问入口为http://example.com/thumb.php,攻击者构造的请求可能是http://example.com/thumb.php?dir=../../../etc/passwd。这里使用../../../等目录穿越字符,试图突破正常的文件访问限制,访问到服务器上的敏感文件/etc/passwd。这是利用了old_thumb.class.php中doshow方法对dir参数过滤不严格的漏洞。- 系统执行流程:当攻击者的请求发送到服务器并到达 thumb.php 文件后,thumb.php 引入 entrance.php 文件,启动系统流程。entrance.php 文件在执行过程中,引入
load.class.php并调用load::module()方法。由于 thumb.php 中定义的M_CLASS为old_thumb,M_ACTION为doshow,load::module方法在执行时,会根据这些默认常量设置,通过_load_class方法实例化old_thumb类并调用其doshow方法。具体来说,load::module方法中if (!$modulename) $modulename = M_CLASS;和if (!$action) $action = M_ACTION;语句使得old_thumb类和doshow方法被选中,然后_load_class方法负责完成类的实例化和方法调用准备工作。- 漏洞触发:在
old_thumb.class.php的doshow方法被调用后,获取到攻击者传入的恶意dir参数。尽管进行了简单的str_replace过滤,但攻击者构造的特殊输入可能绕过该过滤。例如,若攻击者输入....//,经过str_replace后可能仍然包含有效的目录穿越字符。随后,readfile函数会尝试读取经过处理后的dir对应的文件路径。如果攻击者成功绕过过滤,readfile函数就会读取服务器上的敏感文件,如/etc/passwd,将文件内容返回给攻击者,导致敏感信息泄露。
代码分析和理解
- 常量定义的影响:thumb.php 中的常量定义在整个系统执行流程中起着关键的引导作用。
M_CLASS和M_ACTION常量如同系统执行的 “导航标”,虽然它们本身只是简单的字符串定义,但却决定了系统后续执行的具体类和方法。攻击者正是利用了这一机制,通过控制与这些常量相关的请求参数,间接地影响到存在漏洞的old_thumb.class.php中的doshow方法。这种常量定义方式在实现系统功能模块化和灵活性的同时,也为潜在的安全攻击留下了隐患,因为常量的全局有效性使得攻击者可以在系统的执行流程中找到切入点。- 文件加载和方法调用:entrance.php 引入
load.class.php并调用相关方法,体现了系统模块化设计的思路,将不同功能的代码分离到不同文件中,便于管理和维护。然而,这种设计也为攻击者利用模块间的关联创造了条件。load.class.php中的方法负责加载各类资源和调用相应的功能,其逻辑较为复杂。例如,load::module和_load_class方法之间的协作,通过层层传递参数和执行逻辑,最终实现类的加载和方法的调用。攻击者通过分析系统的这种执行逻辑,利用 thumb.php 中的常量设置,精准地控制load::module方法的参数,从而调用到存在漏洞的doshow方法。这表明在系统设计中,虽然模块化提高了开发效率,但也需要更加严格地把控模块间交互的安全性,避免因复杂的调用关系而引入安全风险。- 输入验证和过滤不足:
old_thumb.class.php中doshow方法对dir参数的验证和过滤过于简单。仅使用str_replace函数移除../和./,这种方式无法有效抵御目录穿越攻击。从安全编程的角度来看,对用户输入的验证应该采用更加严格和全面的方式,例如使用白名单过滤,只允许合法的字符和路径格式。攻击者通过构造特殊输入,如....//,可以轻易绕过这种简单的过滤机制。一旦绕过成功,readfile函数就会被攻击者利用来读取任意文件,这充分暴露了代码在安全防护方面的不足。在开发过程中,对于涉及文件操作的用户输入,必须进行严格的安全检查,以防止类似的路径穿越漏洞导致的安全问题。
