[SK shieldus Rookies 16기][취약점 진단] CSRF의 개요와 공격실습

Jina·2023년 12월 22일
0

SK shieldus Rookies 16기

목록 보기
44/59
post-custom-banner

1. CSRF

1.1. 정의

  • 크로스 사이트 요청 위조 Cross-Site Request Forgery 의 약어
  • 서버에서 요청 주체와 요청 절차를 확인하지 않고 요청을 처리하는 경우 Victim의 권한으로 요청이 실행되는 취약점

1.2. 공격원리

1.3. 방어기법

  1. 중요 기능에 대해서 요청 주체 재인증, 재인가
    • 중요기능 : 데이터 생성/수정/삭제, 중요 데이터 다루는 기능, 송금 기능
    • 재인증, 재인가 : 다단계 인증 적용
  2. 정상적인 절차에 따른 요청인지 확인 후 요청 처리
    • 선행 페이지에서 텍스트 기반의 토큰을 부여하고 처리 페이지에서 토큰을 검증하는 방법
    • CAPTCHA 는 요청 과정에 사용자가 참여하도록 만드는 것
      • 자동화된 요청 방지
      • 사용자와의 상호작용(Interaction)을 통한 요청

1.4. CSRF 공격 예시

  • 자동 회원 가입
  • 게시판 자동 글쓰기
  • 광고 배너 클릭

실습문제1 - ChangePassword

beebox > create user 탭 > 회원가입 > 회원가입한 계정으로 로그인 > Cross-Site Request Forgery - ChangePassword

1. 목적

스크립트가 포함된 게시글을 조회한 사용자의 비밀번호를 강제로 변경시키기

패스워드 변경 페이지 확인

패스워드 변경 시도 시 재인증 절차 없이 패스워드가 변경되는 것 확인할 수 있다.

패스워드 변경 요청 주소 확인해보면 정상절차를 검증하는 토큰 값이 포함되어 있지 않다.

패스워드를 변경하는 중요 기능을 구현할 때 요청 주체 및 요청 절차를 확인하지 않고 패스워드를 변경하고 있다. ⇒ CSRF 취약점 존재

로그아웃 후 재로그인 시도 시 기존 패스워드로는 로그인이 불가하고 변경한 패스워드로는 로그인이 가능하다.

자동으로 패스워드 변경을 요청을 발생시키는 코드를 작성하여 게시판에 올려보자.

<iframe src="http://beebox/bWAPP/csrf_1.php?password_new=1234&password_conf=1234&action=chage" witdh="0" height="0"></iframe>

이렇게 업로드 된 게시물을 다른 사용자가 조회하게 되면 요청하지 않았어도 자동으로 패스워드가 변경된다.

개발자 도구 > Network 탭에서 요청이 전달됐는지 확인할 수 있다.

beebox VM에서 소스 코드를 확인해보자.

$ sudo gedit /var/www/bWAPP/csrf_1.php

패스워드 변경에 필요한 사용자 입력을 받아 서버로 전달하는 부분

   <form action="<?php echo($_SERVER["SCRIPT_NAME"]); ?>" method="GET">
<?php
       # 보안 등급이 높으면 현재 패스워드를 함께 입력하도록 되어 있다.
       if($_COOKIE["security_level"] == "1" or $_COOKIE["security_level"] == "2")
        {
?>											     
       <p><label for="password_curr">Current password:</label><br />     	                   
       <input type="password" id="password_curr" name="password_curr"></p>
<?php        										   
        }
?>		
	   # 보안 등급이 낮은 경우 패스워드 변경에 꼭 필요한 값만 입력받는다. (변경할 패스워드)
       <p><label for="password_new">New password:</label><br />			 
       <input type="password" id="password_new" name="password_new"></p>	   
       <p><label for="password_conf">Re-type new password:</label><br />
       <input type="password" id="password_conf" name="password_conf"></p>  

       <button type="submit" name="action" value="change">Change</button>  
   </form>

서버 내부에서 처리

# 패스워드 변경에 필요한 값을 포함하고 있는 요청 파라미터가 존재하는 확인
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            
        {
            # 패스워드 변경 대상 정보를 추출 = 변경 대상의 ID를 추출 = 로그인한 사용자의 ID를 추출
            $login = $_SESSION["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") 
            {
                $sql = "UPDATE users SET password = '" . $password_new . "' WHERE login = '" . $login . "'";
                $recordset = $link->query($sql);
 
                if(!$recordset)
                {
                    die("Connect Error: " . $link->error);
                }
 
                $message = "<font color=\"green\">The password has been changed!</font>";
            }
            else
            {				               
 				# 보안 등급이 높은 경우, 현재 패스워드가 요청 파라미터로 전달되었는 확인
                if(isset($_REQUEST["password_curr"]))
                {                              
                    $password_curr = $_REQUEST["password_curr"];
                    $password_curr = mysqli_real_escape_string($link, $password_curr);
                    $password_curr = hash("sha1", $password_curr, false);                
 
					# 현재 로그인한 사용자의 패스워드가 요청 파라미터로 전달된 것과 동일한지 확인
                    $sql = "SELECT password FROM users WHERE login = '" . $login . "' AND password = '" .$password_curr . "'";
                    $recordset = $link->query($sql);             
 
                    if(!$recordset)
                    {
                        die("Connect Error: " . $link->error);
                    }
 
                    $row = $recordset->fetch_object();   
                    # 패스워드가 일치하는 경우(=로그인한 사용자의 요청이 맞음) 패스워드를 변경
                    if($row)
                    {			      
                        $sql = "UPDATE users SET password = '" . $password_new . "' WHERE login = '" . $login . "'";
 
                        $recordset = $link->query($sql);
                        if(!$recordset)
                        {
                            die("Connect Error: " . $link->error);
                        }
 
                        $message = "<font color=\"green\">The password has been changed!</font>";
                    }
                    else
                    {
                        $message = "<font color=\"red\">The current password is not valid!</font>";
                    }
                }
            }
        } 
    }
}
?>

실습문제2 - 게시판에 form 삽입 및 실행

openeg > 로그인 > 게시판에 스크립트 작성 가능한지 확인

스크립트가 포함되어 있는 게시글 조회 시 스크립트가 실행되는 것을 확인할 수 있다.

아래 코드를 포함한 게시물을 작성한다.

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

작성된 스크립트에서 Submit Query 버튼을 클릭하면 자동으로 게시물이 작성되는 것을 확인할 수 있다.

  • 취약점 종류 : Stored XSS, CSRF
  • 판단 근거
    • Stored XSS : 실행 가능한 스크립트 코드를 게시판에 등록하면 게시판 상세 페이지에서 등록하나 스크립트 코드가 실행되므로,
      Stored XSS 취약점이 존재한다.
    • CSRF : 정상적인 절차에 따른 요청인지, 요청의 주체가 맞는지 확인하지 않고 등록 처리에 필요한 값 존재 여부만 체크해서 등록 처리하기 때문에 CSRF 취약점이 발생했다.
  • 대응 방안
    • Stored XSS : 게시판 저장 페이지에서 입력값을 검증해서 실행 가능한 스크립트 코드가 존재하는 경우, 오류처리 하거나, 제거하거나, 안전한 형태로 변경해서 저장하는 방법이 있고,
      게시판 상세 페이지에서 실행 가능한 스크립트 코드를 제거하거나 안전한 형태로 변경해서 출력하는 방법이 있다.
      해당 기능을 구현할 때 검증된 라이브러리, 프레임워크를 사용하는 것을 권장한다.
    • CSRF : 정상적인 절차에 따른 요청인지를 확인하는 코드를 추가한다.
// BoardController.java
	// 게시판 글쓰기 페이지 요청
	@RequestMapping("/write.do")
	public String boardWrite(@ModelAttribute("BoardModel") BoardModel boardModel, HttpSession session) {
		// (1) 임의의 난수를 발생 시켜서 세션에 저장
		String token = UUID.randomUUID().toString(); // 추가
		session.setAttribute("session_token", token); // 추가
		return "/board/write";
	}
<!-- write.jsp -->
	<form action="write.do" method="post" onsubmit="return writeFormCheck()" enctype="multipart/form-data">
		<!-- (2) 세션에 저장된 토큰값을 HIDDEN 필드의 값으로 설정 -->
		<input type="hidden" name="request_token" value="${session_token}" /> <!-- 추가 -->
				
		<table class="boardWrite">
		<tr>
			<th><label for="subject">제목</label></th>
			<td>
				<input type="text" id="subject" name="subject" class="boardSubject" size="70" />
				<input type="hidden" id="writer" name="writer" value="${userName}" />
				<input type="hidden" id="writerId" name="writerId" value="${userId}" />
			</td>
		</tr>
		<tr>
			<th><label for="content">내용</label></th>
			<td>
				<textarea id="boardContent" name="content" class="boardContent"></textarea>
			</td>
		</tr>
		<tr>
			<th><label for="file">파일</label></th>
			<td>
				<input type="file" id="file" name="file" />
				<span class="date">&nbsp;&nbsp;*&nbsp;임의로 파일명이 변경될 수 있습니다.</span>
			</td>
		</tr>
		</table>
		<br />
		<center>
			<input type="reset" value="재작성" class="writeBt" />
			<input type="submit" value="확인" class="writeBt" />
		</center>
	</form>
// BoardController.java
	// 게시판 글 저장
	@RequestMapping(value = "/write.do", method = RequestMethod.POST)
	public String boardWriteProc(@ModelAttribute("BoardModel") BoardModel boardModel, MultipartHttpServletRequest request, HttpSession session) {
		// (3) 세션에 저장된 토큰 값과 요청 파라미터를 통해 전달된 토큰 값을 비교
		//     일치하면 기존처럼 글 저장을 처리하고, 일치하지 않으면 오류 메시지를 반환
		String stoken = (String)session.getAttribute("session_token");
		String ptoken = request.getParameter("request_token");
		
		if (ptoken == null || !ptoken.equals(stoken)) {
			session.setAttribute("error_message", "잘못된 접근입니다.");
			return "redirect:list.do";
		}
//list.jsp
<script type="text/javascript">
	function selectedOptionCheck(){
		$("#type > option[value=<%=request.getParameter("type")%>]").attr("selected", "true");
	}
	
	function moveAction(where){
		switch (where) {
		case 1:
			location.href = "write.do";
			break;
		
		case 2:
			location.href = "list.do";
			break;
		}
	}
	
	// (4) 세션에 error_message가 있는 경우 출력
	<c:if test="!empty error_message">
		alert("${error_message}");
		
		// (5) 메세지 출력 후 세션에 저장된 error_message의 값을 삭제
		<c:set var="error_message" value="" />
	</c:if>
</script>

Cross-Site Request Forgery (Change Secret)은 BeeBox에서는 해당 실습을 CSRF로 분류하고 있으나 보안기능 결정에 사용되는 부적절한 입력값 취약점으로 보는 것이 맞다.

외부에서 입력도진 값에 의존해서 주요 기능을 처리할 때 발생
외부에서 입력된 값은 제한적으로 사용해야 하며, 반드시 검증 후 사용

실습문제3 - 송금 계좌 변경하기

1. 목표

계좌이체 서비스에서 다른 사람 계좌로 들어갈 금액을 내 계좌로 가져오기

2. 소스코드 확인

GET /bWAPP/csrf_2.php
account=123-45678-90&amount=0&action=transfer

3. 문제점

중요 기능임에도 정상적인 절차에 따른 요청인지 확인하지 않고, 요청 주체를 확인하는 로직이 없다.

계좌번호를 변경하면 내가 보내고자 했던 계좌번호가 아닌 엉뚱한 계좌로 송금되는 것을 확인할 수 있다.

profile
공부 기록
post-custom-banner

0개의 댓글