thinkphp5.0.23-RCE粗糙分析+复现
Ebounce
撰写于 2019年 09月 14 日

前记


说好了每周一篇代码审计,虽然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的地方,并且在传入数据都是脏数据的情况下.
怀疑点有二:

  1. 此处传入了filter和data数据
  2. 本身上面的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值实际上是一个入口的问题):

  3. 原本调用的Request类中自己就会调用构造方法,但这里并不会将我们的传入的参数注册成为$this->$_POST[xxx]的形式,又因为后面回调那里会调用$this->$filter来执行过滤操作,所以这里必须再次使用构造函数,将脏数据注册成为变量,因此我们根据method方法中会动态调用$this->{$this->method}($_POST)的代码,再次调用构造函数,从而将脏数据注册成为php类中流转的变量,传入数据有server['XXXX']也是同理(具体一个点如下图:)

    这里是input()方法中间的一行代码,它会获得已有的$filter的值
  4. 传入的变量会统一放在param()方法中通过input()进行一一解析,并且调用设定好的filter值来调用对应函数来进行过滤,由于filter已经有了值,这里就不会使用默认的filter来调用,而会调用我们传入的值进行过滤(这里是system).
  5. input()又会使用filterValue()方法来获得过滤后的值(这里就是filter值控制调用函数的地方了),这里会将传入的所有数据都一一过滤一遍(也就是system一遍)
  6. filterValue()中含有call_user_func()函数,而调用函数和传入数据为用户可控,当传入数据为call_user_func("system","系统命令")时,这个漏洞便成功触发了,也就达成了命令执行.
    参考文章:
    ThinkPHP5 核心类 Request 远程代码漏洞分析

thinkphp5.0.23-RCE粗糙分析+复现

前记


说好了每周一篇代码审计,虽然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的地方,并且在传入数据都是脏数据的情况下.
怀疑点有二:

  1. 此处传入了filter和data数据
  2. 本身上面的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值实际上是一个入口的问题):

  3. 原本调用的Request类中自己就会调用构造方法,但这里并不会将我们的传入的参数注册成为$this->$_POST[xxx]的形式,又因为后面回调那里会调用$this->$filter来执行过滤操作,所以这里必须再次使用构造函数,将脏数据注册成为变量,因此我们根据method方法中会动态调用$this->{$this->method}($_POST)的代码,再次调用构造函数,从而将脏数据注册成为php类中流转的变量,传入数据有server['XXXX']也是同理(具体一个点如下图:)

    这里是input()方法中间的一行代码,它会获得已有的$filter的值
  4. 传入的变量会统一放在param()方法中通过input()进行一一解析,并且调用设定好的filter值来调用对应函数来进行过滤,由于filter已经有了值,这里就不会使用默认的filter来调用,而会调用我们传入的值进行过滤(这里是system).
  5. input()又会使用filterValue()方法来获得过滤后的值(这里就是filter值控制调用函数的地方了),这里会将传入的所有数据都一一过滤一遍(也就是system一遍)
  6. filterValue()中含有call_user_func()函数,而调用函数和传入数据为用户可控,当传入数据为call_user_func("system","系统命令")时,这个漏洞便成功触发了,也就达成了命令执行.
    参考文章:
    ThinkPHP5 核心类 Request 远程代码漏洞分析

评论区(暂无评论)

这里空空如也,快来评论吧~

我要评论