Diggid's Blog

[蚁剑改造]给php-fpm绕过disabled_functions增加对话函数选择

字数统计: 2.2k阅读时长: 8 min
2021/06/17 Share

前言

之前写过蓝帽杯的One Pointer那题,那题绕过disabled_functions(下文全称DF)利用php-fpm来打,而对话fpm的方式是利用FTP的passive mode,但是看了这位师傅的wp,发现对话fpm的fsockopen被禁了,但是pfsockopen没有被禁,而且在那位师傅的文章中也提到了是改蚁剑的fsockopen为pfsockopen来实现对话fpm的。因此就萌生了尝试改造蚁剑的想法,文章比较水,就是记录一下改造过程和一些坑的地方。顺便梳理以下蚁剑php-fpm的整个攻击流程是怎样的

对话php-fpm

首先我们知道php-fpm有两种模式,一种是tcp,另一种的unix,这就涉及到一些即用的直接对话函数和一些间接发恶意bin的方式

fsockopen

支持tcp和unix。初始化一个套接字连接到指定主机(hostname)。返回一个文件句柄,之后可以被其他文件类函数调用,如fgets等

1
2
3
4
5
6
7
fsockopen(
string $hostname,
int $port = -1,
int &$errno = ?,
string &$errstr = ?,
float $timeout = ini_get("default_socket_timeout")
): resource

常见用法:

1
2
fsockopen("127.0.0.1", 9001, $errno, $errstr, 5); //tcp
fsockopen("unix:///var/run/php7.4-fpm.sock", -1, $errno, $errstr, 5); //unix

pfsockopen

和fsockopen基本一样,只不过该连接建立的是持久连接,即只需初始化一次连接,当在脚本执行完后,连接一直不会关闭。

stream_socket_client

也支持tcp/udp和unix

1
2
3
4
5
6
7
8
stream_socket_client(
string $remote_socket,
int &$errno = ?,
string &$errstr = ?,
float $timeout = ini_get("default_socket_timeout"),
int $flags = STREAM_CLIENT_CONNECT,
resource $context = ?
): resource
  • $remote_socket:如果是tcp的方式,则需要传入<tcp/udp>://<host>:<port>,unix的话就和上面一样

常见用法:

1
2
stream_socket_client('tcp://127.0.0.1:9001', $errorno, $errorstr, 5);
stream_socket_client("unix:///var/run/php7.4-fpm.sock", $errorno, $errorstr, 5);

SSRF + Gopher

如果提供了SSRF的接口,那么可以直接利用gopher配合ssrf来发送恶意bin,这时我们要改造一下php-fpm的利用脚本,改为输出恶意bin而不直接进行连接或者Gopherus生成,具体可以参考我的这篇文章

SSRF + FTP Passive Mode

FTP Passive Mode可以让服务器访问我们伪造的ftp客户端,然后伪造的客户端欺骗服务器将数据打在php-fpm的服务上,同样实现了恶意bin的发送。生成方式还是在上面的文章中

浅析蚁剑php-fpm攻击结构 & 流程

目录结构

其实蚁剑的攻击方式很简单,关于绕过DF具体的逻辑在[蚁剑安装目录]/antData/plugins/as_bypass_php_disable_functions-master/,简单介绍一下目录结构

  • index.js:初始化入口,定义窗口、插件、语言库,新增其他功能选项(插件)的话可以在这里修改

  • payload.js:集成了绕过DF的几种方法所需要的payload,包括FastCgiClientProxyScriptProxyScriptFsock等,在需要的时候取出相应的payload或者上传相应的payload到已连接的shell上

  • ext目录:包含了恶意so、dll,用于LD_PRELOAD、php-fpm等需要利用恶意扩展来RCE的地方。

  • core目录:核心攻击目录,所有攻击方式的逻辑都在core的子目录中,因此我们要改造php-fpm的话,就关注core/php-fpm这个库

    • core/base.js:Base类,所有攻击方式类的一个父类(可以理解为一个接口),包含generateExt(生成扩展)、uploadProxyScript(上传代理脚本)、CompVersion(比较版本)这几个方法

    • 其他目录:各个攻击模块

  • language:语言依赖,和模块UI的定义,比如zh.js,以对象的方式存储各个模块显示在UI上的条目及描述说明,供各个攻击模块类在初始化的时候获取

language/zh.js:

image-20210617150202292

core/php-fpm/index.js:

image-20210617144828262

image-20210617150406777

攻击流程

我们重点关注一下core/php-fpm这个目录,就只有一个index.js文件,包含了所有逻辑,其他依赖的so和payload都从上面提到的文件中获取。

  • 开头获取一些依赖

image-20210617143633267

  • createForm方法初始化构建UI,因此我们要新增选项的话就在这里改

image-20210617143847162

image-20210617143903202

  • exploit方法,完整的攻击逻辑,先从createForm方法中初始化好的Form中获取一些攻击参数的信息,如TCP/Unix地址,PHP可执行文件的位置、对话php-fpm的函数选择(就是本次改造的地方)

整个攻击流程:

  1. 获取攻击参数信息:TCP/Unix地址,PHP可执行文件的位置、对话php-fpm的函数
  2. 生成并上传扩展至可写目录(恶意so或dll),往扩展里面注入的命令如下,该命令用于在目标某服务器的随机6000+端口上不使用php.ini(默认配置,无DF和open_basedir)起一个web服务,启动成功后我们访问该web服务器就没有DF限制了
1
2
3
4
5
6
7
let cmd = `${phpbinary} -n -S 127.0.0.1:${port} -t ${self.top.infodata.phpself}`;
相当于
php -n -S 127.0.0.1:<随机合法port> -t /var/www/html
参数:
-S 在<host>:<port>上起一个web服务,即随机端口起一个相同的php web server
-n 不使用php.ini配置
-t 指定root目录,根据open_basedir的情况获取,一般就是/var/www/html
  1. 从payload.js获取并生成payload(最常见的那个打fpm的php版本payload),然后执行payload,并验证payload是否执行成功,若执行成功,则继续上传.antproxy.php的代理脚本,若不成功,则回显失败不上传代理脚本。验证方式就是利用连接函数去测试是否成功开启了web服务
  2. 得到.antproxy.php脚本后,该脚本相当于一个代理,我们可以新增一个条目,选择.antproxy.php作为shell文件,该文件构造访问原先shell.php文件的数据包,然后发送给新起的php服务器,然后接受数据。相当于代理访问了shell.php文件(但此时的服务器是没有DF的服务器),连接密码还是原先的密码。

image-20210617153319247

增加对话php-fpm函数的选项

最终UI效果:

image-20210617153919768

增加UI

根据前面所说的几个点,我们大致知道了UI的位置,剩下就是ctrl + F来试了23333

  • language/zh.js:给form增加一个func选项

image-20210617154140091

  • core/php-fpm/index.js:给新增的func选项增加值

image-20210617154302835

  • payload.js:给侧边的DF(函数支持)检测增加pfsockopenstream_socket_client两个选项

找到这句代码,加上要检测的值即可。

1
$func_arr = array("fsockopen", "pfsockopen", "stream_socket_client", "iconv", "dl", "putenv", "error_reporting", "error_log", "file_put_contents", "file_get_contents", "fopen", "fclose", "fwrite", "tempnam", "imap_open", "symlink", "curl_init");

对话函数选择

这一部分其实可以自由发挥,毕竟知道了结构之后咋写都可以,但这里也有蛮多坑的,因为攻击流程中有很多地方用到对话连接函数,如果改漏了一个,那整个流程可能就会失败。按逻辑来说,就从php-fpm/index.js的exploit方法部分开始改,下面修改几处用到连接函数的地方:

  • 第一处:payload.js的FastCgiClient方法,该方法就是攻击php-fpm的payload,原先的逻辑是根据$_persistentSocket来选择是否长连接(pfsockopen),这里就随便改一下

image-20210617170858590

image-20210617175351148

  • 第二处:php-fpm/index.php,由于fosockopen和stream_socket_client的函数参数不一样,所以还要判断一下

image-20210617170339640

  • 第三处:payload.js的ProxyScriptFsock方法,这个方法的内容就是.antproxy.php文件的内容,同样修改一下连接函数

image-20210617170616339

image-20210617170622757

测试 & 出错可能原因

就拿蓝帽杯那题来测一下,在连上蚁剑之后,选择pfsockopen可以一键打通。但是会发现,随着测试次数的增多,大部分情况下却打不通了,之前一直以为是脚本修改哪里出了问题,但经过多次测试,几乎第一次都是能打通的,后面几次就随机通,猜测可能是端口被占用无法开启php服务或者Nginx连接数限制的原因。这里测完第一个后,拿着.antproxy.php重开一个shell,这个shell就没有任何限制了,这里就顺手拿fsockopen、stream_socket_client测试一下。都可以成功(还是概率问题)

总结

蚁剑的整体结构大致了解了一些,改起来还是比较简单的,如果打不通,可以考虑以下几项

  • 修改的地方是否出错,可以打印相关参数来看和对比一下
  • 修改连接函数的地方是否出错,这是大概率payload打不通的原因
  • 要打的端口被占了,概率问题,多试几次。
CATALOG
  1. 1. 前言
  2. 2. 对话php-fpm
    1. 2.1. fsockopen
    2. 2.2. pfsockopen
    3. 2.3. stream_socket_client
    4. 2.4. SSRF + Gopher
    5. 2.5. SSRF + FTP Passive Mode
  3. 3. 浅析蚁剑php-fpm攻击结构 & 流程
    1. 3.1. 目录结构
    2. 3.2. 攻击流程
  4. 4. 增加对话php-fpm函数的选项
    1. 4.1. 增加UI
    2. 4.2. 对话函数选择
    3. 4.3. 测试 & 出错可能原因
  5. 5. 总结