前记
说好了每周一篇代码审计,虽然yxcms那个挖的坑还不小,但是看了一下目前的CTF题目和Thinkphp关系大点,所以先来审计一篇Thinkphp吧,这个洞是今年年初爆出的,虽然已经有团队给出了比较详细的分析文章,但还是斗胆在这里写上自己的分析文章.
<!--more-->
先说说payload和环境配置
本来是准备使用P神Vulhub进行环境搭建和复现的,但那个是docker文件,不好具体分析和调试,因此还是在本机简单搭建的环境.
PHP版本为:5.4以上即可
搭建方式:按照开发手册中的安装先安装应用,再在应用文件里面安装核心即可
有关Payload:
原版Vulhub给出的payload:

payload为:
GET:
s=captcha
POST:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
由于我的环境搭建在window下,因此我将ip命令换成了dir命令,如下:
整体结构是不变的,不过我试了一下这里filter不传入数组也可以远程命令执行,所以暂时不是很清楚这个传数组的意义在哪,好了废话不多说,开始分析环节吧
发现过程
首先我们根据传入的内容猜测肯定是和解析$_GET和$_POST变量有关的,因此我们需要知道Thinkphp是如何处理请求的,通过翻阅开发手册,发现Thinkphp定义一个Request类,在这个类中集中处理请求相关的操作,因此主要中心就放在了Request类上,同样的,通过翻阅开发手册,得知在Request类中是使用param方法获得请求变量的.

然后查看该方法体发现,中间几个可能出现问题的方法,比如
$this->method()
和$this->input()
等,在通过查看这些方法体一步步向下深入,最后找到了漏洞出发处.具体执行
首先查看Request类的param()方法,方法体如下:

注意其中使用了$this->method()和$this->input()方法,从调用顺序先来看看method()方法,方法体如下:

这里我们进行一次Payload传入的动态调试来看看,这些变量是如何传递和变化的:

首先method参数是已经定义好的属性,本身默认的值就是false,然后经一系列判断来到了动态调用
$this->{$this->method}($_POST)
处,这里通过动态调试已经显示出来了method的值是__CONSTRUCT
,原因在于在Config类中的var_method值就是$_POST["_method"]
,如下图:
而由于我们传入的
$_POST["_method"]=__construct
,因此这里动态调用实际上调用的是$this->__construct($_POST)
直接就将$_POST数组作为参数传给了构造方法,在动态调试中就直接跳转至__construct方法之中了,如下图:
这里使用了property_exists()函数来判断对于的键值对是否存在,若存在则以键为名将其注册成为变量,随后保存一个变量,这就完成了一个构造过程了,然后通过几轮对传入方法的判断之后,跳入param方法中执行(解析并获得传入参数的值),同样的需要执行method方法去判断传入参数方法,不过由于进入param方法之前,已经将method值设置为了true,因此这个时候会跳去执行server()方法,如下图:

紧接着就会调用input()方法:

我们注意到在input()方法内部出现了一处调用filter还有data的地方,并且在传入数据都是脏数据的情况下.
怀疑点有二:
- 此处传入了filter和data数据
本身上面的array_walk_recursive()函数就是递归调用,且调用函数为$this->filterValue()
因此怀疑漏洞点就发生在这里,然后我们查看源码,如下图:
出现了一个非常危险的函数call_user_func()
,第一个参数$filter根据上面的input()来看实际上就是我们传入的filter[]=system
,接着我们再进行动态调试,看看下一步会如何执行,如下图:
这里使用了is_callable()
进行判断是否可以回调,由于system函数是存在的,因此可以通过这层判断,随后外部使用一个foreach循环进行控制整个流程,通过循环之后,thinkphp就开始利用传入的filter值进行函数的调用,这里原意应该是用filter控制过滤函数的调用,但这个filter参数从外部来看,是用户可控的,也就意味着用户可以通过控制filter参数进行系统函数的调用,从而实现远程命令执行,然后fliter参数对应的传入数据是我们用param()方法传入的数据,当其中一个参数传入形似"id","pwd"等系统命令时,就会被system成功调用,从而成功执行系统命令,这也是这个漏洞的成因,原因在于filter参数和data参数(传入数据)都是用户可控的最后的分析
先总结一下流程图吧:start=>start: 用户输入 e=>end: 执行系统命令 op0=>operation: 传入_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=dir op8=>operation: 调用param()方法解析这些参数 c1=>condition: 判断用户输入方法_method是否存在 op1=>operation: 调用__construct魔术方法 op2=>operation: 将传入参数解析注册成为php内部的变量 op3=>operation: 回调param()方法解析这些注册好的变量 op4=>operation: 由param()方法调用input()方法分别解析传入 op5=>operation: 因为要对数据进行过滤,所以使用filter参数提供的函数名传入filterValue()进行动态过滤 op6=>operation: 调用filterValue()内部的call_user_func()函数 op7=>operation: 调用call_user_func($_POST['filter'],$_POST['server']) start->op0 op0->op8->c1 c1(yes)->op1 op1->op2 op2->op3 op3->op4 op4->op5 op5->op6 op6->op7->e
然后总结一下漏洞触发过程(先挖坑为啥s需要等于capcha.这里的s值实际上是一个入口的问题):
- 原本调用的Request类中自己就会调用构造方法,但这里并不会将我们的传入的参数注册成为
$this->$_POST[xxx]
的形式,又因为后面回调那里会调用$this->$filter
来执行过滤操作,所以这里必须再次使用构造函数,将脏数据注册成为变量,因此我们根据method方法中会动态调用$this->{$this->method}($_POST)
的代码,再次调用构造函数,从而将脏数据注册成为php类中流转的变量,传入数据有server['XXXX']也是同理(具体一个点如下图:)
这里是input()方法中间的一行代码,它会获得已有的$filter的值 - 传入的变量会统一放在
param()
方法中通过input()
进行一一解析,并且调用设定好的filter值来调用对应函数来进行过滤,由于filter
已经有了值,这里就不会使用默认的filter
来调用,而会调用我们传入的值进行过滤(这里是system). input()
又会使用filterValue()
方法来获得过滤后的值(这里就是filter值控制调用函数的地方了),这里会将传入的所有数据都一一过滤一遍(也就是system一遍)- filterValue()中含有
call_user_func()
函数,而调用函数和传入数据为用户可控,当传入数据为call_user_func("system","系统命令")
时,这个漏洞便成功触发了,也就达成了命令执行.
参考文章:
ThinkPHP5 核心类 Request 远程代码漏洞分析