[WEB] Cookie & Session

hack_98·2023년 2월 22일
0

WEB

목록 보기
2/4

1) 쿠키

  • 클라이언트의 정보 기록과 상태 정보를 표현하는 용도로 사용
  • 쿠키는 key와 value로 이루어진 일종의 단위
  • 아래와 같은 HTTP 특징으로 서버는 클라이언트를 기억할 수 없기 때문에 쿠키를 사용

HTTP 프로토콜 특징
Connectionless : 요청 하나에 하나의 응답을 한 후 연결을 종료
Stateless : 통신이 끝난 후 상태 정보를 저장하지 않는 것


2) 쿠키변조

  • 쿠키는 클라이언트에 저장되고, 요청에 포함되는 정보
  • 따라서, 악의적인 클라이언트가 쿠키 정보를 변조해 서버에 요청을 보낼 수 있음
  • 서버가 검증 없이 쿠키를 통해 이용자의 인증정보를 식별한다면 타 이용자를 사칭해 정보를 탈취할 수 있음
    ex) Cookie : id=admin;

3) 세션

  • 쿠키에 인증 상태를 저장하지만, 클라이언트가 인증정보를 변조할 수 없게 세션을 사용
  • 작동 방식 : 인증정보를 서버에 저장하고, 데이터에 접근 할 수 있는 키(랜덤 문자열)을 만들어 클라이언트에게 전달
    • 해당 키를 Session ID라고 함
  • 브라우저는 해당 키를 쿠키에 저장하고 HTTP요청을 보낼 때 사용
  • 서버는 요청에 포함된 키에 해당하는 데이터를 가져와 인증 상태를 확인
    ex) Cookie: session=nhmuifinwkernejrwbwkja;

    쿠키는 데이터 자체를 이용자가 저장 / 세션은 서버가 저장


4) 쿠키 적용법

  • 쿠키는 클라이언트에 저장되기 때문에 조회,수정,추가 가능하며 만료시간을 지정할 수 있음
    만료시간 이후에는 클라이언트에서 쿠키가 삭제됨
  • 서버 : HTTP 응답 헤더에 쿠키 설정 헤더 (Set-Cookie)를 추가하면 클라이언트의 브라우저가 쿠키를 설정
    클라이언트 : 자바스크립트를 사용해 쿠키를 설정
  • Console탭 >  document.cookie를 입력하면 쿠키 정보를 확인가능
    • 쿠키 옵션(HttpOnly)에 따라 자바스크립트에서 쿠키 확인이 불가능
  • Application탭 >  Cookies를 펼치면 쿠키 정보 확인가능

5) 세션 활용

  • 쿠키에는 세션정보가 저장되어 있고, 서버는 이를 통해 이용자를 식별하고 인증 처리함
  • 공격자가 이용자의 쿠키를 훔칠 수 있으면, 세션에 해당하는 이용자의 인증상태를 훔칠 수 있음
    == 이를 세션 하이재킹(Session Hijacking)


1) 엔드포인트: /

  • 쿠키에 존재하는 username이 “admin”일 경우 FLAG를 출력하는 문제
    @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')

2) 엔드포인트: /login

  • 메소드 요청마다 다른 기능을 수행 하는 것을 확인 가능

    • GET : username과 password를 입력할 수 있는 로그인 페이지 제공
    • POST : 입력받은 username과 password값을 users 변수값과 비교
      ex) 손님 계정의 비밀번호는 “guest”, 관리자 비밀번호는 파일에서 읽어온 FLAG임을 확인
      @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가 동일하지 않은 경우
        try:
            FLAG = open('./flag.txt', 'r').read() # flag.txt 파일로부터 FLAG 데이터를 가져옴.
        except:
            FLAG = '[**FLAG**]'
        users = {
            'guest': 'guest',
            'admin': FLAG # FLAG 데이터를 패스워드로 선언
        }

3) 취약점 분석

  • 요청 패킷 내 username 변수가 포함된 쿠키에 의해 결정되어 문제가 발생함

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

    @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')

4) 익스플로잇

  • 쿠키에 존재하는 username을 “admin”문자열로 조작하면 해결 !
  • 서버가 검증 없이 쿠키를 신뢰하고 인증 정보를 식별할 때 발생할 수 있는 문제점임
    - 이러한 문제점은 세션을 사용해 해결 가능하며
    - 세션은 인증정보를 서버에 저장하고, 랜덤 키를 클라이언트에게 발급해 서비스 사용


3. Mitigation: Same Origin Policy

1) Same Origin Policy (SOP)

  • 동일 출처 정책(Same Origin Policy, SOP)
    : 클라이언트가 가져온 데이터를 악의적인 페이지에서 읽을 수 없도록 하는 보안 매커니즘
  1. 이용자가 웹 서비스에 접속할 때, 브라우저는 쿠키를 HTTP요청에 포함 시켜 전달
    (웹 리소스를 통해 간접적으로 타 사이트에 접근할 때도 쿠키 전달)
  2. 이러한 특징 때문에, 악의적인 페이지가 클라이언트의 권한을 이용해 대상 사이트에 http 요청을 보내고, http응답 정보(쿠키 등)를 획득하는 코드를 실행 가능

2) Same Origin Policy의 오리진(Origin) 구분 방법

  • Origin은 프로토콜,포트,호스트로 구성
    - 해당 구성요소가 모두 일치해야 동일한 오리진으로 판단함

3) Same Origin Policy 실습

  • SOP는 same origin일때만 정보를 읽을 수 있도록 해줌
  • 외부 출처에서 불러온 데이터를 읽으려고 할때는 오류가 발생해 읽지 못함
    - 읽는 것 외에 데이터를 쓰는것을 문제없이 동작 가능

4) Same Origin Policy (SOP) 데모

<!-- 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>
  • same origin 버튼 : 모두 성공적으로 수행, treasure 값 출력
  • cross origin 버튼 : 세번째 과정이 실패하여 cross origin 데이터에 접근 불가한 오류 발생

5) Same Origin Policy 제한 완화

  • 브라우저가 SOP에 구애받지 않고 외부 출처에 대한 접근을 허용해 주는 경우가 존재
  • 이미지나 자바스크립트 CSS등 리소스를 불러오는 <img><style><script>
    등의 태그는 SOP의 영향을 받지 않음
  • 웹 서비스에서 동일 출처 정책인 SOP를 완화하여 다른 출처의 데이터를 처리해야 하는 경우도 존재함
    • ex) 특정 포털 사이트에서 다른 각각의 host인 카페,블로그,메일 서비스를 운영하는 경우에 오리진이 다르지만, 메인페이지에서 메일서비스를 요청하도록 해야함 ⇒ SOP를 적용받지 않고 리소스 공유할 방법이 필요
  • 자원을 공유하기 위해 사용할 수 있는 공유방법을 교차 출처 리소스 공유 (Cross Origin Resource Sharing, CORS)

6) Cross Origin Resource Sharing (CORS)

  • 교차 출처 리소스 공유 (Cross Origin Resource Sharing, CORS) 는 HTTP헤더에 기반하여 Cross Origin간에 리소스를 공유하는 방법임

    /*
        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'}");
    • 발신측에서 POST방식으로 HTTP 요청을 보냈으나, OPTIONS 메소드를 가진 HTTP요청이 전달된 것을 확인 가능 ⇒ 이를 CORS preflight 라고 함 (수신측에 리소스 요청해도 되는지 질의 과정)
    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/
    • “Access-Control-Request”로 시작하는 헤더가 존재, 해당 헤더는 각각 메소드와 헤더를 추가적으로 사용할 수 있는지 질의
    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
    • 서버에서 허용되는 Origin,Methods,Credentials(쿠키사용 여부), Header 를 응답해줌

7) JSON with Padding (JSONP)

  • 이미지나 자바스크립트, CSS 등 리소스는 SOP에 구애받지 않고 외부 출처에 대해 접근을 허용
  • JSONP 방식은 이러한 특징을 이용해 <script>태그로 Cross Origin의 데이터를 불러옴
    • <script>태그에서는 자바스크립트로 인식하기 때문에 Callback함수를 활용
    • Cross Origin 에 요청할 때 callback 파라미터에 받아오는 데이터를 넘겨주면,
      대상 서버는 전달된 callback으로 데이터를 감싸 응답함
  • 현재는 거의 사용하지 않으며, CORS를 사용하는 추세임
<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'});
profile
Go Big or Go Home

0개의 댓글