浅谈几种Bypass disable_functions的方法
/Bypass disable_functions的方法有很多,这里先将一些常用的方法小结一下,后面会继续补充各种方法。
0x01 disable_functions
disable_functions是php.ini中的一个设置选项,可以用来设置PHP环境禁止使用某些函数,通常是网站管理员为了安全起见,用来禁用某些危险的命令执行函数等。
我们查看phpinfo的时候,会发现有这么一栏,我本地这里默认是未设置的:
要更改的话打开php.ini,找到对应的行修改即可,这里修改如下:
1 | ; This directive allows you to disable certain functions for security reasons. |
注意:eval并非PHP函数,放在disable_functions中是无法禁用的,若要禁用需要用到PHP的扩展Suhosin。
然后重启服务,在phpinfo中可以看到已经生效:
下面就逐一对各种方法进行简单的归纳。
0x02 黑名单绕过
我们知道,disable_functions其实是一个黑名单机制,我们可以通过观察是否存在可利用的漏网之鱼,直接通过其实现绕过即可。
……
0x03 利用Windows系统组件COM绕过
Windows系统组件COM在Windows默认就存在,是位于System32目录下的wshom.ocx文件。
环境配置
先在php.ini中查看是否已经开启com.allow_dcom,若未开启则将前面的;分号去掉:
1 | ; allow Distributed-COM calls |
然后在php/ext/里面查找是否存在php_com_dotnet.dll这个文件。
再到php.ini中查看是否存在extension=php_com_dotnet.dll这项,有的话去掉注释开启,否则直接添加上去即可。
重启服务,在phpinfo中查看是否开启了:
Bypass
前提通过phpinfo知道disable_functions选项如下:
1 | disable_functions = exec,system,passthru,shell_exec,proc_open,popen,dl, |
此时在Web服务器中写入任意的以上函数的WebShell都是无法执行命令的。
上传comshell.php至Web服务器中:
1 |
|
这里创建一个COM对象,然后通过调用COM对象的exec()方法来实现执行系统命令,从而绕过disable_functions禁用PHP命令执行函数的限制:
防御方法
彻底的解决方案是直接删除System32目录下wshom.ocx文件。
0x04 利用LD_PRELOAD绕过
LD_PRELOAD是Linux中的环境变量,可以设置成一个指定库的路径,动态链接时较其他库有着更高的优先级,允许预加载指定库中的函数和符号覆盖掉后续链接的库中的函数和符号。即可以通过重定向共享库函数来进行运行时修复。这项技术可用于绕过反调试代码,也可以用作用户机rootkit。
Method1——劫持getuid()
基本原理
前提是在Linux中已安装并启用sendmail程序。
php的mail()函数在执行过程中会默认调用系统程序/usr/sbin/sendmail,而/usr/sbin/sendmail会调用getuid()。如果我们能通过LD_PRELOAD的方式来劫持getuid(),再用mail()函数来触发sendmail程序进而执行被劫持的getuid(),从而就能执行恶意代码了。
细化一下:
- 编写一个原型为 uid_t getuid(void); 的 C 函数,内部执行攻击者指定的代码,并编译成共享对象 evil.so;
- 运行 PHP 函数 putenv(),设定环境变量 LD_PRELOAD 为 evil.so,以便后续启动新进程时优先加载该共享对象;
- 运行 PHP 的 mail() 函数,mail() 内部启动新进程 /usr/sbin/sendmail,由于上一步 LD_PRELOAD 的作用,sendmail 调用的系统函数 getuid() 被优先级更好的 evil.so 中的同名 getuid() 所劫持;
- 达到不调用 PHP 的各种命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。
调用过程分析
先调用如下命令可查看sendmail程序可能调用的系统API明细:
1 | root@sq:/tmp# readelf -Ws /usr/sbin/sendmail | grep getuid |
或如下命令也可:
1 | root@sq:/tmp# nm -D /usr/sbin/sendmail 2>&1 | grep getuid |
由于程序运行时会根据命令行选项、运行环境作出不同反应,导致真正运行时调用的API可能只是readelf查看的子集,通过如下命令跟踪查看sendmail程序的实际API调用情况:
1 | root@sq:/tmp# strace -f /usr/sbin/sendmail 2>&1 | grep -A5 -B5 getuid |
可以看到,sendmail程序确实调用了getuid()函数。
接着用man 2 getuid
查看函数原型:
现在知道sendmail程序会调用getuid()函数以及getuid()函数的原型,剩下的问题就是寻找在PHP中除了那些命令执行函数外会调用sendmail程序的函数了。
sendmail程序,顾名思义,就是发送邮件的功能,由此自然而然地联想到PHP的mail()函数,写段测试代码:
1 |
|
然后运行如下命令查看mail()是否启动新进程:
1 | root@sq:/tmp# strace -f php mail.php 2>&1 | grep -A2 -B2 execve |
简单分析一下,第一个execve是启动PHP解释器而已,除此之外必须找到第二个execve,没有则说明并未启动新进程;这里第二和第三个execve都是直接或间接调用系统sendmail程序,这就对了。
注意一点,通过/bin/sh方式调用sendmail的execve,我们在看/bin/sh程序的调用哪些API时会发现,其实也是调用了getuid():
1 | root@sq:/tmp# strace -f /bin/sh 2>&1 | grep -A5 -B5 getuid |
也就是说,如果别的环境和我本地的一样,在mail()中存在启动execve调用了/bin/sh程序来间接调用sendmail的这种情况,即使目标系统未安装或未开启sendmail程序,我仍然可以通过PHP的mail()函数来触发调用了/bin/sh程序的execve,从而调用getuid()达到执行劫持函数的目的。
攻击利用
编写test.c,劫持getuid()函数,获取LD_PRELOAD环境变量并预加载恶意的共享库,再删除环境变量 LD_PRELOAD,最后执行由EVIL_CMDLINE环境变量获取的系统命令:
1 |
|
当这个共享库中的getuid()被调用时,尝试加载payload()函数执行命令。
接着用以下语句编译C文件为共享对象文件:
1 | gcc -shared -fPIC test.c -o test.so |
最后编写test.php:
1 |
|
这里接受3个参数,一是cmd参数,待执行的系统命令;二是outpath参数,保存命令执行输出结果的文件路径,便于在页面上显示,另外该参数,你应注意web是否有读写权限、web是否可跨目录访问、文件将被覆盖和删除等几点;三是sopath参数,指定劫持系统函数的共享对象的绝对路径。
这里通过putenv()函数将LD_PRELOAD环境变量设置为恶意的test.so、将自定义的EVIL_CMDLINE环境变量赋值为要执行的命令;然后调用mail()函数触发sendmail(),再通过sendmail()触发getuid()从而使恶意的test.so被加载执行;最后再输出内容到页面上并删除临时存放命令执行结果的文件。
访问test.php,输入相应的参数即可执行成功:
其实本地测试发现,即使Linux系统未安装或未启用sendmail,还是能够成功触发Bypass,这和前面分析的mail()会启动/bin/sh进而调用getuid()有关,验证了这种方法的特殊性。
Method2——劫持启动进程
基本原理
第一种方法是劫持getuid(),是较为常用的方法,但存在缺陷:
- 目标Linux未安装或为启用sendmail;
- 即便目标可以启用sendmail,由于未将主机名添加进hosts中,导致每次运行sendmail都要耗时半分钟等待域名解析超时返回,www-data也无法将主机名加入hosts;
回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了。这种场景与 C++ 的构造函数简直神似!
GCC 有个 C 语言扩展修饰符
__attribute__((constructor))
,可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行__attribute__((constructor))
修饰的函数。这一细节非常重要,很多朋友用 LD_PRELOAD 手法突破 disable_functions 无法做到百分百成功,正因为这个原因,不要局限于仅劫持某一函数,而应考虑拦劫启动进程这一行为。此外,我通过 LD_PRELOAD 劫持了启动进程的行为,劫持后又启动了另外的新进程,若不在新进程启动前取消 LD_PRELOAD,则将陷入无限循环,所以必须得删除环境变量 LD_PRELOAD。最直观的做法是调用
unsetenv("LD_PRELOAD")
,这在大部份 linux 发行套件上的确可行,但在 centos 上却无效,究其原因,centos 自己也 hook 了 unsetenv(),在其内部启动了其他进程,根本来不及删除 LD_PRELOAD 就又被劫持,导致无限循环。所以,我得找一种比 unsetenv() 更直接的删除环境变量的方式。是它,全局变量extern char** environ
!实际上,unsetenv() 就是对 environ 的简单封装实现的环境变量删除功能。
攻击利用
bypass_disablefunc.c
1 |
|
接着用以下语句编译C文件为共享对象文件:
1 | gcc -shared -fPIC bypass_disablefunc.c -o bypass_disablefunc.so |
bypass_disablefunc.php,代码和test.php一致:
1 |
|
访问bypass_disablefunc.php,输入参数设置LD_PRELOAD环境变量和要执行的命令的值,页面直接返回命令执行结果:
0x05 利用PHP 7.4 FFI绕过
FFI(Foreign Function Interface),即外部函数接口,允许从用户区调用C代码。简单地说,就是一项让你在PHP里能够调用C代码的技术。
当PHP所有的命令执行函数被禁用后,通过PHP 7.4的新特性FFI可以实现用PHP代码调用C代码的方式,先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions。
也就是说,通过PHP调用C的命令执行函数来绕过。
具体原理和利用可参考《从RCTF nextphp看PHP7.4的FFI绕过disable_functions》。
0x06 利用Bash破壳(CVE-2014-6271)漏洞绕过
前提条件
这种利用方法的前提是目标OS存在Bash破壳(CVE-2014-6271)漏洞,该漏洞的具体介绍可参考《破壳漏洞(CVE-2014-6271)综合分析:“破壳”漏洞系列分析之一 》。
在我本地的Metasploitable2虚拟机环境中,是存在Bash破壳漏洞的:
基本原理
假设目标OS存在Bash破壳漏洞后,我们再来看看PHP到底是哪些函数调用触发到了Bash博客漏洞的。
这里我们以mail()函数作为例子,当然其他一些函数也可以,原理一致,可以自行分析。
PHP的mail()函数用于发送邮件,提供了3个必选参数和2个可选参数:
1 | mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] ) : bool |
这里我们主要看最后一个参数,PHP官方手册上对最后一个参数的说明:
The additional_parameters parameter can be used to pass an additional parameter to the program configured to use when sending mail using the sendmail_path configuration setting. For example, this can be used to set the envelope sender address when using sendmail with the -f sendmail option.
The user that the webserver runs as should be added as a trusted user to the sendmail configuration to prevent a ‘X-Warning’ header from being added to the message when the envelope sender (-f) is set using this method. For sendmail users, this file is /etc/mail/trusted-users.
简单地说,就是这个参数可以通过添加附加的命令作为发送邮件时候的配置,比如使用-f参数可以设置邮件发件人等,官方文档在范例Example #3也有所演示,具体可以参考官方文档:http://php.net/manual/zh/function.mail.php。
为什么关注最后第五个参数呢?
我们看到在PHP mail()函数的源代码mail.c中,有如下代码片段,其中mial()函数的第五个参数即为extra_cmd:
1 | if (extra_cmd != NULL) { |
当extra_cmd(用户传入的一些额外参数)存在的时候,调用spprintf()将sendmail_path和extra_cmd组合成真正执行的命令行sendmail_cmd。 然后将sendmail_cmd丢给popen()执行:
1 |
|
如果系统默认sh是bash,popen()会派生bash进程,而我们刚才提到的CVE-2014-6271漏洞,直接就导致我们可以利用mail()函数执行任意命令,绕过disable_functions的限制。
同样,我们搜索一下php的源码,可以发现,明里调用popen派生进程的php函数还有imap_mail,如果你仅仅通过禁用mail函数来规避这个安全问题,那么imap_mail是可以做替代的。当然,php里还可能有其他地方有调用popen或其他能够派生bash子进程的函数,通过这些地方,都可以通过破壳漏洞执行命令的。也就是说,单单禁用mail()函数进行黑名单的防御,是很容易被Bypass的。
Bypass
Exp如下:
1 |
|
在我本地的Metasploitable2虚拟机测试,其存在Bash破壳漏洞,直接Bypass getshell:
防御方法
修补Bash破壳漏洞。
0x07 利用imap_open()绕过
环境配置
安装PHP的imap扩展:apt-get install php-imap
;在php.ini中开启imap.enable_insecure_rsh选项为On;重启服务。
成功配置好环境后,在phpinfo中会看到如下信息:
基本原理
PHP 的imap_open函数中的漏洞可能允许经过身份验证的远程攻击者在目标系统上执行任意命令。该漏洞的存在是因为受影响的软件的imap_open函数在将邮箱名称传递给rsh或ssh命令之前不正确地过滤邮箱名称。如果启用了rsh和ssh功能并且rsh命令是ssh命令的符号链接,则攻击者可以通过向目标系统发送包含-oProxyCommand参数的恶意IMAP服务器名称来利用此漏洞。成功的攻击可能允许攻击者绕过其他禁用的exec 受影响软件中的功能,攻击者可利用这些功能在目标系统上执行任意shell命令。利用此漏洞的功能代码是Metasploit Framework的一部分。
imap_open()
imap_open()函数定义如下:
1 | resource imap_open ( string $mailbox , string $username , string $password [, int $options = 0 [, int $n_retries = 0 [, array $params = NULL ]]] ) |
mailbox参数的值由服务器名和服务器上的mailbox文件路径所组成,INBOX代表的是当前用户的个人邮箱。比如,我们可以通过如下方式来设置mailbox参数:
1 | $mbox = imap_open ("{localhost:993/PROTOCOL/FLAG}INBOX", "user_id", "password"); |
在括号内的字符串中,我们可以看到服务器名称(或者IP地址)、端口号以及协议名称。用户可以在协议名后设置标志(第3个参数)。
在PHP官方文档中,关于imap_open参数的设置有如下一段警告内容:
根据警告信息,除非我们禁用了enable_insecure_rsh选项,否则不要将用户数据直接传输到mailbox参数中。
为什么官方文档会有这个警告信息呢?
简单地说,就是imap_open()函数会调用到rsh的程序,而该程序中会调用execve系统调用来实现rsh的调用,其中的邮件地址参数是由imap_open()函数的mailbox参数传入,同时,由于rsh命令是ssh命令的符号链接,所以当我们利用ssh的-oProxyCommand参数来构造恶意mailbox参数时就能执行恶意命令。
具体分析过程请参考《如何在PHP安装中绕过disable_functions》。
-oProxyCommand参数
ProxyCommand指定用于连接服务器的命令。
当我们执行以下这条命令时,可以发现即便我们没有建立与localhost的SSH连接,也会创建mi1k7ea文件:
1 | root@sq:/var/www/html# ssh -oProxyCommand="touch mi1k7ea" localhost |
至此,就知道了可以通过-oProxyCommand参数来执行系统命令。
但是我们不能直接将上述命令直接转移到PHP脚本来代替imap_open服务器地址,因为在解析时它会将空格解释为分隔符和斜杠作为标志。但是我们可以使用\$IFS这个shell变量来替换空格符号或使用\t替换。还可以在bash中使用Ctrl + V热键和Tab键插入标签。要想绕过斜杠,可以使用base64编码和相关命令对其进行解码。
Bypass
exp如下,先判断是否存在imap_open()函数,然后构造exp执行通过外部GET输入的命令然后保存结果到本地文件中,最后输出结果文件内容,注意sleep(5)是为了等imap_open()函数执行完、因为该函数执行时需要DNS轮询会存在延时:
1 |
|
当然,替换空格符的\t也可以换成$IFS$()
来Bypass掉。
此时环境中disable_functions配置如下:
访问,能够成功Bypass:
防御方法
- 设置imap.enable_insecure_rsh选项为Off;
- 可以的话禁用imap_open()函数;
0x08 利用pcntl插件绕过
前提条件
前提是PHP安装并启用了pcntl插件。
基本原理
原理比较简单,就是利用pcntl_exec()这个pcntl插件专有的命令执行函数来执行系统命令,从而Bypass黑名单。
Bypass
这里直接贴exp:
1 | #exec.php |
防御方法
disable_functions的黑名单中添加pcntl相关函数实现禁用。
参考
无需 sendmail:巧用 LD_PRELOAD 突破 disable_functions
PHP Webshell下命令执行限制及绕过disable_function方法总结
PHP Execute Command Bypass Disable_functions