Web Hacking/Dreamhack

[Dreamhack] Web Hacking STAGE 3

SolB 2022. 7. 13. 23:04

STAGE 3: Cookie & Session

Cookie & Session

 

> Background : Cookie & Session

쿠키

: Key와 Value로 이뤄진 일종의 단위

 ⋅ 서버와 클라이언트에게 쿠키를 발급하면, 클라이언트는 서버에 요청을 보낼 때마다 쿠키를 같이 전송한다. 

 ⋅ 서버는 클라이언트의 요청에 포함된 쿠키를 확인해 클라이언트를 구분할 수 있다.

클라이언트의 IP 주소와 User-Agent는 매번 변경될 수 있는 고유하지 않은 정보일 뿐만 아니라, HTTP 프로토콜의 
Connectionless와 Stateless 특징 때문에 웹 서버는 클라이언트를 기억할 수 없다. 

- Connectionless : 하나의 요청에 하나의 응답을 한 후 연결을 종료하는 것
 ⋅ 특정 요청에 대한 연결은 이후의 요청과 이어지지 않고 새 요청이 있을 때 마다 항상 새로운 연결을 맺는다.

- Stateless : 통신이 끝난 후 상태 정보를 저장하지 않는 것
 ⋅ 이전 연결에서 사용한 데이터를 다른 연결에서 요구할 수 없다.

> 용도

- 정보 기록

 ⋅ 웹 서버는 각 클라이언트의 팝업 옵션( ex | "다시 보지 않기", "7 일간 표시하지 않기" )을 기억하기 위해 쿠키에 해당 정보를 기록하고, 쿠키를 통해 팝업 창 표시 여부를 판단한다.

 ⋅ 쿠키는 서버와 통신할 때마다 전송되기 때문에 쿠키가 필요없는 요청을 보낼 때 리소스 낭비가 발생할 수 있다.

-> 이를 보완하기 위해 Mordern Storage APIs를 통해 데이터를 저장하는 방식을 권장하고 있다.

 

- 상태 정보

 ⋅ 웹 서버에서는 수많은 클라이언트의 로그인 상태와 이용자를 구별해야 하는데, 이때 클라이언트를 식별할 수 있는 값을 쿠키에 저장해 사용한다.

 

> 쿠키가 없는 통신

서버는 요청을 보낸 클라이언트가 누군지 알 수 없기에 현재 어떤 클라이언트와 통신하는지 알 수 없다.

 

> 쿠키가 있는 통신

클라이언트는 서버에 요청을 보낼 때마다 쿠키를 포함하고, 서버는 해당 쿠키를 통해 클라이언트를 식별한다.

 

> 쿠키 변조

- 쿠키는 클라이언트의 브라우저에 저장되고 요청에 포함되는 정보이다. 따라서 악의적인 클라이언트는 쿠키 정보를 변조에 서버에 요청을 보낼 수 있다. 

- 서버가 별다른 검증없이 쿠키를 통해 이용자의 인증정보를 식별한다면, 공격자가 타 이용자를 사칭한 정보 탈취가 가능하다.

 

세션

 : 쿠키에 인증상태를 저장하지만 클라이언트가 인증 정보를 변조할 수 없게 하기 위해 사용하는 것

 ⋅ 세션은 인증정보를 서버에 저장하고 해당 데이터에 접근할 수 있는 키(Session ID ; 유추 가능한 랜덤 문자열)를 만들어 클라이언트에 전달하는 방식으로 작동

 ⋅ 브라우저는 해당 키를 쿠키에 저장하고 이후에 HTTP 요청을 보낼 때 사용한다. 서버는 요청에 포함된 키에 해당하는 데이터를 가져와 인증 상태를 확인한다.

 ⋅ 쿠키는 데이터 자체를 이용자가 저장하며, 세션은 서버가 저장한다.

 

 

Same Origin Policy

> Mitigation : Same Origin Policy

Same Origin Policy (SOP)

: 웹 브라우저 보안을 위해 프로토콜, 호스트, 포트가 동일한 서버로만 요청을 주고 받을 수 있도록 한 동일 출처 정책

 ⋅ Cross Origin이 아닌 Same Origin일 때만 정보를 읽을 수 있다.

 

- Origin

 : 프로토콜, 포트, 호스트로 구성, 구성요소가 모두 일치해야 동일한 오리진이라고 함

 

- Same Orgin Policy 데모

<!-- iframe 객체 생성 -->
<iframe src="" id="my-frame"></iframe>
<!-- Javascript 시작 -->
<script>
/* 2번째 줄의 iframe 객체를 myFrame 변수에 가져옵니다. */
let myFrame = document.getElementById('my-frame')
/* iframe 객체에 주소가 로드되는 경우 아래와 같은 코드를 실행합니다. */
myFrame.onload = () => {
    /* try ... catch 는 에러를 처리하는 로직 입니다. */
    try {
        /* 로드가 완료되면, secret-element 객체의 내용을 콘솔에 출력합니다. */
        let secretValue = myFrame.contentWindow.document.getElementById('secret-element').innerText;
        console.log({ secretValue });
    } catch(error) {
        /* 오류 발생시 콘솔에 오류 로그를 출력합니다. */
        console.log({ error });
    }
}
/* iframe객체에 Same Origin, Cross Origin 주소를 로드하는 함수 입니다. */
const loadSameOrigin = () => { myFrame.src = 'https://same-origin.com/frame.html'; }
const loadCrossOrigin = () => { myFrame.src = 'https://cross-origin.com/frame.html'; }
</script>
<!--
버튼 2개 생성 (Same Origin 버튼, Cross Origin 버튼)
-->
<button onclick=loadSameOrigin()>Same Origin</button><br>
<button onclick=loadCrossOrigin()>Cross Origin</button>
<!--
frame.html의 코드가 아래와 같습니다.
secret-element라는 id를 가진 div 객체 안에 treasure라고 하는 비밀 값을 넣어두었습니다.
-->
<div id="secret-element">treasure</div>

 

Cross Origin Resource Sharing (CORS)

: SOP를 적용받지 않고 리소스를 공유하기 위한 교차 출처 리소스 공유 방법

 ⋅ HTTP 헤더에 기반하여 Cross Origin 간에 리소스를 공유하는 방법

 ⋅ 발신측에서 CORS 헤더를 설정해 요청하면, 수신측에서 헤더를 구분해 정해진 규칙에 맞게 데이터를 가져갈 수 있도록 설정한다.

Access-Control-Allow-Origin 헤더 값에 해당하는 Origin에서 들어오는 요청만 처리
Access-Control-Allow-Methods 헤더 값에 해당하는 메소드의 요청만 처리
Access-Control-Allow-Credentials 쿠키 사용 여부를 판단
Access-Control-Allow-Headers 헤더 값에 해당하는 헤더의 사용 가능 여부를 나타냄

 

JSON with Padding (JSONP)

: script 태그로 Cross Origin의 데이터를 불러옴

 ⋅ Callback 함수 활용

 

 

[혼자 실습] Session-basic

#!/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
}


# this is our session storage 
session_storage = {
}


@app.route('/')
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        # get username from session_storage 
        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:
            # you cannot know admin's pw 
            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(32).hex()
            session_storage[session_id] = username
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'


@app.route('/admin')
def admin():
    # what is it? Does this page tell you session? 
    # It is weird... TODO: the developer should add a routine for checking privilege 
    return session_storage


if __name__ == '__main__':
    import os
    # create admin sessionid and save it to our storage
    # and also you cannot reveal admin's sesseionid by brute forcing!!! haha
    session_storage[os.urandom(32).hex()] = 'admin'
    print(session_storage)
    app.run(host='0.0.0.0', port=8000)

 

http://host3.dreamhack.games:13917 주소 뒤에 /admin을 붙여주었더니다음과 같은 문구가 나왔다.

 

admin에 해당하는 값을 sessionid의 value값으로 넣어주고 새로고침을 해주었다.

 

다음과 같이 flag를 획득할 수 있었다.

 

[함께실습] Cookie

- index 페이지 코드

@app.route('/') # / 페이지 라우팅 
def index():
    username = request.cookies.get('username', None) # 이용자가 전송한 쿠키의 username 입력값을 가져옴
    if username: # username 입력값이 존재하는 경우
        return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not admin"}') # "admin"인 경우 FLAG 출력, 아닌 경우 "you are not admin" 출력
    return render_template('index.html')

username이 admin일 경우 FLAG를 출력한다.

 

- login 페이지 코드

@app.route('/login', methods=['GET', 'POST']) # login 페이지 라우팅, GET/POST 메소드로 접근 가능
def login():
    if request.method == 'GET': # GET 메소드로 요청 시
        return render_template('login.html') # login.html 페이지 출력
    elif request.method == 'POST': # POST 메소드로 요청 시
        username = request.form.get('username') # 이용자가 전송한 username 입력값을 가져옴
        password = request.form.get('password') # 이용자가 전송한 password 입력값을 가져옴
        try:
            pw = users[username] # users 변수에서 이용자가 전송한 username이 존재하는지 확인
        except: 
            return '<script>alert("not found user");history.go(-1);</script>' # 존재하지 않는 username인 경우 경고 출력
        if pw == password: # password 체크
            resp = make_response(redirect(url_for('index')) ) # index 페이지로 이동하는 응답 생성
            resp.set_cookie('username', username) # username 쿠키 설정
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>' # password가 동일하지 않은 경우

GET : username과 password를 입력할 수 있는 로그인 페이지 제공

POST : 이용자가 입력한 username과 password 입력 값을 users 변숫값과 비교

 

- users 변수 선언

try:
    FLAG = open('./flag.txt', 'r').read() # flag.txt 파일로부터 FLAG 데이터를 가져옴.
except:
    FLAG = '[**FLAG**]'
users = {
    'guest': 'guest',
    'admin': FLAG # FLAG 데이터를 패스워드로 선언
}

손님 계정의 비밀번호는 guest, 관리자 계정의 비밀번호는 파일에서 읽어온 FLAG이다.

 

> 취약점 분석

이용자의 계정을 나타내는 username 변수가 요청에 포함된 쿠키에 의해 결정되어 문제가 발생한다. 

서버는 별다른 검증없이 이용자 요청에 포함된 쿠키를 신뢰하고, 이용자 인증 정보를 식별하기 때문에 공격자는 쿠키에 타 계정 정보를 삽입해 계정을 탈취할 수 있다.