所需进行代码审计的文件路径:
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
函数就会被攻击者利用来读取任意文件,这充分暴露了代码在安全防护方面的不足。在开发过程中,对于涉及文件操作的用户输入,必须进行严格的安全检查,以防止类似的路径穿越漏洞导致的安全问题。