文末附赠相关综合利用工具
文章中主要介绍以下4种漏洞的原理分析过程:
5.0.X 路由过滤不严谨rce
5.1.X 路由过滤不严谨rce
__construct 变量覆盖导致RCE
5.0.X 数据库信息泄露
设置兼容模式(pathinfo)路由解析时,没有对”\“进行过滤,导致可以指定任意模块控制器和方法,例如:
1.
http://127.0.0.1/tp5.0/public/index.php?s=index/think/app/invokefunction
模块:index
控制器:think
方法:app
参数:invokefunction
正常解析会出错,因为没有think中没有app方法。
2.
http://127.0.0.1/tp5.0/public/index.php?s=index/think\app/invokefunction
模块:index
控制器:think\app
方法:invokefunction
加上"\"后就会将think\app解析成为一个整体,就是think下的app文件。
首先进入入口文件public下的index.php中:
进入start.php中,先加载base.php,再执行app.php中的run方法。
跟进run方法,前面为初始化的一些东西
到这开始进入路由解析,使用的routeCheck方法。
跟进routeCheck方法,先调用Request.php中的path方法,
跟进path方法,调用pathinfo方法,来判断pathinfo(是否为兼容模式)。
跟进pathinfo方法,Config::get(‘var_pathinfo’)查看是否配置了var_pathinfo参数,如果配置了为兼容模式,并提取出默认参数s的值,最后传到app.php中的$path。
下面$config[‘url_route_must’]为查看是否开启了强制路由,如果开启解析’\’时就会失败,就不会产生rce。
进入parseUrlPath方法中,该方法将路由拆分为模块、控制器、方法。这里面并没有过滤’\’。
返回将返回得值赋值给$path,如下图:
提取模块,提取$path中的模块”index”。
提取控制器和方法,提取$path中的控制器”think\app”和方法”invokefunction”。
将拆分的信息封装进$route中,再将$route以数组中值的方式返回给$dispatch。
判断$dispatch中$type类型,选择执行module函数。
跟进静态module函数,一些初始化模块。
获取路由中的模块、控制器、方法。
执行控制器中的方法,调用静态方法invokeMethod。
实例化ReflectionMethod方法传入模块、控制器、方法,再通过Request类中的filterValue方法递归出传入的参数。
将递归出的参数,进行递归绑定,并写入$args中。
再调用反射的方法,并传入参数,也就是执行传入的恶意命令。
返回$data
http://[ip]/tp5/public/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
由于找不到5.1的低版本,所以只能分析漏洞关键点,大体流程和5.0差不多。
关键点:
路由没有过滤"\",导致可以通过"\"来指定任意模块和控制器。
利用链同样是invokefuntion方法,在think/Container.php中,但与5.0不同的是方法中存在了call_user_func_array函数,该函数可以将出入的第一个值当作回调函数,第二个值为回调函数的参数。 bindParams方法为递归提取传入的参数绑定到$args上。
直接从路由解析那块开始分析,App.php的run方法,调用routeCheck()
跟进routeCheck(),$path将url中的模块控制器方法函数取出来,$must判断是否为强路由模式,$dispatch路由检测,检测模块控制。
routeCheck()返回$dispatch,回到App.php中,$this->routeCheck()->init()就等于$this->$dispatch->init()就等于think\route\dispatch\Url->init()
跟进到think\route\dispatch\Url.php的init()中,调用parseUrl()
跟进parseUrl()中,我这个版本是修复了的,所以多出来的那块代码就是修复代码,低版本中并没有,所以就没有过滤”\“,导致可以调用think/Container中的invokeFuntion方法,导致可以使用call_user_func_array函数执行恶意命令。
?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
以thinkphp5.0.x为例
关键点
Request.php中method()中,没有对'var_method'进行过滤,用户可控传入__constrct,导致调用__construct析构函数,将传入变量循环覆盖到$this->method="GET",$this->get="whoami",$this->filter="system";如果开启debug,会调用call_user_func导致rce。
我们payload传入
POST http://127.0.0.1/tp5.0/public/index.php?s=index
_method=__construct&filter[]=system&method=GET&get[]=whoami
上面的那些操作就不说了,直奔主题,进行路由检测会调用静态方法routeCheck()
跟进又调用Route类里面的静态方法check()
跟进check(),又调用method()
跟进Request.php中method(),Config::get(‘var_method’)提权var_method中的值,var_method的又为_method,这个_method可控,我们传入’__construct’,到$this->{$this->method}($_POST);中就变为$this->__construct($POST)。
跟进__construst(),该方法将我们POST方法传入的参数通过foreach循环覆盖到相对应的参数上。
返回$method=‘get’。
到self::$rules[$method];,这也是为什么要传入method=GET的原因,因为不传入的的话$method=__construct,如果静态数组$rules内不存在该参数就会报错。
所以我们传入的method可以不是GET,只要在这个数组里面就行。
跟进Request.php中param(),,将POST中的参数赋值给$vars,又调用get()。
跟进get(),调用input(),传入刚才覆盖的$this->get。
跟进input(),将$this->get的值赋值给$data。
返回到param()中,又调用input()。
跟进input(),将$this->filter赋值给$filter。
再到下面通过array_walk_recursive函数,调用filterValue方法,将$data和$filter的值带入到filterValue方法中。
跟进filterValue(),使用call_user_func回调函数,回调$filter的值system函数,将$value用上面的array_walk_recursive函数遍历传入system函数中执行,并把执行结果传入到$data中。
再到下面,$dispatch[‘type’]的值为’module’,调用静态方法module()。
跟进module(),到最后调用静态方法invokeMethod()。
跟进invokeMethod(),调用静态方法bindParams()
跟进bindParams(),通过Config::get(‘url_param_type’)获取参数为0,调用Request::instance()->param(),又到了熟悉的param()了,后面的跟上面一样。
将命令执行结果返回到$vars中。
为什么输出四个呢? 前两个为开启debug里面调用param()链,然后因为$values里面又两个’whoami’,所以遍历的时候执行了2次,后面的是module方法调用的param()链,后面也一样执行了两次,所以显示4个结果。
POST http://127.0.0.1/tp5.0/public/index.php?s=index
_method=__construct&filter[]=system&method=GET&get[]=whoami
关键点
也是路由过滤不严谨,导致调用任意类方法,这回只是利用链不一样了,调用think\Config中的get方法,读取配置文件。
直奔关键代码部分,调用module()执行传入的方法。
跟进到module()中,到最后调用静态方法invokeMethod()执行操作。
跟进invokeMethod(),开始实例化反射类ReflectionFunction,传入$class和$method,也就是模块控制器方法,再用静态方法bindParams(),把参数绑定到$args中,也就是我们传入的”database.username”,最后调用invokeArgs将参数传入之前的反射类中执行。
因为执行的是get方法,跟进get()方法,此方法为读取配置文件,所以我们构造的参数进入get()中就会控制读取内容,后面返回self::$config[$range][$name[0]][$name[1]]。
也就是::config下的_sys_下的database下的username,也就是返回了数据库的用户名,我们构造的database.username,也可以换成database下的任意一个元素,database.password也就是查看数据库密码。
http://127.0.0.1/tp5.0/public/index.php?s=index/thinkconfig/get&name=database.username
综合利用工具地址:
https://github.com/iceberg-N/thinkphp5.x_Scan