Dreamhack

웹 해킹 Stage 3

kchabin 2022. 7. 22. 23:01

Background : Cookie & Session

HTTP 프로토콜 통신

메소드 - 웹 서버 명령. GET, POST 등

URL - 자원의 위치

 

헤더(Header)

클라이언트 정보 및 요청 내용 구체화하는 데이터 포함. 

+) 클라이언트의 인증 정보

Cookie, Session

 

쿠키

클라이언트의 IP 주소, User-Agent는 매번 변경될 수 있는 고유X 정보.

HTTP 프로토콜의 Connectionless, Stateless 특징 -> 웹 서버는 클라이언트를 기억할 수 없음.

이런 특성을 갖는 HTTP에서 상태를 유지하기 위해 쿠키 탄생.

 

Key와 Value로 이루어진 일종의 단위. 

 

- 서버가 클라이언트에 쿠키 발급

- 클라이언트는 서버에 요청을 보낼 때 마다 쿠키를 같이 보냄.

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

 

 

 * HTTP 프로토콜 특징 *

- Connectionless : 하나의 요청, 하나의 응답 후 연결 종료.

새 요청이 있을 때 마다 항상 새로운 연결을 맺음.

 

- Stateless : 통신이 끝난 후 상태 정보를 저장하지 않음.

이전 연결에서 사용한 데이터를 다른 연결에서 요구할 수 없음.

 

쿠키 용도 : 클라이언트의 정보 기록, 상태 정보 표현

 

정보 기록

'다시 보지 않기', '7일 간 표시하지 않기' 버튼

웹 서버 :  각 클라이언트의 팝업 옵션을 기억하기 위해 해당 정보를 쿠키에 기록하고, 쿠키를 통해 팝업 창 표시 여부를 판단함.

과거 : 클라이언트 정보 저장용으로 종종 사용됨. 

 

단점 : 쿠키는 서버와 통신할 때마다 전송. 필요 없는 요청을 보낼 때 리소스 낭비가 발생할 수 있음.

-> Mordern Storage APIs 를 통한 데이터 저장 방식 권장 중.

 

상태 정보

회원 가입과 로그인을 통한 개개인 맞춤형 서비스 제공.

-> 웹 서버에서는 수많은 클라이언트의 로그인 상태와 이용자를 구별해야함.

쿠키에 클라이언트 식별 값을 저장해서 사용.

 

 

쿠키가 없는 통신

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

쿠키가 있는 통신

 

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

쿠키 : "이름=드림이"

 

쿠키 변조

중국집 도장 위조해서 쿠폰에 도장 찍기 -> 식당에서 구분 불가능

악의적인 클라이언트도 이처럼 쿠키 정보를 변조해 서버에 요청을 보낼 수 있음. 

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

공격자의 이용자 사칭

 

 

세션

쿠키에 저장한 인증 정보를 클라이언트가 변조하지 못하게 하는 용도. 

 

- 인증 정보를 서버에 저장. 

- 해당 데이터 접근 키(유추 불가능 랜덤 문자열) 클라이언트에 전달. => Session ID

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

 

중국집 랜덤 문자열 쿠폰. 제시하면 문자열과 방문 횟수 기록. -> 문자열은 조작할 수 있어도 방문 횟수는 조작 불가능해짐.

 

세션키가 티켓이 된다.

쿠키와 세션의 차이

쿠키 - 데이터 자체를 이용자가 저장함.

세션 - 서버가 저장.

세션 키 값은 랜덤한 문자열. id에 따라 문자열이 다름.  쿠키는 랜덤한 문자열이 아닌 데이터 자체가 저장됨. 

 

쿠키 적용법

클라이언트 - 저장된 쿠키 조회, 수정, 추가 가능.

서버에 요청을 보낼 때 저장된 쿠키를 요청 헤더에 넣어 전송. 이용자가 요청을 보낼 때 쿠키 헤더를 변조할 수 있음.

만료시간 지정 가능 -> 만료 시간 이후에는 클라이언트에서 쿠키 삭제. 

쿠키의 만료는 클라이언트(브라우저)에서 관리

 

서버

HTTP 응답 중 헤더에 쿠키 설정 헤더(Set-Cookie) 추가 -> 클라이언트의 브라우저가 쿠키 설정.

 

클라이언트

자바스크립트로 쿠키 설정.

document.cookie = "name=test;"
document.cookie = "age=30; Expires=Fri, 30 Sep 2022 14:54:50 GMT;"

 

크롬 Console 활용법

페이지 우클릭 시 나타나는 검사 버튼 누른 후 Console 탭에서 document.cookie를 입력

쿠키 정보 확인 가능.

 

쿠키 옵션(Http Only)에 따라 자바스크립트에서 쿠키 확인이 불가능 할 수 있음.

 

크롬 Application 활용

우클릭 검사 - Application 탭 - 좌측 Cookies - Origin 목록 확인 - 설정된 쿠키 정보 확인/수정 가능.

Network 탭에서 Preserve log을 체크하고 로그인 하면 로그인 성공 시 응답을 살펴볼 수 있는데,

서버에서 set-cookie 헤더를 통해 브라우저 쿠키에 세션 정보를 저장하는 것을 볼 수 있음.

Application을 누르고 set-cookie 헤더를 통해 설정된 쿠키를 확인할 수 있음.

헤더의 값을 복사하고 우클릭한 후  Delete 클릭하면

새로고침 시 로그아웃 돼있는 걸 확인할 수 있음.

쿠키 빈 칸 더블클릭 해서 sessionid 헤더를 추가하고 아까 복사해뒀던 값을 다시 입력해준 뒤 새로고침하면 다시 로그인 돼있는 것을 확인할 수 있음.

 

쿠키에는 이용자의 세션 정보가 저장돼있고 서버는 이를 통해 이용자 식별하고 인증을 처리함.

공격자가 이용자의 쿠키를 훔칠 수 있으면 세션에 해당하는 이용자의 인증 상태를 훔칠 수 있는데

이를 세션 하이재킹(Session Hijacking)이라고 함.

 

세션 하이재킹 : 타 이용자의 쿠키를 훔쳐 인증 정보 획득 공격.


session-basic 혼자 실습

다운 받은 문제 파일 app.py를 열어보면 일단 유저가 나오는데, 왼쪽은 아이디, 오른쪽이 비밀번호로 보인다.

실제로 guest를 입력해서 로그인이 된다. 내가 알아내야 할 admin 계정의 비밀번호는 FLAG로, 알아내야 한다.

guest 로그인 시 화면

코드를 살펴보면 아래와 같이 @app.route라는게 보이고, 각 코드 아래에 함수가 정의돼있다.

@app.route('/')
@app.route('/login', methods=['GET', 'POST'])
@app.route('/admin')

이 route는 Flask 모듈 내장 함수로 보이는데, 한번 찾아봤다. 

라우트는 외부 웹브라우저에서 웹서버로 접근 시 해당 주소로 입력을 하게 되면

특정 함수가 실행되도록 도와주는 기능을 한다. 

Index:session 사이트의 주소에 /login을 추가하면 로그인 화면으로 바뀐다. 위에 정의된 login 함수가 실행되는 것이다.

그러면 위 코드를 보면서 '/admin'을 주소에 추가해주면 session_storage를 리턴해주는 함수가 실행되는 것이라고 생각해 볼 수 있다. 주석에도 대놓고 이게 이게 이상하다고 말해주고 있다.

guest의 세션키와 admin의 세션키로 보이는 것이 나왔다. 이제 원래 메인 화면으로 돌아가서 Application 탭에 세션키를 입력해주고 새로고침한다.

플래그를 찾았다

 

 

 


 

함께 실습 : Cookie

잘못된 쿠키의 설계로 발생할 수 있는 문제점을 다루는 문제.

 

파이썬 Flask 프레임워크로 구현된 서버.

웹 프레임워크 : 웹 페이지 개발 과정에서 발생하는 유지보수, 반복 작업 등의 어려움을 줄이기 위해 명명 및 스크립트 작성 규칙 또는 디렉터리 구조 등이 명시된 개발 표준 사이드를 정의하고, 개발자가 이에 맞춰 개발할 수 있도록 함.

 

문제 목표 및 기능

/ : 이용자의 username을 출력하고 관리자 계정인지 확인

/login : username, password를 입력받고 로그인함.

@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를 출력함.

 

@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가 동일하지 않은 경우

엔드 포인트: /login

 

GET 

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

POST

입력된 username과 password를 users 변숫값과 비교.

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

 

코드를 잘 보면 관리자 계정의 패스워드가 FLAG인 것을 알 수 있음.

 

취약점 분석 

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

쿠키는 이용자가 임의로 조작할 수 있음.

서버는 별다른 검증 없이 이용자 요청에 포함된 쿠키를 신뢰하고, 이용자 인증 정보를 식별하기 때문에

공격자는 쿠키에 타 계정 정보를 삽입해 계정을 탈취할 수 있음.

 

이러한 취약점을 세션으로 해결할 수 있음.

세션은 인증 정보를 서버에 저장하고, 랜덤한 키를 클라이언트에게 발급함.(sessionid)

클라이언트는 해당 키를 포함해 서버에게 요청하고,

서버는 저장한 세션 키와 대응하는 클라이언트인지 확인하므로 안전한 서비스를 구현할 수 있음.

아까 풀었던 혼자 실습 문제에는 username이 쿠키에 의해 결정되지 않음. username은 session_storage와 관련있음.

익스플로잇

본 문제를 해결하기 위해서는 쿠키에 존재하는 username을 'admin' 문자열로 조작해야함.

웹 브라우저의 개발자 도구를 사용하면 쿠키의 정보를 확인하거나 수정할 수 있음.

guest였던 것을 admin으로 수정
새로고침 시 플래그 획득!

 


Same Origin Policy

쿠키에는 민감한 정보가 보관됨.

브라우저로 웹 서비스에 접속할 때 SNS 로그인 된 상태로 접속한 걸 요청을 보내면

헤더에는 당연히 해당 웹 서비스 쿠키가 포함돼있을거고,

악의적인 페이지는 로그인 된 이용자의 SNS 응답을 받게 됨.

맘대로 페이지 접속자의 SNS에 글을 올리거나, 삭제하고, SNS 메신저를 읽는 것도 가능해짐.

 

이를 방지하기 위해 동일 출처 정책, Same Origin Policy(SOP) 메커니즘이 탄생함.

클라이언트 사이드 웹 보안에 중요한 요소임.

 

악의적 페이지가 클라이언트의 권한을 위해 대상 사이트에 HTTP 요청을 보내고,

HTTP 응답 정보를 획득하는 코드를 실행할 수 있음. 

-> 보안 위협 발생 요소.

 

가져온 데이터를 악성 페이지에서 읽을 수 없도록 해야함.

 

SOP의 오리진(Origin) 구분 방법

오리진 = 프로토콜, 포트, 호스트로 구성

이 세가지가 모두 일치해야 동일한 오리진.

 

비교 대상 : https://same-origin.com/

 

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 실습

sameNewWindow = window.open('https://dreamhack.io/lecture');
console.log(sameNewWindow.location.href);
결과: https://dreamhack.io/lecture

코드 첫째줄 입력 시 이동되는 화면.

Same Origin

 

Cross Origin

Cross Origin 데이터 읽기/쓰기

외부 출처에서 불러온 데이터를 읽으려고 할 때는 오류가 발생해 읽지 못하지만

데이터를 쓰는 것은 문제 없이 동작함.

오류 발생 X

 

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>

 

1. 두 번째 줄 iframe : 현재 웹 페이지 안에 또 다른 하나의 웹 페이지를 삽입.

src 요소를 설정함으로써 삽입할 웹 페이지의 주소가 결정됨.

<iframe src="" id="my-frame"></iframe>

 

2. 10번째 줄 onload : 이벤트 핸들러. 

해당 객체가 성공적으로 로드되었을 때 동작함.

10 ~ 23번 줄이 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'; }

 

3. 14~15번째 줄은 로드가 완료되면 iframe 내에 삽입된 주소에서 

secret-element 객체의 값인 treasure를 읽어와 콘솔에 출력하는 동작을 수행함.

let secretValue = myFrame.contentWindow.document.getElementById('secret-element').innerText;
        console.log({ secretValue });

Same Origin 버튼 : 앞선 과정이 모두 성공적으로 수행되어 treasure라는 값이 출력됨.

Cross Origin 버튼 : 앞선 과정 중 세번째 과정이 실패(14~15코드) Cross Origin의 데이터에 접근할 수 없다는 오류 출력.

 


Cross Origin Resource Sharing (CORS)

SOP에 구애받지 않고 외부 출처에 대한 접근을 허용해주는 경우 존재함.

예 : <img>, <style>, <sctript> 등의 태그

 

웹 서비스에서 SOP를 완화하여 다른 출처의 데이터를 처리 해야 하는 경우도 있음.

 

특정 포털 사이트 카페, 블로그, 메일, 메인 주소

 

https://cafe.a

https://blog.a

https://mail.a

https://a

 

이용자가 수신한 메일의 개수를 메인 페이지에 출력하려면,

개발자는 메인 페이지에서 메일 서비스에 관련된 리소스를 요청하도록 해야함.

이때 오리진이 다르므로 SOP를 적용받지 않고 리소스를 공유할 방법이 필요함.

 

교차 출처 리소스 공유(Cross Origin Resource Sharing, CORS) 방법

 

CORS와 관련된 HTTP 헤더를 추가하여 전송함.

JSON with Padding(JSONP)로 대체 가능.

 

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 : 수신 측에 웹 리소스를 요청해도 되는지 질의하는 과정.

 

"Access-Control-Request"로 시작하는 헤더가 존재함.

해당 헤더 뒤에 따라오는 Method와 Headers는 

각각 메소드와 헤더를 추가 사용할 수 있는지 질의함.

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

서버의 응답

 

 

 
  Header                                                     설명

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

 

위 과정을 마치면 브라우저는 수신 측의 응답이 발신 측의 요청과 상응하는지 확인하고, 그때야 비로소 POST 요청을 보내 수신측의 웹 리소스를 요청하는 HTTP 요청을 보냄.

 

 

JSON with Padding (JSONP)

이미지나 자바스크립트, CSS 등의 리소스는 SOP에 구애 받지 않고 외부 출처에 대한 접근 허용

-> 이를 이용, <script> 태그로 Cross Origin의 데이터를 불러옴

 

BUT

<script> 태그 내에서는 데이터를 JS의 코드로 인식함

-> Callback 함수 활용.

 

Cross Origin에 요청 시, callback 파라미터에 어떤 함수로 받아오는 데이터를 핸들링할지 넘겨주면,

대상 서버는 전달된 Callback으로 데이터를 감싸 응답함.

 

 

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

웹 리소스 요청 코드

 

마지막 줄에서 Cross Origin의 데이터를 불러옴. 이때 callback 파라미터로 myCallback을 함께 전달함.

Cross Origin에서는 응답할 데이터를 myCallback 함수의 인자로 전달될 수 있도록 

myCallback으로 감싸 JS 코드를 반환해줌.

 

반환된 코드는 요청 측에서 실행되기 때문에 3~6 번 줄에서 정의된 myCallback 함수가 전달된 데이터를 읽을 수 있음.

/*
수신측은 myCallback 이라는 함수를 통해 요청측에 데이터를 전달합니다.
전달할 데이터는 현재 theori.io에서 클라이언트가 사용 중인 계정 정보인
{'id': 'dreamhack'} 입니다. 
*/
myCallback({'id':'dreamhack'});

웹 리소스 요청에 따른 응답 코드

 

다만 JSONP는 CORS가 생기기 전에 사용하던 방법으로 현재는 거의 사용 X.

'Dreamhack' 카테고리의 다른 글

웹 해킹 : XSS-2  (0) 2022.07.25
웹 해킹 ClientSide: XSS  (0) 2022.07.25
시스템 해킹 quiz.c  (0) 2022.07.22
웹 해킹 Stage 2  (0) 2022.07.22
시스템 해킹 Stage 3  (0) 2022.07.19