前言

前几天禅道出了一个全版本的 rce,今天闲下来稍微看了一遍,实际上,从漏洞发现到找到利用链,从我的角度出发,这应该算是一个定位漏洞函数,然后回溯构造利用链的过程。

参考:http://foreversong.cn/archives/1410

思考

先不谈漏洞,从路由出发,大致了解一下框架的基本路由。

01

这里的 requestFix 是-,模块名和方法名是根据 url 中的moduleName-methodName确定,模块名和方法名都经过正则的过滤,防止模块名通过../目录跳转。

1
2
3
4
public static function checkCode($var)
{
return self::checkREG($var, '|^[A-Za-z0-9]+$|');
}

setControlFile() 找到控制器文件。

1
2
3
4
5
public function setControlFile($exitIfNone = true)
{
$this->controlFile = $this->moduleRoot . $this->moduleName . DS . 'control.php';
if(file_exists($this->controlFile)) return true;
}

最终调用的是/module/[moduleName]/control.php中定义的类的 methodName 方法。

接着,通过反射取到我们传入的 moduleName 和 methodName 指定的方法的默认参数值,没有默认值的用_NOT_SET占位。

02

接着从 url 中取实际参数值。

1
2
3
4
if($this->config->requestType != 'GET') 
{
$this->setParamsByPathInfo($defaultParams);
}

跟进 setParamsByPathInfo()。

03

这里的分割符 requestFix 和前面一样是-,从 url 中取出参数值后,和默认值进行 merge。所以,实际的路由为/moduleName-methodName-参数1-参数2...。最终,会调用 module 目录下指定模块文件夹的 control.php 文件中定义的类的方法。当然,根据不同的角色,是有权限控制的。

1
2
// index.php line 67
$common->checkPriv();

下面,从原作者角度出发,原作者本身一开始就是想找 rce 的点,自然我们可以通过正则定位一下危险函数,比如 eval()、system() 等。而大部分面向对象的框架、cms,我们也可以定位一下 call_user_func()、call_user_func_array(),如果传入其中的参数可控,我们可以实现调用任意类的任意方法。

定位到 /module/api/control.php 的 getModel() 方法。

04

这个方法是在 module 目录下的 api 模块中,我们是可以直接路由到的,且参数也可控,

比如,我们访问/api-getModel-moduleName-methodName-test1=llfam,test2=ll

05

可以看到,进入 call_user_func_array() 函数的所有参数都可控,但是具体调用哪个类是由 loadModel() 方法决定,传入该方法的 moduleName 参数我们可控。

跟进 loadModel()。

06

这里有两个分支,如果在 loadedModels 变量中找到了[moduleName]Model类,则返回该类,如果没找到,就调用 setModelFile() 根据 moduleName(可控)去 module 目录下找指定模块的 model.php 中定义的类,实例化后返回。

总结

整理一下审计的思路,跟一遍正常请求,熟悉基本路由,定位危险函数 call_user_func_array(),找到漏洞点 getModel(),因为这是一个路由可以定位到方法,所以参数完全可控,接着就是去构造利用链,关于具体利用方式,可以参考开头的文章,我就不多做分析了。

其实,我个人觉得这个洞并不是很难发现,因为以前没有审过禅道,一开始以为这个 getModel() 接口是新版本的新功能。但是,我发现在 11.4.1 中就已经存在这个问题了。其实,不光是前面说的 rce 相关的,还有写文件、包含、读文件等函数都可以去定位一遍,根据经验和回溯或许就能发现一些问题。