24-2 SISS/웹해킹

[SISS/웹해킹 스터디] 2학기 2, 3주차 스터디

noname64 2024. 9. 14. 19:30

2학기 2, 3주차 스터디
수강 인증 캡쳐 화면

2주차 09/09 ~ 09/15 [드림핵] Cookie & Session

 

쿠키


  • 클라이언트의 IP 주소, User-Agent는 매번 변경될 수 있음
  • HTTP 프로토콜은 Connectionless, Stateless함
    • Connectionless → 한 요청 당 하나의 응답을 한 후 연결을 종료
    • Stateless → 통신 종료 후 상태 정보를 저장하지 않음

→ 웹 서버는 클라이언트를 기억할 수 없으므로 쿠키를 이용

 

  • 쿠키
    • 클라이언트의 정보(인증 정보 포함)와 요청의 내용을 구체화하는 데이터
    • Key + Value
    • 이용
      • (클라이언트) 서버에 요청 전송 시 쿠키를 함께 전송
      • (서버) 쿠키를 통해 클라이언트 구분
    • 용도
      • 정보 기록 → 클라이언트 정보 저장 (팝업 다시 보지 않기 등)
      • 상태 정보 → 로그인 상태 및 이용자 구별
  • 쿠키 변조
    • 변조된 쿠키를 검증 없이 사용할 경우 악의적인 정보 탈취 가능

 

세션


  • 인증 정보를 변조하지 못하도록 함 (인증 상태는 쿠키에 저장)
    • 이용자가 데이터를 저장하는 쿠키와 달리 서버에 데이터를 저장
  • 인증 정보를 서버에 저장
  • 세션 ID(정보 데이터에 접근할 수 있는 키)를 클라이언트에 전달

 

쿠키 및 세션 실습


  • 쿠키

쿠키

  • 세션 → 이름 변경이 불가 (키 값으로 접근하므로)

세션

 

  •  실습
    • 세션 키 확인하기 (Session Storage)
    • 세션 키 입력하기

세션 키 확인하기 (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;"
      

 

  • 쿠키 열람
    • 크롬 콘솔 활용
      • 페이지 우클릭 후 [Console] 탭을 누름
      • document.cookie를 입력
    • 크롬 애플리케이션 활용
      • 페이지 우클릭 후 검사 버튼 누르기
      • [Application] 탭에서 [Cookies]을 통해 쿠키 정보 확인

 

드림핵 세션을 통한 연습


  • 로그인 응답 확인
    • (로그인 페이지) 우클릭 후 검사 선택
    • [Network] 탭의 Preserve log를 체크 (로그인 성공 시 응답을 볼 수 있음) → 응답의 set-cookie 헤더를 통해 브라우저의 쿠키에 세션 정보를 저장함을 확인
  • 설정된 쿠키 확인
    • 크롬 검사의 [Application] > [Cookies]의 주소(https://dreamhack.io)를 누를 경우 설정된 쿠키 확인 가능
  • 저장된 세션 값 삭제
    • (sessionid 헤더 값 메모장에 복사하기) sessionid 헤더 값을 메모장에 복사한 후 새로고침하면 로그아웃
  • 세션 값 재설정
    • 쿠키의 빈 칸에 sessionid 헤더 및 복사한 세션 값 입력 시 로그인
  • 세션 하이재킹
    • 이용자의 쿠키 탈취를 통해 세션에 해당하는 이용자의 인증 상태를 훔치는 것

 

퀴즈 1


퀴즈 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을 admin으로 변경(Application > Cookies > username - Value) → 웹 브라우저의 개발자 도구를 사용 (Application > Cookies > username - Value)
    • 페이지 소스 (파이썬 플라스크 프레임워크)
      • 존재하는 사용자는 guest, admin
    • 로그인
      1. 서버 생성 후 아래에 나오는 링크를 클릭
      2. URL 뒤에 /login 을 입력하여 로그인 페이지로 이동
      3. 개발자 도구 > 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변경 → 플래그 획득

페이지 소스 - 존재하는 계정 정보 확인
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>";
          

 

SOP 실습


  • 입력

같은 입력
다른 scheme (https → http)
다른 호스트 (dreamhack → dreamhackhack)
다른 포트 (https 기본 443 → 1234)

 

  • 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>

same origin을 눌렀을 경우
cross origin을 눌렀을 경우 (로그 참고)

 

교차 출처 리소스 공유 (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가 추가됨
// 발신 측의 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


퀴즈 2