티스토리 뷰
이번 10월 21일에 열린 '더 해킹 챔피언십 주니어 2023'에 참여하여 본선에 진출하였다.
우리 팀은 2 문제를 풀고 간신히 본선에 진출할 수 있었고, 풀었던 문제와 아쉽게 못 푼 문제를 정리하겠다.
BBBBB (crypto)
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
BLOCK_SIZE = 16
KEY = os.urandom(BLOCK_SIZE)
try:
with open('flag.txt', 'rb') as f:
FLAG = f.read()
except:
FLAG = b'flag{test_flaggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg}'
def encrypt(data):
cipher = AES.new(KEY, AES.MODE_ECB)
return cipher.encrypt(pad(data, BLOCK_SIZE))
def main():
print("[ BBBBB Server!! ]")
while True:
print("1. Encrypt")
print("2. Bye")
choice = input("> ")
if choice == "1":
user_input = bytes.fromhex(input("Plaintext: "))
encrypted_data = encrypt(user_input + FLAG)
print(f"Encrypted data: {encrypted_data.hex()}")
elif choice == "2":
break
if __name__ == "__main__":
main()
AES의 ECB모드에서 발생할 수 있는 취약점인 Oracle Padding Attack을 이용하여 공격을 시도하였다.
(참고 자료: https://lactea.kr/entry/ECB-설명-및-취약점)
아주 아주 간단하게 설명하자면
user_input = 'AAAAAAAAAAAAAAAA', flag = 'namchun{tistory}'이고
user_input + flag를 암호화하려고 할 때, 블록의 크기가 16이라면
['AAAAAAAAAAAAAAAA'] ['namchun{tistory}'] 이런 식으로 암호화를 수행할 것이다.
하지만 user_input의 길이가 15라면?
['AAAAAAAAAAAAAAAn'] ['amchun{tistory}?'] 이런 식으로 flag의 첫 글자가 앞으로 밀려와서 user_input과 같이 암호화가 수행된다.
따라서 블록의 크기보다 작은 길이의 input을 넣었을 때 나온 값으로 브루트포싱하여 한 글자씩 얻을 수 있을 것이다.
from pwn import *
p = remote('3.39.87.239', 8713)
payload = b'61'*64
def sla(msg):
p.sendlineafter(b'1. Encrypt\n2. Bye\n> ', b'1')
p.sendlineafter(b'Plaintext: ', msg)
flag = b''
for i in range(63):
payload = payload[:-2]
sla(payload)
target = p.recvline()[16:-1]
for ct in range(32, 127):
test_payload = payload + flag + hex(ct)[2:].encode()
sla(test_payload)
encrypted_data = p.recvline()[16:-1]
if target[:128] == encrypted_data[:128]:
flag += hex(ct)[2:].encode()
print(bytes.fromhex(flag.decode()))
break
print(bytes.fromhex(flag.decode()))
flag{sometimes..._ECB_m0de_i5_vuln3rble!!_673214966bf6302c4ba3}
Hash-rain (crypto)
해당 문제는 내가 푼 것이 아닌 팀원분이 푸신 문제라 설명이 조금은 부족할 수도 있다.
#!/usr/bin/python3
import hashlib
import os
import random
import string
import signal
FLAG_FILE = "./flag.txt"
ROUNDS = 50
TIME_LIMIT = 100
def alarm_handler(signum, frame):
print("Time's up! You didn't find the correct input in time.")
exit(0)
def generate_random_hex_string(length):
return ''.join(random.choice('0123456789abcdef') for _ in range(length))
def calculate_md5_hash(input_string):
return hashlib.md5(input_string.encode()).hexdigest()
def main():
signal.signal(signal.SIGALRM, alarm_handler)
signal.alarm(250)
for round_num in range(1, ROUNDS + 1):
if round_num == ROUNDS:
print("Congratulations! You've completed all rounds.")
os.system("cat ./flag.txt")
break
print(f"Round {round_num}/{ROUNDS}")
answer = generate_random_hex_string(6)
print(f"Find an input whose MD5 hash ends with: {answer}")
input_string = input("Your Input : ").strip()
md5_hash = calculate_md5_hash(input_string)
if md5_hash[-6:] == answer:
print(f"Correct!")
continue
else:
print("Nop!")
break
if __name__ == "__main__":
main()
이 문제의 취약점은 MD5 해시의 마지막 6자리만을 사용하여 검증한다는 점에 있다.
이로 인해 충돌의 가능성이 상당히 높아졌고, 다양한 입력값이 동일한 MD5 해시의 마지막 6자리를 가질 수 있어서 결국 브루트포스로 문제를 해결할 수 있었다.
해결 방법은 가능한 모든 7자리 16진수 문자열에 대하여 미리 MD5 해시를 계산하고, 그 결과의 마지막 6자리를 키로, 원래 문자열을 값으로 하는 딕셔너리를 생성한다.
(내가 이 문제를 풀면서 타임아웃 때문에 골치가 아팠었는데 해시를 미리 계산해놓는 방법이 있었다 👍)
import hashlib
import itertools
# 모든 경우의 수를 미리 계산
def precompute_hashes():
chars = '0123456789abcdef'
precomputed = {}
total_combinations = len(chars) ** 7
completed = 0
for combo in itertools.product(chars, repeat=7):
completed += 1
percentage = (completed / total_combinations) * 100
print(f"Progress: {percentage:.2f}%", end='\r')
candidate = ''.join(combo)
md5_hash = hashlib.md5(candidate.encode()).hexdigest()
suffix = md5_hash[-6:]
precomputed[suffix] = candidate
print() # Newline for cleaner output
return precomputed
precomputed_hashes = precompute_hashes()
def find_answer(target_suffix):
return precomputed_hashes.get(target_suffix, None)
# 사용 예
for i in range(50):
target_suffix = input(f"Round {i + 1}/50\nFind an input whose MD5 hash ends with: ")
answer = find_answer(target_suffix)
if answer is not None:
print(f"Your Input : {answer}")
else:
print("No match found!")
(팀원분의 코드)
못 푼 문제 - 방탈출 (misc)
이 문제는 step1과 step2로 나뉘어져 있었는데, step1에서는 10000개의 이미지 파일을 주고 OCR을 통해 제대로 된 KEY를 알아내는 방식이었다.
이미지는 대부분 이런 식으로 되어 있었는데, key의 형식을 몰랐었기에 우선 정규표현식을 이용하여서
"영어대소문자3글자{영어숫자특수문자18글자}"로 파일들을 걸러내었고 나온 문자열들에서 다시 특수문자의 허용범위를 줄여가며 key를 얻을 수 있었다.
하지만 python의 ocr 모듈은 믿을만 하지 못하였는데, 0을 O로 읽거나, 1을 l로 읽는 등 수준이 높지 못한 모습을 보여주었다. 이 때문에 key를 출력하는 것이 아닌 filepath를 출력하여 직접 key를 찾아내었다.
step2에서는 간단한 flask 서버 문제가 있었다.
from flask import Flask, request, redirect, render_template, session
import hashlib
import secrets
SECRET_MD5 = "8f4551d89079c8c02877be799c96e162"
app = Flask(__name__)
app.secret_key = secrets.token_urlsafe(16)
flag = open("/app/FLAG", "r")
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
user_input = request.form.get('key')
hashed_input = hashlib.md5(user_input.encode()).hexdigest()
print(hashed_input)
if hashed_input == SECRET_MD5:
session["key"] = user_input
return redirect('/read_file')
else:
return render_template('403.html', message="KEY를 찾고 오렴"), 403
return render_template('index.html')
@app.route('/read_file', methods=['GET', 'POST'])
def read_file():
if 'key' not in session:
return redirect('/')
if request.method == 'POST':
file_path = request.form.get('file_path')
if len(file_path) > 13:
return render_template('403.html', message="너무 길어"), 403
deny_list = ['app', 'flag']
for k in deny_list:
if k in file_path.lower():
return render_template('403.html', message="FLAG를 설마 내가 쉽게 줄까봐?"), 403
try:
with open(file_path, 'r') as file:
content = file.read()
return content
except Exception as e:
return str(e)
return render_template('file.html')
@app.route('/flag', methods=['GET'])
def last_step():
if flag.read() == request.args.get("flag"):
return "방 탈출 성공!"
else:
return "방 탈출 실패! 좀 더 노력해보자"
if __name__ == "__main__":
app.run(host="0.0.0.0")
flag는 /app/flag에 위치해있고 입력받은 file_path에 'app'과 'flag'가 있다면 403을 뱉어낸다.
다른 문제들에서는 ../을 필터링하거나, replace를 우회하는 것만 있었지만 이것은 flag라는 문자가 들어가면 안 되는 문제였다.
도저히 생각이 안 나서 대회가 끝나고 웹해킹 고수 선배에게 물어보니
고민도 없이 바로 답을 알려주셨다.
리눅스의 File Descriptors를 이용하면 프로그램이 실행 중에 사용하는 파일에 할당된 리소스 번호를 통해 해당 파일에 접근할 수 있게 된다.
문제에서도 flag 파일을 open 해놓고 있었다;
은근히 난이도가 있는? 그런 대회였고 배운 것도 많은 대회였다고 생각한다. 11월 9일에 있는 본선에서도 괜찮은 성적을 거두었으면 좋겠다.
필력이 쓰레기 같지만 끝까지 봐주셔서 감사합니다.
'CTF' 카테고리의 다른 글
JBUCTF 2023 문제풀이 및 후기 (1) | 2023.11.01 |
---|---|
27th Hacking Camp 후기 (0) | 2023.09.04 |
순천향대학교 제 21회 정보보호 페스티벌 본선 풀이 및 후기 (1) | 2023.08.29 |
순천향대학교 제 21회 정보보호 페스티벌 예선전 풀이 및 후기 (1) | 2023.08.28 |
2022 MetaRed CTF (0) | 2022.11.13 |
- Total
- Today
- Yesterday
- 디스코드 봇
- solidity
- Write up
- 이세돌
- 정렬
- smart contract
- 오블완
- ctftime
- 코딩테스트
- 이더넛
- YISF
- blockchain
- web3
- Ethernaut
- CTF
- Los
- 디지털 포렌식
- forensic
- 티스토리챌린지
- 워게임
- Crypto
- misc
- Python
- Ethereum
- 웹해킹
- 파이썬
- Lord of SQL injection
- 프로그래머스
- writeup
- 알고리즘
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |