[Dreamhack] Web Hacking STAGE 3
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 변수가 요청에 포함된 쿠키에 의해 결정되어 문제가 발생한다.
서버는 별다른 검증없이 이용자 요청에 포함된 쿠키를 신뢰하고, 이용자 인증 정보를 식별하기 때문에 공격자는 쿠키에 타 계정 정보를 삽입해 계정을 탈취할 수 있다.