SK shieldus Rookies 16기 (클라우드 보안 기술 #05)

만두다섯개·2023년 12월 20일
0

SK 루키즈 16기

목록 보기
35/52

주요 정보

  • 교육 과정명 : 클라우드기반 스마트융합보안 과정 16기
  • 교육 회차 정보 : '23. 12. 20. 클라우드 보안 기술 #05

학습 참고

https://docs.google.com/document/d/1Jw1UeK9s8bbSD39tS3I-2m3yHNwhUwj6xkmNOmqZjgo/edit#heading=h.8xxtvhur9wfm

학습 요약

  1. 웹 브라우저관점에서 본 XSS 취약점 방어 방법
    • CSP
    • SOP
    • CORS (자바스크립트, 출처 상속)
  2. CSRF

웹 브라우저는 어떻게 XSS 취약점을 대비하는가?

MDN(Mozilla Developer Network) 웹 문서 기술 관련 안내해주고 있다.

XSS 공격은 서버에서 받은 콘텐츠를 브라우저가 신뢰한다는 점을 악용합니다
즉, XSS 공격의 근본적 문제는 스크립트 파일 등을 브라우저가 실행시켜준다는 점이다.
어떻게 이를 방지할 수 있을까?

먼저, SOP, CSP, 출처 등에 대해 알아보자.

CSP (Content Security Policy)란?

  1. 목적: 컨텐츠 보안 정책 웹 애플리케이션에서 허용되는 리소스의 유형과 출처를 명시적으로 정의하여 XSS(Cross-Site Scripting)와 같은 공격을 방지하는 데 사용됩니다.
  2. 동작: CSP는 웹 애플리케이션에서 허용되는 스크립트 실행 위치, 이미지 로딩 위치, 스타일 적용 위치 등을 제한하는 정책을 설정합니다. 이를 통해 악성 스크립트의 실행을 방지하고, 외부 리소스로부터의 공격을 최소화합니다.

SOP (Same-Origin Policy)란?

  1. 목적: 동일 출처 정책은 웹 브라우저에서 실행되는 스크립트 언어(주로 JavaScript)를 대상으로 하는 보안 정책입니다. 동일한 출처에서 로드된 문서 간에만 상호 작용을 허용하고, 다른 출처에서 로드된 문서에 대한 접근을 제한합니다.

  2. 동작: 웹 페이지의 스크립트는 해당 페이지와 동일한 프로토콜, 호스트, 포트 번호를 가진 웹 서버에서만 로드된 리소스와 상호 작용할 수 있습니다.

소스코드 : 브라우저야, 다른 출처로 데이터 가져와~
브라우저 : SOP로 인해 거부합니다. 안돼~
SOP가 아닌 CSP로 인해 거부 가능한거 아닌가요? => CSP는 조금 더 넓은 개념

CSP: 정책을 통해 특정 출처에서 허용되는 리소스 및 스크립트 실행 규칙을 지정
SOP: 브라우저의 기본적인 출처 간 보안을 담당하는 정책이며, 스크립트 및 다른 웹 리소스의 동일 출처 여부를 확인하여 차단.

웹 콘텐츠의 출처란?

MDN에서 명시하는 웹 콘텐츠의 출처

웹 콘텐츠의 Origin(출처)란 접근 시 사용하는 URL schema(프로토콜), host(domain), port로 정의된다.
프로토콜, 포트(명시된 경우), 호스트가 같은 경우, 두 URL은 동일한 출처라고 볼 수 있다.
스키마/호스트/포트 튜플 혹은 튜플로 참조된다고 한다.

예시)
아래와 같은 객체가 존재할 때, 서로 동일한 출처라고 볼 수 있다.

http://www.naver.com/index.html
http://www.naver.com/index.html

그러나 URL 스키마가 다르면, 아래와 같이 동일 출처라고 할 수 없다.

http://www.naver.com/index.html
https://www.naver.com/index.html

아래는 서로다른 도메인을 가졌으므로 동일 출처가 아니다.

http://www.naver.com/index.html
http://www.navers.com/index.html

아래는 서로다른 호스트를 가졌으므로 동일 출처가 아니다.

http://www.naver.com/index.html
http://www.naver.com/double_index.html

서버는 디폴트 포트로 80번으로 HTTP 콘텐츠 전달한다. 따라서 아래는 포트가 서로다르므로 동일 출처가 아니다.

http://www.naver.com/index.html
http://www.naver.com/index.html:8080

동일 출처 정책 세부내용

특정 출처에서 불러온 문서, 스크립트가 다른 출처에서 가져온 리소스와 상호 작용할 수 있는 방법을 제한하는 메커니즘이다.
잠재적 악성 문서를 격리해 가능한 공격 벡터를 감소시킨다.
(예시 : 브라우저에서 JS 실행해 사용자 로그인 웹 메일, 회사 인트라넷에서 공격자에게 전달 방지)

출처 상속 : about:blank 또는 javascript: URL이 있는 페이지에서 실행된 스크립트는 해당 URL이 포함된 문서의 출처를 상속합니다. 이러한 유형의 URL에는 원본 서버에 대한 정보가 포함되어 있지 않기 때문입니다.

파일 출처: 최신 브라우저는 일반적으로 file:/// 스키마를 사용하여 로드된 파일의 출처를 불투명한 출처로 취급합니다. 이는 파일에 동일한 폴더(예시로)에 있는 다른 파일이 포함된 경우, 같은 출처에서 온 것으로 간주되지 않으며 CORS 오류가 발생할 수 있다는 것을 뜻합니다.

CORS란?

MDN에서 명시하는 CORS

예전 수업에서 만든 웹 애플리케이션 구조이다. 서로 다른 출처의 데이터를 CORS 허용 설정으로 가져온다.

해당 실습과정에서 CORS 의문점이 들었다.
의문점 : 브라우저가 CORS 보안 정책으로 한 앱의 요청 전송을 제한한다. => 어떻게 요청 React의 요청(데이터 가져와)를 CORS 보안 정책 허용 설정을 브라우저가 확인하는가?

내 예상 : 요청 전송이 브라우저에서 React 앱으로 이동하고, 해당 앱에서 브라우저로, 그리고 브라우저는 다시 get 으로 Flask 앱으로부터 데이터를 받아오는데, 이때 Flask 앱은 모든 도메인으로부터 CORS 보안 정책과 무관하게 요청을 처리한다(누가 데이터 달라는데? 그냥 줘~)
브라우저가 Flask에게 데이터 요청 시, 출처가 다른 데이터를 요청함에도 불구하고 (현재 앱은 React로 실행 중) CORS 정책 허용으로 인해 브라우저가 해당 요청을 수행한다.

결론 : CORS 정책 여부로 브라우저가 다른 출처(도메인) 데이터 요청 수행이 결정된다.

React : 브라우저야, Flask(다른 출처)한테 데이터 가져와~
웹 브라우저 : 싫은데? SOP때문에 안돼~
React : CORS 정책 허용했어. 가져와.
웹 브라우저 : 네. Flask에게 데이터 요청 => 데이터를 웹 페이지에 게시

CORS 예시로 알아보자

일반적으로 웹 어플리케이션은 기원이 다른 리소스를 사용할 수 있음

http://www.exam.com/index.html 
==============================
<img src="http://www.test.com/myimage.gif" />
<script src="http://www.test.com/lib.js"></script>

단, JavaScript를 이용해서 리소스를 가져오는 경우는 동일 기원에서 가져 온 것만 사용할 수 있도록 제한
= ajax 통신을 통해서 가져오는 경우

즉, 웹 애플리케이션에서는 출처 상속 기능으로, 동일 출처라면, 불분명한 출처의 리소스를 요청해도 존재한다면 응답해준다. 이는 동일 출처 정책을 느슨하게 만들어 좀 더 원활한 자원 공유를 위해 존재한다.

만약 자바스크립트로 자원 요청을 하면 어떻게 될까?
sop.html

<html>
<head>
</head>
<body>
	<img src="/bWAPP/images/bee_1.png" />
	<img src="http://beebox/bWAPP/images/bee_1.png" />
	<hr/>
	<img src="http://victim:8080/WebGoat/images/logos/owasp.jpg" />
	<img src="/WebGoat/images/logos/owasp.jpg" />
	<hr/>

	Image from http://beebox <img id="bimage" src="#" />
	<br/>
	Image from http://victim:8080 <img id="wimage" src="#" />
	<br/>
	<button onclick="loadImage('bimage', 'http://beebox/bWAPP/images/bee_1.png')">Load BeeBox Image</button>
	<button onclick="loadImage('wimage', 'http://victim:8080/WebGoat/images/logos/owasp.jpg')">Load WebGoat Image</button>

	<script>
		function loadImage(id, url) {
			const xhr = new XMLHttpRequest(); // 비동기 방식으로 직접 이미지를 가져온다. 

			xhr.onload = function() {
				if (xhr.status === 200) {
					const img = document.getElementById(id);
					img.src = URL.createObjectURL(xhr.response);					
				} else {
					console.error('Fail to load image', xhr.statusText);
				}
			};
		
			xhr.open("GET", url, true);
			xhr.responseType = 'blob';
			xhr.send();
		}
	</script>
</body>
</html>

실행 결과는 아래와 같다

동일하게 스크립트를 사용해 이미지를 가져오지만, 첫번째 버튼은 성공하고 두번째 버튼을 실패했다.
왜 그럴까?
첫번째 버튼 : 동일 출처를 사용하는 리소스 요청 => 스크립트로 이미지 요청 가능
두번째 버튼 : 동일 출처가 아닌 리소스 요청 => CORS 정책으로 인해 해당 요청 block.

CORS 등장 이유가 뭘까?

웹 브라우저에서 XSS, 등 여러 보안 취약점을 보호하기 위해 SOP를 사용하는데, 이와 반대로 왜 CORS 정책을 또 만들었을까?
기상청으로부터 기상 정보를 받아와 보여주는 API를 사용한다면, 출처가 다른 리스를 사용하게 된다.
즉, 오픈 API 활용에 문제가 되는 SOP 정책과 공존하기 위해 CORS 정책을 사용한다.

웹 서비스 개발자 : 오픈 API 사용할게~
웹 브라우저 : SOP로 막을게~
웹 서비스 개발자 : CORS 정책 설정하고 오픈 API 사용할게~
웹 브라우저 : SOP가 있지만, CORS 정책 설정 되어 있으니까 데이터 가져올게~
오픈 API 제공 외부 리소스 제공 웹 서버 : 굿. 여기 데이터 줄게.

CORS 등장 결론 : SOP 기반해 보안성과 CORS로 서비스 확장성, 사용성 증대

CSP에서 어떻게 개발자가 정의한 스크립트와 공격 스크립트를 구분하는가?

MDN에서 명시한 CSP
아래 링크에서는 브라우저 별 제공하는 CSP 버전이 다름을 설명한다.
https://content-security-policy.com/browser-test/
해당 링크로 들어가면 아래와 같은 결과를 획득 가능

CSP 내용을 정의한 파일은 디폴트로 외부에 존재한다.

이때 외부가 무슨 말일까?
바로 현재 페이지를 보여주는 소스코드 파일이 아닌, 다른 소스코드 파일의 스크립트를 사용한다는 의미이다.
아래 SCP를 명시하는 웹 브라우저의 Network의 헤더의 예시를 확인해보자.
각 외부 파일에서 스크립트 파일 사용을 명시하고 있다.

default-src 'none'; 
script-src 
	'self' 
	www.googletagmanager.com platform.twitter.com syndication.twitter.com
    'sha256-ewTm8QMx/IkmbIFAIapvCHoCrGgIIHhn8qKC7/5Y2Ro='  ⇐ 믿을 수 있는 Internal 스크립트 코드의 해쉬를 정의
	'unsafe-hashes'  => 해시함수를 생성해 아웃라인 스크립트 사용에 쓰는 것이 아닌, CSP에서
    정의된 해시 외 (이후 생성된) 해시함수로 아웃라인 스크립트 사용에 쓴다는 말이다. 
    
	'sha256-mplq9U9bn5xLaFQjbIOde0Eu7cXsI2xaTPex2jLztp0='; 
style-src 
	'self' 
	cdnjs.cloudflare.com fonts.googleapis.com 
	'sha256-5g0QXxO6NfvHJ6Uf5BK/hqQHtso8ZOdjlnbyKtYLvwc='; 
font-src 
	fonts.gstatic.com cdnjs.cloudflare.com; 
img-src 
	'self' 
	syndication.twitter.com; 
frame-src 
	platform.twitter.com; 
connect-src 
	www.google-analytics.com

여기서 script-src란 해당 소스코드의 아웃라인 파일로부터 스크립트 파일을 가져온다는 것이다.
왜 여기에 암호화 관련 코드가 정의되어 있을까? 인라인 스크립트 코드의 해쉬를 정의한다.

인라인, 아웃라인의 구분

<script src="(문서 밖의) 스크립트 파일 주소"></script>		⇐ External

<script>								⇐ Internal
   스크립트 코드
</script>

<img onclick="스크립트 코드" />					⇐ Inline

CSP 리소스 참조 과정

⇒ CSP에 정의되어 있는 해쉬에 포함되므로 해당 스크립트 코드가 실행
사진의 우측 스크립트 호출 소스코드는, 응답으로 받은 소스코드이다. (실제 웹 서버의 소스코드와 일치하지 않는다)

CSRF

Cross Site Request Forgery, 크로스 요청 사이트 위조란 무엇일까?
서버에서 요청 주제와 요청 절차를 확인하지 않고 요청을 처리하는 경우 발생한다.
즉, 절차와 상관 없이 인증된 요청을 위조해 악용할 수 있게 되는 취약점이다.
(로그인 된 사용자가 공격자의 패스워드 변경 문구를 누르는 등..)

CSRF 방어기법

  1. 중요 기능에 대해서 재인증, 재인가 해야 한다.

    데이터를 생성, 수정, 삭제하는 기능
    중요 정보를 다루는 기능
    금융 관련 데이터를 다루는 기능

  2. 정상적인 절차에 따른 요청인지를 확인 후 처리한다.
    2-1. 선행 페이지에서 (텍스트 기반) 토큰 부여 후 처리 페이지에서 토큰을 검증한다.
    => 공격자가 공격 스크립트에서 선행 페이지를 거치게 해 토큰을 받는다면, 이 방법은 안전하지 않는 방법이 될 수 있다.
    2-2. CAPCHA를 사용한다. 사람은 이해할 수 있지만, 컴퓨터는 인식하지 못하는 이미지를 보고 사용하는 정보로 사용자인지 확인한다.

자동화된 요청을 방지하고, 사용자와의 상호작용(interaction)을 통한 요청이다.

2-3. ReCAPCHA
CAPCHA를 사용하자, 사용자들이 글자 인식의 어려움이 존재해, 조금 더 쉬운 사용자 확인을 위해 ReCAPCHA 사용.

CSRF 실습

웹 브라우저에서 http://beebox/bWAPP/user_new.php 로 접속후 test 계정을 생성한다.

bee/bug 로 로그인 후 CSRF-(Change Password)로 들어가보자.

현재 계정에서 비밀번호 변경을 할 수 있다
해당 페이지에는 패스워드 변경을 신청 시, 재인증을 입력 창이 존재하지 않는다. (CSRF 취약점 가능)

비밀번호를 bug로 변경해보면 아래 그림과 같이 패스워드가 변경되었다고 나온다.
패스워드 변경 요청 URL에서도 정상 절차를 검증하는 값인 토큰이 보이지 않는다.

자동으로 계정의 비밀번호 변경하는 url을 요청하는 코드를 작성한다

<iframe src="http://beebox/bWAPP/csrf_1.php?password_new=password&password_conf=password&action=change" width="0" height="0"></iframe>

해당 코드를 bee-box 게시판에 넣는다.
게시판이 존재하는 게시판 웹 페이지로 이동. (http://beebox/bWAPP/xss_stored_1.php)
게시판은 스크립트 코드를 그대로 내보낸다

이제 여기에 여기에 미끼 글을 올리고, 정상 사용자가 이 게시글을 읽을 수 있게 만들자.
추가로 위에서 만든 CSRF 공격 스크립트를 동시에 넣자.

이제 정상적인 사용자가 해당 게시물에 방문하면, 스크립트가 실행되어. 비밀번호가 password로 변경된다.

tester/tesrt 계정으로 로그인 후, 해당 게시판에 가서 모든 게시물을 확인하기 위해 Show all에 체크하고, Submit을 누르면, 좌측 네트워크에서 패스워드의 변경 스크립트가 실행됨을 확인 가능하다.

tester 계정을 로그아웃 하고 다시 들어가 보면 이전 패스워드로 로그인이 되지 않는다.
계정의 패스워드가 변경됨을 확인 가능하다.

CSRF는 웹 방화벽으로 막기 어렵다.
왜 그럴까? 정상적인 사용자가 요청을 하는 것이기 때문이다.

소스코드 확인

bee-box 소스코드를 확인해 보자.

sudo gedit /var/www/bWAPP/csrf_1.php 
  1. security_level에 따른 현재 패스워드 입력 필드 추가

  2. 웹 서버 패스워드 변경 내부 수행

if(isset($_REQUEST["action"]) && isset($_REQUEST["password_new"]) && isset($_REQUEST["password_conf"]))
{
    
    $password_new = $_REQUEST["password_new"];
    $password_conf = $_REQUEST["password_conf"];
    
    if($password_new == "")  // 새 패스워드 공백시 메시지 출력
    {
        
        $message = "<font color=\"red\">Please enter a new password...</font>";       
        
    }
    
    else
    {

        if($password_new != $password_conf)		// 새 패스워드, 확인 패스워드가 불일치시 메시지 출력
        {

            $message = "<font color=\"red\">The passwords don't match!</font>";       

        }

        else            
        {

            $login = $_SESSION["login"];		// login 세션을 가져온다. 
            
            $password_new = mysqli_real_escape_string($link, $password_new);	//새 패스워드를 이스케이프화 시킨다. 
            $password_new = hash("sha1", $password_new, false);    // 새 패스워드를 암호화 시킨다.

            if($_COOKIE["security_level"] != "1" && $_COOKIE["security_level"] != "2") // security_level이 1또는 2가 아니면, DB 테이블에서 패스워드 변경
            {

                $sql = "UPDATE users SET password = '" . $password_new . "' WHERE login = '" . $login . "'";

                // Debugging
                // echo $sql;      

                $recordset = $link->query($sql);

                if(!$recordset)
                {

                    die("Connect Error: " . $link->error);

                }
                
                $message = "<font color=\"green\">The password has been changed!</font>";

            }

            else		// security_level이 1또는 2 이면, 아래와 같이 패스워드 변경한다. 사용자가 다시 입력한 계정 정보로 로그인 후, 해당 정보가 맞다면, 해당 계정의 패스워드를 변경한다.
            {
                
                if(isset($_REQUEST["password_curr"]))
                {
                              
                    $password_curr = $_REQUEST["password_curr"]; // 사용자로부터 현재 패스워드 입력을 가져온다. 
                    $password_curr = mysqli_real_escape_string($link, $password_curr); // 현재 패스워드를 이스케이프화 시키고, 로그인 쿼리를 $sql에 담는다. 
                    $password_curr = hash("sha1", $password_curr, false);                

                    $sql = "SELECT password FROM users WHERE login = '" . $login . "' AND password = '" . $password_curr . "'";

                    // Debugging
                    // echo $sql;    

                    $recordset = $link->query($sql);    // 비밀번호 변경 쿼리 실행 후 결과를 recordset에 true / false로 반환         

                    if(!$recordset)
                    {

                        die("Connect Error: " . $link->error);

                    }

                    // Debugging                
                    // echo "<br />Affected rows: ";                
                    // printf($link->affected_rows);

                    $row = $recordset->fetch_object();   

                    if($row)
                    {

                        // Debugging
                        // echo "<br />Row: ";
                        // print_r($row);

                        $sql = "UPDATE users SET password = '" . $password_new . "' WHERE login = '" . $login . "'";

                        // Debugging
                        // echo $sql;

                        $recordset = $link->query($sql);

                        if(!$recordset)
                        {

                            die("Connect Error: " . $link->error);

                        }

                        // Debugging              
                        // echo "<br />Affected rows: ";         
                        // printf($link->affected_rows);

                        $message = "<font color=\"green\">The password has been changed!</font>";

                    }

                    else
                    {

                        $message = "<font color=\"red\">The current password is not valid!</font>";

                    }
                
                }
                
            }
                           
        } 
    
    }
    
}

CSRF 취약점 게시판에 자동 글쓰기 스크립트 삽입

openeg CSRF 취약점이있는 게시판에 자동 글쓰기 스크립트를 삽입해 보자.
/openeg/src/main/java/kr/co/openeg/lab/login/controller/LoginController.java 에서 이전 실습에서 XSS 취약점을 막기 위해 추가한 lucy 필터를 주석처리한다.

http://victim:8080/openeg/login.do 접속해 로그인 하고(test/test), 게시판으로 들어가자.


정상적으로 스크립트가 실행되는 것을 볼 수 있다.
자동으로 글을 쓰는 스크립트를 작성해보자

<form action="write.do" method="post" enctype="multipart/form-data">
<input type="text" name="subject" value="저렴한 대출상품 안내" /> 
<input type="hidden" name="writer" value="김실장" /> 
<input type="hidden" name="writerId" value="test" />
<textarea name="content"> 쉽고 간단하게 세상을 사는 방법(주식아님)</textarea>
<input type="submit" id="btnSubmit" />
</form>
<script> document.getElementById("btnSubmit").click(); </script>

이제 이 content를 누르면, 스크립트가 실행되어, 글이 게시판에 자동으로 등록될 것이다.
아래와 같이 게시물을 작성한다.

이제 해당 게시물을 누를 때마다 새로운 게시물이 생성된다.

profile
磨斧爲針

0개의 댓글