-
[SISS/웹해킹 스터디] 2학기 2, 3주차 스터디24-2 SISS/웹해킹 2024. 9. 14. 19:30
2주차 09/09 ~ 09/15 [드림핵] Cookie & Session
쿠키
- 클라이언트의 IP 주소, User-Agent는 매번 변경될 수 있음
- HTTP 프로토콜은 Connectionless, Stateless함
- Connectionless → 한 요청 당 하나의 응답을 한 후 연결을 종료
- Stateless → 통신 종료 후 상태 정보를 저장하지 않음
→ 웹 서버는 클라이언트를 기억할 수 없으므로 쿠키를 이용
- 쿠키
- 클라이언트의 정보(인증 정보 포함)와 요청의 내용을 구체화하는 데이터
- Key + Value
- 이용
- (클라이언트) 서버에 요청 전송 시 쿠키를 함께 전송
- (서버) 쿠키를 통해 클라이언트 구분
- 용도
- 정보 기록 → 클라이언트 정보 저장 (팝업 다시 보지 않기 등)
- 상태 정보 → 로그인 상태 및 이용자 구별
- 쿠키 변조
- 변조된 쿠키를 검증 없이 사용할 경우 악의적인 정보 탈취 가능
세션
- 인증 정보를 변조하지 못하도록 함 (인증 상태는 쿠키에 저장)
- 이용자가 데이터를 저장하는 쿠키와 달리 서버에 데이터를 저장
- 인증 정보를 서버에 저장
- 세션 ID(정보 데이터에 접근할 수 있는 키)를 클라이언트에 전달
쿠키 및 세션 실습
- 쿠키
- 세션 → 이름 변경이 불가 (키 값으로 접근하므로)
- 실습
- 세션 키 확인하기 (Session Storage)
- 세션 키 입력하기
쿠키 적용법
- 쿠키 설정
- 서버 → 헤더(HTTP 응답)에 쿠키 설정 헤더를 추가하면 클라이언트의 브라우저가 쿠키를 설정
HTTP/1.1 200 OK Server: Apache/2.4.29 (Ubuntu) Set-Cookie: name=test; Set-Cookie: age=30; Expires=Fri, 30 Sep 2022 14:54:50 GMT; ...
- 클라이언트 → 자바스크립트를 사용해 쿠키 설정
document.cookie = "name=test;" document.cookie = "age=30; Expires=Fri, 30 Sep 2022 14:54:50 GMT;"
- 서버 → 헤더(HTTP 응답)에 쿠키 설정 헤더를 추가하면 클라이언트의 브라우저가 쿠키를 설정
- 쿠키 열람
- 크롬 콘솔 활용
- 페이지 우클릭 후 [Console] 탭을 누름
- document.cookie를 입력
- 크롬 애플리케이션 활용
- 페이지 우클릭 후 검사 버튼 누르기
- [Application] 탭에서 [Cookies]을 통해 쿠키 정보 확인
- 크롬 콘솔 활용
드림핵 세션을 통한 연습
- 로그인 응답 확인
- (로그인 페이지) 우클릭 후 검사 선택
- [Network] 탭의 Preserve log를 체크 (로그인 성공 시 응답을 볼 수 있음) → 응답의 set-cookie 헤더를 통해 브라우저의 쿠키에 세션 정보를 저장함을 확인
- 설정된 쿠키 확인
- 크롬 검사의 [Application] > [Cookies]의 주소(https://dreamhack.io)를 누를 경우 설정된 쿠키 확인 가능
- 저장된 세션 값 삭제
- (sessionid 헤더 값 메모장에 복사하기) sessionid 헤더 값을 메모장에 복사한 후 새로고침하면 로그아웃
- 세션 값 재설정
- 쿠키의 빈 칸에 sessionid 헤더 및 복사한 세션 값 입력 시 로그인
- 세션 하이재킹
- 이용자의 쿠키 탈취를 통해 세션에 해당하는 이용자의 인증 상태를 훔치는 것
퀴즈 1
실습 - 쿠키
- 웹 서비스 분석
- 엔드 포인트 → URL (API가 서버에서 자원에 접근할 수 있도록 하는)
- @app.route(’/’) → 인덱스 페이지
- 요청에 포함된 쿠키를 통해 이용자 식별 (문제에서는 username이 admin일 경우 플래그를 출력)
# 인덱스 페이지 코드 @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')
- @app.route('/login’) → 로그인 페이지
- GET → username과 password 입력 페이지 제공
- POST → 입력된 username, password를 users 변수의 값과 비교
# 로그인 페이지 코드 @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가 동일하지 않은 경우
# users 변수 선언 try: FLAG = open('./flag.txt', 'r').read() # flag.txt 파일로부터 FLAG 데이터를 가져옴. except: FLAG = '[**FLAG**]' users = { 'guest': 'guest', 'admin': FLAG # FLAG 데이터를 패스워드로 선언 }
- 취약점 분석
- username 변수가 쿠키에 의해 결정(요청에 포함) → 이용자의 임의 조작이 가능
# 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 변수가 쿠키에 의해 결정(요청에 포함) → 이용자의 임의 조작이 가능
- 풀이
- username을 admin으로 변경(Application > Cookies > username - Value) → 웹 브라우저의 개발자 도구를 사용 (Application > Cookies > username - Value)
- 페이지 소스 (파이썬 플라스크 프레임워크)
- 존재하는 사용자는 guest, admin
- 로그인
- 서버 생성 후 아래에 나오는 링크를 클릭
- URL 뒤에 /login 을 입력하여 로그인 페이지로 이동
- 개발자 도구 > Elements 에서 guest 계정의 비밀번호 확인 (guest/guest)
- 쿠키 변경 (guest로 로그인 되어있는 상태)
- 개발자 도구 > Application > value - username 을 변경 → 플래그 확인
실습 - 쿠키와 세션
- @app.route('/') → 인덱스 페이지
- 세션을 통해 이용자를 식별 (쿠키의 sessionid를 통해 session_storage에서 username을 조회)
- username이 admin일 경우 플래그를 출력
# index 페이지 코드 @app.route('/') # / 페이지 라우팅 def index(): session_id = request.cookies.get('sessionid', None) # 쿠키에서 sessionid 조회 try: username = session_storage[session_id] # session_storage에서 해당 sessionid를 통해 username 조회 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('/admin') → 관리자 페이지
- 무작위 값 생성을 통해(os.urandom(32).hex()) session_storage에 admin의 세션 정보를 생성
# admin 페이지 코드 @app.route('/admin') def admin(): # developer's note: review below commented code and uncomment it (TODO) #session_id = request.cookies.get('sessionid', None) #username = session_storage[session_id] session_storage에 저장된 username을 불러옴 #if username != 'admin': # username이 admin인지 확인 # return render_template('index.html') return session_storage
# admin 세션 생성 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' # username이 admin인 Session ID를 무작위로 생성 print(session_storage) app.run(host='0.0.0.0', port=8000)
- 취약점 분석
- session_storage가 인증을 거치지 않으므로 조회 가능 (주석 처리 되었으므로)
- 풀이
- /admin 페이지를 통해 이용자 별 세션 정보를 조회
- sessionid를 admin의 것으로 생성
- 페이지 소스 (파이썬 플라스크 프레임워크)
- 존재하는 계정 정보 확인
- sessionid 확인 및 변경
- /admin 페이지에 접속해 admin의 sessionid 복사하기
- guest로 로그인해 생성되는 쿠키에서 sessionid변경 → 플래그 획득
동일 출처 정책 (Same Origin Policy)
- 악의적인 페이지에서 데이터를 읽을 수 없도록
- 쿠키(인증 정보)를 브라우저 내부에 보관하여 HTTP 요청에 포함시킴 (자동 로그인 등의 서비스에 이용됨)
- 오리진(Orgin / 정보 출처) 구분 방법
- 오리진 → 프로토콜(Protocal, Scheme) + 포트(Port) + 호스트(Host)로 구성
- 동일한 오리진이려면? → 세 개가 모두 일치해야함
URL 결과 이유 https://same-origin.com/frame.html Same Origin Path만 다름 **http://**same-origin.com/frame.html Cross Origin Scheme이 다름 https://**cross.**same-origin.com/frame.html Cross Origin Host가 다름 https://same-origin.com:1234/ Cross Origin Port가 다름 - SOP 실습 (자바스크립트를 이용해 SOP 테스트)
- window.open → 새 창
- object.location.href → 객체가 가리키는 URL 읽기
# same origin sameNewWindow = window.open('<https://dreamhack.io/lecture>'); console.log(sameNewWindow.location.href); // 결과: <https://dreamhack.io/lecture>
# cross origin crossNewWindow = window.open('<https://theori.io>'); console.log(crossNewWindow.location.href); // 결과: Origin 오류 발생
- Cross Origin 데이터 읽기/쓰기 → 읽기는 불가하지만 쓰기는 가능
# 가능한 코드 crossNewWindow = window.open('<https://theori.io>'); crossNewWindow.location.href = "<https://dreamhack.io>";
- 동일한 오리진이려면? → 세 개가 모두 일치해야함
- 오리진 → 프로토콜(Protocal, Scheme) + 포트(Port) + 호스트(Host)로 구성
SOP 실습
- 입력
- Same Origin Policy 작동 예시
- 코드
- 2 → (iframe) 현재 웹 페이지 안에 새로운 웹 페이지를 삽입
- 12 → (onload) 객체가 성공적으로 로드되었을 때 동작
- 16~17 → iframe 객체의 location.href를 읽어 my-frame-log (div)에 출력
<!-- create iframe object --> <iframe src="" id="my-frame"></iframe> <div id="my-frame-log"></div> <!-- begin javascript code --> <script> /* assign iframe object to myFrame variable */ let myFrame = document.getElementById("my-frame"); let myFrameLog = document.getElementById("my-frame-log"); /* on assigning address to iframe object (myFrame.src), execute following code */ myFrame.onload = () => { /* try ... catch for handling errors */ try { /* upon load, print location.href value of iframe object to div element */ let link = myFrame.contentWindow.location.href; myFrameLog = link; } catch (err) { /* print logs in case of errors */ myFrameLog.innerText = err; } } /* functions for assigning Same Origin/Cross Origin addresses to iframe object */ const loadSameOrigin = () => { myFrame.src = window.location.origin; } const loadCrossOrigin = () => { myFrame.src = "https://dreamhack.io/"; } </script> <!-- create two buttons (Same Origin, Cross Origin) --> <button onclick=loadSameOrigin()>Same Origin</button><br> <button onclick=loadCrossOrigin()>Cross Origin</button>
- 코드
교차 출처 리소스 공유 (Cross Origin Resource Sharing)
- 브라우저가 SOP와 상관 없이 외부 출처에 대한 접근을 허용 → <img>, <style>, <script> 태그 등
- 메일 등에서 여러 호스트의 오리진이 다르다고 인식할 경우 여러 불편 발생 → CORS와 관련된 HTTP 헤더를 추가하여 자원(이미지, JS, CSS 등) 공유 (JSONP 방법도 있음 - JSON with Padding)
- CORS
- SOP를 완화한 방법으로 HTTP 헤더에 기반하여 Cross Origin 간 리소스 공유 (CORS 헤더 설정)
- 수신 측의 응답이 발신 측의 요청과 상응하는지 확인한 후 수신 측의 웹 리소스를 요청하는 HTTP 요청을 보냄
Header 설명 Access-Control-Allow-Origin 헤더 값에 해당하는 Origin에서 들어오는 요청만 처리합니다. Access-Control-Allow-Methods 헤더 값에 해당하는 메소드의 요청만 처리합니다. Access-Control-Allow-Credentials 쿠키 사용 여부를 판단합니다. 예시의 경우 쿠키의 사용을 허용합니다. Access-Control-Allow-Headers 헤더 값에 해당하는 헤더의 사용 가능 여부를 나타냅니다.
- 과정 예시
- 웹 리소스 요청 코드 → POST 방식으로 HTTP 요청을 보냄
/* 웹 리소스 요청 코드 */ /* XMLHttpRequest 객체를 생성합니다. XMLHttpRequest는 웹 브라우저와 웹 서버 간에 데이터 전송을 도와주는 객체 입니다. 이를 통해 HTTP 요청을 보낼 수 있습니다. */ xhr = new XMLHttpRequest(); /* <https://theori.io/whoami> 페이지에 POST 요청을 보내도록 합니다. */ xhr.open('POST', '<https://theori.io/whoami>'); /* HTTP 요청을 보낼 때, 쿠키 정보도 함께 사용하도록 해줍니다. */ xhr.withCredentials = true; /* HTTP Body를 JSON 형태로 보낼 것이라고 수신측에 알려줍니다. */ xhr.setRequestHeader('Content-Type', 'application/json'); /* xhr 객체를 통해 HTTP 요청을 실행합니다. */ xhr.send("{'data':'WhoAmI'}");
- 발신 측의 HTTP 요청 → OPTIONS 메소드를 가진 HTTP 요청이 전달 (CORS) → (헤더) Access-Control-Request
- 서버 측의 HTTP 응답 → (CORS preflight) OPTIONS가 추가됨
- 웹 리소스 요청 코드 → POST 방식으로 HTTP 요청을 보냄
// 발신 측의 HTTP 요청 OPTIONS /whoami HTTP/1.1 Host: theori.io Connection: keep-alive Access-Control-Request-Method: POST Access-Control-Request-Headers: content-type Origin: https://dreamhack.io Accept: */* Referer: https://dreamhack.io/
// 서버 측의 HTTP 응답 HTTP/1.1 200 OK Access-Control-Allow-Origin: https://dreamhack.io Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: Content-Type
JSON with Padding (JSONP)
- <script> 태그를 이용해 Cross Origin의 데이터를 불러옴
- Callback 함수 이용 → <script> 태그 내에서는 데이터를 자바스크립트의 코드로 인식하므로
- CORS 이전 사용되던 방법으로 현재는 사용하지 않음
- 과정 예시
- 웹 리소스 요청 코드
- (13) Cross Origin의 데이터를 불러오며 callback 파라미터를 통해 myCallback을 함께 전달
- 웹 리소스 요청에 따른 응답 코드
- 웹 리소스 요청 코드
/* 웹 리소스 요청 코드 */ <script> /* myCallback이라는 콜백 함수를 지정합니다. */ function myCallback(data){ /* 전달받은 인자에서 id를 콘솔에 출력합니다.*/ console.log(data.id) } </script> <!-- https://theori.io의 스크립트를 로드하는 HTML 코드입니다. 단, callback이라는 이름의 파라미터를 myCallback으로 지정함으로써 수신측에게 myCallback 함수를 사용해 수신받겠다고 알립니다. --> <script src='http://theori.io/whoami?callback=myCallback'></script>
/* 웹 리소스 요청에 따른 응답 코드 */ /* 수신측은 myCallback 이라는 함수를 통해 요청측에 데이터를 전달합니다. 전달할 데이터는 현재 theori.io에서 클라이언트가 사용 중인 계정 정보인 {'id': 'dreamhack'} 입니다. */ myCallback({'id':'dreamhack'});
퀴즈 2
'24-2 SISS > 웹해킹' 카테고리의 다른 글
[SISS/웹해킹 스터디] 2학기 6주차 스터디 - SQL Injection (0) 2024.11.03 [SISS/웹해킹 스터디] 2학기 5주차 스터디 - CSRF (1) 2024.10.06 [SISS/웹해킹 스터디] 2학기 4주차 스터디 - XSS (0) 2024.09.29 [SISS/웹해킹 스터디] 2학기 1주차 스터디 (2) 2024.09.08