ThinkPHP5 代码执行初探

0x01:前言

此篇文章只是简单的对我看到ThinkPHP5 代码执行的payload时遇到的疑问进行学习和解释。
如果你和我有同样的问题,希望这篇文档能对你有所帮助

0x02:复现问题

首先给出payload:
/thinkphp5/public/index.php?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

对于本人来说,这个payload会带来几个问题:
1)如何理解这一串url
2)程序是如何调用的
3)s=index/think\Container/invokefunction这个url的意思

针对这几个问题将官方文档过了一遍,结合一些师傅的文章算是有了大概的理解。

0x03:框架初探

会有以上问题的原因是对框架不理解,看到网上一些师傅的复现文章都是直接拿php文件进行分析,所以自己看文章就是调用来调用去,不明所以。

所以首先去了解了ThinkPHP的URL模式,对于ThinkPHP5来说:
在没有定义路由的情况下典型的URL访问规则(PATHINFO模式),如果不支持PATHINFO的话可以使用兼容模式

我这里创建一个测试的方法

1)以PATHINFO访问

http://localhost:8888/thinkphp5/public/index.php/index/user/test/name/tom

即在/public/index.php/模块/控制器/方法/参数/参数值

2)以兼容模式访问
http://localhost:8888/thinkphp5/public/index.php?s=index/user/test/name/tom

http://localhost:8888/thinkphp5/public/index.php?s=index/user/test?name=test

先理解这两种访问模式,后面会用到,因为这漏洞前提是未开启强制路由从而导致任意类方法调用导致的rce

在配置文件中

0x04:漏洞复现

回到payload中:/thinkphp5/public/index.php?s=index/think\Container/invokefunction,发现payload与两种url模式有点不太一样,在兼容模式的链接中有一个\,即think\Container

带着问题去分析程序执行流程,在/thinkphp/library/think/App.php文件中

在此处设下断点,然后访问payload:http://localhost:8888/thinkphp5/public/index.php?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=

可以自行跟进每个流程,这里我只列出了部分过程

在think/route/dispatch/Url.php中,进入路由解析

跟进parseUrl函数,可以看到parseUrlPath函数

在parseUrlPath()内对路由进行分割 拿到模块、控制器、操作、参数、参数值,可以看到是以/进行分割的,为了得到完整了控制器,传入的链接为:think\Container

继续跟进,可以看到获取了完整的控制器

继续跟进,在进行路由调度的过程中,可以在/thinkphp/library/think/App.php中可以看到如下函数

因为控制器传入的是:为think\container,所以进入了第一个if判断,即:
class = “think\container”,module = “index”

最后进入控制器类的实例化,从而去调用think\Container类下的invokefunction函数

但是相信有人和我一样对此payload还有一个疑问就是这个开头的index/,invokefunction在/thinkphp/library/think/Container.php文件中,与index模块并关系

分析流程可以在thinkphp5/thinkphp/library/think/route/dispatch/Module.php中可以看到如下函数

程序调用到了这一步,进行了if语句下的初始化模块操作,说明module 和available均为true

而available开始为false,经过三个if语句中的其中一个才设置为true,这里由于没有路由绑定,所以$bind为null,不满足第一个if

empty_module设置为空,不满足第三个,所以这里是进入了第二个if语句

elseif (!in_array($module, $this->rule->getConfig('deny_module_list')) && is_dir($this->app->getAppPath() . $module)) {
                $available = true;

首先判断模块在不在禁止的模块列表中,然后模块得是真实存在的模块

这就要求在构造POC时,模块名必须真实存在并且不能在禁用列表中。所以POC中指定index默认模块

所以为什么payload为s=index/think\Container/invokefunction

这时在看整个payload:
http://localhost:8888/thinkphp5/public/index.php?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

即think目录下的Container类中的invokefunction函数存在漏洞,后面为invokefunction函数的参数

0x05:快速理解

若不去学习tp框架的基础知识,跟流程还是云里雾里的,简单来说就是:控制器过滤不严,导致可以用命名空间的方式来调用任意类的任意方法。

通过payload体现:
/index.php?s=/index/namespace\class/method

什么意思呢,本地写个小例子用于理解

按照上面payload体现,我们调用的方法应该是:s=/index/app\index\controller(namespace)\index(class)/test(method)

可以看到成功访问到了,那么要做的就是找到存在漏洞的类方法通过调用来实现rce

再找到一个漏洞payload:
http://localhost:8888/thinkphp5/public/index.php?s=index/\think\Request/input&filter[]=system&data=pwd

注意这个5.1.x下利用的

通过上面说的来理解这个payload,think命名空间下的Request类下的input方法

可以通过这个快速找到漏洞所在位置

传入的data不是数组进入else的filterValue函数,跟进此函数

filter和value均可控,调用了call_user_func从而导致了代码执行

0x06:小结

当然这篇文章不能带你彻底理解整个漏洞的起因和框架流程,想要理解这些可以去过一遍官方文档了解框架,再自己debug走一遍程序的调用流程。但是希望能够对理解payload以及定位漏洞函数点有所帮助,至少不做一个只会用poc的人便是一种进步。

发表评论

电子邮件地址不会被公开。 必填项已用*标注