ThinkPHP5.0.x远程执行漏洞
2024-5-11 20:43:38 Author: www.freebuf.com(查看原文) 阅读量:3 收藏

参考链接:链接一链接二

漏洞概要

  • 漏洞名称:ThinkPHP 5.0.x-5.1.x 远程代码执行漏洞

  • 参考编号:无

  • 威胁等级:严重

  • 影响范围:ThinkPHP v5.0.x < 5.0.23,ThinkPHP v5.1.x < 5.0.31

  • 漏洞类型:远程代码执行

  • 利用难度:容易

  • payload:?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

漏洞概述

2018年12月10日,ThinkPHPv5系列发布安全更新,修复了一处可导致远程代码执行的严重漏洞。此次漏洞由ThinkPHP v5框架代码问题引起,其覆盖面广,且可直接远程执行任何代码和命令。电子商务行业、金融服务行业、互联网游戏行业等网站使用该ThinkPHP框架比较多,需要格外关注。由于ThinkPHP v5框架对控制器名没有进行足够的安全检测,导致在没有开启强制路由的情况下,黑客构造特定的请求,可直接进行远程的代码执行,进而获得服务器权限。

漏洞分析

前期准备

  1. 为什么从?s=开始

在application/config.php第78行左右有这个定义,var_pathinfo键的默认值就是s

image-20240416175137116

搜索一下,发现在thinkphp/library/think/Request.php中有这个键

image-20240511162804562

用Config::get获取了var_pathinfo的值,也就是s的值,然后变成了$_GET['s']然后把值赋给$_SERVER['PATH_INFO'],判断url里是否有兼容模式参数,然后下面$this->pathinfo = empty($_SERVER['PATH_INFO']) ? '/' : ltrim($_SERVER['PATH_INFO'], '/');

去除了最左边的正斜杠。得出结论,s就是用来获取payload中的index/\think\app/invokefunction这部分的,而它的值在源码里面就作为pathinfo。知道这个后,继续跟着程序执行流程

执行到thinkphp\library\think\Route.php#check()

image-20240509122013318

其中,$url实际上传入的就是刚才的pathinfo的值。然后$url = str_replace($depr, '|', $url);把所有/换成了|。那么现在就是index|\think\app|invokefunction这个样子。但是因为没有定义好的路由规则,最后还是执行了

image-20240511164514016

之后在thinkphp\library\think\App.php中

image-20240509122254011

可以看到,如果没有定义好路由规则,也就是刚才的return了false,并且还有强制路由的话就会抛出错误。但是这个漏洞利用条件之一就是不开启强制路由~。

那么再往下走,就会到parseUrl

image-20240511172146183

然后parseUrl用parseUrlPath()方法,用$path = explode('/', $url);来把这个url给处理为了一个数组,返回了一个path数组

image-20240511172317091

这样实际上就是分成了模型、控制器、方法了。最终逐个获取的位置是parseUrl方法的:

image-20240509133426666

漏洞原因

当程序执行到thinkphp\library\think\App.php#exec(),会进入到module分支,来到module方法,在:

image-20240509133750350

这里的result数组实际上就是刚才parseUrl返回的那个path数组,

image-20240511172646999

1=>控制器,2=>操作名(也叫做方法名)。在这里获取了之后,程序没有再对控制器、方法名过滤导致了任意控制器下任意方法的调用。然后会使用加载器

image-20240509134758591

加载think\app,在thinkphp\library\think\Loader.php#controller()

image-20240509134958642

然后在invokeClass中使用反射API来动态实例化一个类,并根据提供的参数调用其构造函数。这在需要动态创建对象而不直接使用new关键字时非常有用。

image-20240509135142260

这个函数的作用是把think\app实例化成一个类(描述不准确),然后就可以调用这个类的函数了

返回到了$instance变量中,这时控制器这块已经基本上处理完了,think\App实际上就是thinkphp\library\think\App.php这个php文件里的App类(在MVC架构中,某些类也是可以叫做控制器),think是一个命名空间。意思就是think命名空间下的都是thinkphp的核心代码。

new \ReflectionClass($class):这行代码创建了一个ReflectionClass对象,它用于获取关于给定类的信息,比如它的构造函数、方法、属性等。

$reflect->getConstructor():这行代码获取了类的构造函数。如果类没有定义构造函数,则返回null

$constructor ? self::bindParams($constructor, $vars) : []:这是一个三元运算符。如果$constructor不为null(即类有构造函数),则调用self::bindParams方法,该方法用于将$vars数组中的值绑定到构造函数的参数上。如果$constructornull,则$args数组为空。

$reflect->newInstanceArgs($args):这行代码使用ReflectionClass对象的newInstanceArgs方法来实例化类。这个方法接受一个参数数组,这些参数将传递给类的构造函数。如果之前没有构造函数或者没有提供参数,则不会调用构造函数。

接下来就是获取方法,调用反射执行类的方法。App类里的invokefunction方法使用反射API来动态调用一个函数,并根据提供的参数传递给函数。这在需要动态调用函数而不直接使用函数调用语法时非常有用

image-20240509135934467

它先获取了$function的信息,然后把$vars数组中的参数与函数绑定,然后使用invokeArgs方法调用了这个函数以及绑定的参数

`self::bindParams($reflect, $vars):这行代码调用了一个名为bindParams的静态方法,该方法用于将$vars数组中的值绑定到函数的参数上。$reflect对象提供了函数参数的信息。

self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'):这是一个条件语句,用于记录函数的执行信息。如果self::$debugtrue,则调用Log::record方法记录一条信息日志,其中包含了函数的名称和执行信息。$reflect->__toString()返回函数的名称。

$reflect->invokeArgs($args):这行代码使用ReflectionFunction对象的invokeArgs方法来调用函数。这个方法接受一个参数数组,这些参数将传递给函数。

总结

所以总体流程如下:

  1. 传入的?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami,其中s(var_pathinfo)会取值index/\think\app/invokefunction

  2. s会先处理成为index|\think\app|invokefunction,之后进一步被处理为path数组

  3. 然后进入module函数中,$controller得到值为/think/app,作为参数执行controlle函数

  4. /think/app作为参数在invokeClass函数中被实例化,之后就可以调用,\think\app的函数

  5. 执行了App里的invokeFunction,并给其传入了参数1:function=call_user_func_array,参数2:vars[]=system&vars[1][]=whoami,相当于执行了call_user_func_array('system', ['whoami']),也就是system('whoami')。执行了命令。


文章来源: https://www.freebuf.com/vuls/400709.html
如有侵权请联系:admin#unsafe.sh