본문 바로가기
SK Shieldus Rookies 19th/애플리케이션 보안

[SK shieldus Rookies 19기][애플리케이션 보안] - WebGoat, Bee box, BeEF, XSS, CSRF

by En_Geon 2024. 3. 31.

1. BeEF를 이용한 XSS 공격

1) 설치 및 실행

(1) 설치

sudo apt install beef-xss 입력으로 설치

 

(2) 실행

sudo beef-xss 입력 실행

 

처음 실행하면 기본 비밀번호 재 설정을 해야 한다. 각자의 비밀번호를 입력하고 넘어가면 127.0.0.1:3000 주소로 BeEF 페이지가 열린다. ID에는 beef를 입력하고 각자가 설정한 비밀번호를 입력하고 로그인한다.

 

 

2) XSS 공격

(1) XSS - Stored(Blog)

 

 

이전 포스팅에서 스크립트로 공격했던 페이지에서 BeEF로 공격 실습한다.

 

(2) hook.js

 

 

Kali 가상머신에서 XSS 취약점을 가지고 있는 게시판에 hook.js 파일을 실행하는 글을 등록한다.

 

(3) BeEF 확인

 

 

 

BeEF 콘솔에서 스크립트 코드가 실행된 것을 확인할 수 있다.

위 bee.box에 나온 IP 주소는 Kali 가상머신의 주소다.

 

(4) 호스트 PC

 

호스트 PC에서 XSS에 위 게시글이 저장된 페이지를 접속해서 hook.js가 계속해서 호출하는 것을 확인한다.

 

 

 

(5) BeEF 확인

 

 

 

 

 

BeEF 콘솔을 다시 확인하면 호스트 PC가 감염된 것을 볼 수 있고, 감염된 브라우저 정보를 확인할 수 있다.

정보 위에 있는 탭 중 Commands를 들어가면 더 많은 정보를 얻을 수 있다.

 

 

쿠키

 

 

 

Get Cookie를 누르면 오른쪽 칸 아래에 Execute 버튼이 있는데, 버튼을 누르면 쿠키를 얻기 위해 스크립트를 만들지 않아도 쿠키 정보를 얻을 수 있다.

 

쿠키 정보만 얻을 수 있는 것은 아니고, Module Tree를 확인해 보면 많은 정보를 얻을 수 있다.

 

  • Get Page HTML - HTML 소스 코드 정보
  • Get Page HREFs - 링크 정보
  • Play Sound - 사운드 파일의 링크를 입력하면 브라우저에서 소리가 난다.
  • Redirect Browser - 입력한 주소로 리다이렉션

 

Fake Flash Update

 

 

 

 

 

호스트 PC에서 이상한 업데이트 창이 나오게 되는데 많은 사람은 위 설명을 자세히 읽지 않고 설치하게 된다.

 

 

Google Phishing

 

 

 

 

 

Execute 하면 구글 로그인 페이지가 나오게 된다. 구글은 익숙하고 신뢰 있는 사이트로 생각하기 때문에 많은 사람들은 가짜 페이지에 대해 의심 없이 로그인하게 된다. 

 

 

로그인하게 되면 BeEF 콘솔에 정보가 나오게 된다.

 

가짜 페이지에서 로그인 시도를 하면 로그인 시도가 잘못되었다는 로그인 실패 메시지를 몇 번 보여주고 원래 구글 페이지로 리다이렉션을 한다. 그렇게 되면 사용자는 진짜 구글 페이지에서 로그인하고 로그인이 되어 의심을 하지 않는다.

하지만 몇 번의 입력한 값들은 공격자가 이미 빼돌리게 된다.

 

3) 취약한 소스 코드 확인

sudo gedit /var/www/bWAPP/xss_stored_1.php
functions_external.php

 

//xss_stored_1.php

while($row = $recordset->fetch_object())
{

    if($_COOKIE["security_level"] == "2")
    {
?>
        <tr height="40">
            <td align="center"><?php echo $row->id; ?></td>
            <td><?php echo $row->owner; ?></td>
            <td><?php echo $row->date; ?></td>
            <td><?php echo xss_check_3($row->entry); ?></td>	 
        </tr>
<?php
    }
    else
        if($_COOKIE["security_level"] == "1")
        {
?>
        <tr height="40">
            <td align="center"><?php echo $row->id; ?></td>
            <td><?php echo $row->owner; ?></td>
            <td><?php echo $row->date; ?></td>
            <td><?php echo xss_check_4($row->entry); ?></td>	 
        </tr>
<?php
        }
        else        
            {
?>
        <tr height="40">
            <td align="center"><?php echo $row->id; ?></td>
            <td><?php echo $row->owner; ?></td>
            <td><?php echo $row->date; ?></td>
            <td><?php echo $row->entry; ?></td>			 
        </tr>								   
<?php          
            }
}      


// functions_external.php

function xss_check_3($data, $encoding = "UTF-8")
{
    // htmlspecialchars - converts special characters to HTML entities    
    // '&' (ampersand) becomes '&amp;' 
    // '"' (double quote) becomes '&quot;' when ENT_NOQUOTES is not set
    // "'" (single quote) becomes '&#039;' (or &apos;) only when ENT_QUOTES is set
    // '<' (less than) becomes '&lt;'
    // '>' (greater than) becomes '&gt;'  		
    return htmlspecialchars($data, ENT_QUOTES, $encoding);
}

function xss_check_4($data)
{
    // addslashes-returns a string with backslashes before characters that need to be quoted in database queries etc.
    // These characters are single quote ('), double quote ("), backslash (\) and NUL (the NULL byte).
    // Do NOT use this for XSS or HTML validations!!!

    return addslashes($data);
}

 

 

  • 보안 등급이 높은 경우
    • xss_check_3 함수 호출
    • <script> 태그가 &lt;script&gt; 형태로 변경되어 전달
    • &lt;script&gt; 형태를 브라우저는 <script> 텍스트로 단순 출력
  • 보안 등급이 중간인 경우
    • xss_check_4 함수 호출
    • addslashes 함수
    • 작은따옴표, 큰따옴표, 백슬래시, NUL에 대해 이스케이프
  • 보안 등급이 가장 낮은 경우
    • DB에 저장된 내용을 그대로 출력

 

 

2. Django 에서 XSS

1) 질문에 스크립트 코드 포함 저장

 

 

2) 저장된 질문 확인

(1) 질문 페이지 확인

스크립트 코드가 실행되지 않고 단순한 문자열로 출력된다. 페이지 소스 코드는 어떻게 되어 있는지 확인해 본다.

 

 

(2) 페이지 소스 코드

 

소스 코드에서 보면 스크립트 코드가 HTML 인코딩되어 단순한 문자열로 출력되는 것을 확인할 수 있다.

 

 

(3) DB 확인

 

 

DB에는 스크립트 코드가 들어있는 것을 확인할 수 있다.

 

 

(4) 코드 확인

 

 

DB에 저장된 내용을 XSS 공역에 안전하도록 출력해 주는 부분이다. DTL에서 실행할 수 있는 코드를 안전하게 HTML 인코딩해서 출력해 준다.

 

(5) 태그 동작

 

 

위와 같이 등록했는데 태그가 동작하지 않는다. 그런데 DB에 있는 내용을 그대로 출력해야 할 경우도 있다.

 

특정 부분에 HTML 태그가 동작하도록 하려면 autoescape off 설정을 추가해 주어야 한다.

 

autoescape off

 

 

 autoescape off 설정을 한 후 답변을 보면 태그가 작동하는 것을 볼 수 있다.

 

 

 

 

 

 

 

태그가 작동하는 것을 확인했다면, 스크립트 코드가 작동하는 것이므로 쿠키값을 얻을 수 있다.

 

 

safe 필터

autoescape off 설정과 같은 기능을 한다.

 

 

 

mark_safe 함수

 

  • pybo\views.py 수정
def detail(request, question_id): 
    # question = Question.objects.get(id=question_id)
    question = get_object_or_404(Question, pk=question_id)
    msg = "<script> alert('xss') </script>"
    
    context = { 'question': question, 'msg': msg }

    return render(request, 'pybo/question_detail.html', context)

 

 

실행 가능한 스크립트 코드를 템플릿으로 전달하는 코드를 추가한다.

 

 

  • templates\pybo\question_detali.html 수정
<h5 class="border-bottom my-3 py-2">
		{{ question.answer_set.count }}개의 답변이 있습니다. ({{ msg }})
</h5>

 

전달된 msg를 출력하는 코드를 추가한다.

 

 

  • 확인

 

DTL에 의해서 태그가 HTML 인코딩되어 단순 문자열로 출력된다.

 

이 스크립트를 이스케이프 처리 하지 않도록 지정할 수 있다.

 

  • pybo\views.py 수정
from django.shortcuts import render, get_object_or_404, redirect
from .models import Question
from .forms import QuestionForm
from django.utils import timezone
import subprocess
from django.utils.safestring import mark_safe	# 추가

def index(request):
    question_list = Question.objects.order_by('-create_date')
    context = { 'question_list': question_list }
    
    return render(request, 'pybo/question_list.html', context)


def detail(request, question_id): 
    # question = Question.objects.get(id=question_id)
    question = get_object_or_404(Question, pk=question_id)
    msg = "<script> alert('xss') </script>"
    msg = mark_safe(msg)	# mark_safe 함수 추가

    context = { 'question': question, 'msg': msg }

    return render(request, 'pybo/question_detail.html', context)

 

 

  • 확인

스크립트 코드가 실행되는 것을 확인할 수 있다.

 

  • 결론

Django에서는 DTL이 HTML 엔티티를 자동으로 HTML 인코딩 처리하여 단순 문자열로 출력되도록 하고 있으나, 

mark_safe() 함수, autoescape off 설정, safe 필터 등을 사용하는 경우 해당 기능을 무효화할 수 있으므로 유의해서 사용해야 한다.

 

 

3. CSRF(크로스 사이트 요청 위조)

요청을 전달받은 서버가 요청의 절차와 주체를 검증하지 않고 요청을 처리했을 때 발생

 

  • 희생자의 권한으로 요청이 처리되는 문제 발생
  • 자동 회원가입, 자동 글쓰기, 광고 배너 클릭 등에 악용

 

1) 동작 원리

 

만약 패스워드 변경 처리 페이지가 있을 때 아래와 같이 기능을 구현했다고 가정한다.

 

  1. 로그인(인증) 여부 확인
    • 로그인해야만 패스워드 변경 가능
  2. 처리에 필요한 사용자 입력값이 전달되었는지 확인
    • 패스워드 변경을 위한 사용자가 입력한 새 패스워드가 전달되었는지 확인
  3. 처리에 필요한 시스템이 가지고 있는 값을 추출
    • 패스워드 변경을 위한 변경 대상 정보(사용자 ID)를 세션으로부터 추출
  4. 요청을 처리
    • 로그인한 사용자의 패스워드를 요청 파라미터로 전달된 값으로 변경


위와 같은 기능을 구현한 회원제 게시판에 공격자가 게시글을 남긴다면 어떻게 될까?

많은 사람이 클릭할 수 있도록 유도해 제목을 정하고, 내용도 사람들이 흥미를 느끼는 내용으로 공격자가 글을 게시할 때 공격자는 게시글에 HTML 태그를 포함해 작성한다.

 

<iframe src="ChangePasswordProc?newpw=1234&newpwre=1234" width="0" height="0"></iframe>

 

iframe은 한 페이지 안에 다른 HTML 문서를 삽입할 때 사용하는 태그다. 눈에 보이면 안 되기 때문에 크기를 0으로 설정한다. 그러면 게시글은 보이지만 공격자가 추가한 태그의 문서는 보이지 않게 된다.

 

해당 게시물을 보는 모든 사용자의 패스워드가 1234로 변경된다.

 

 

(1) CSRF 취약점 존재

 

정상적인 절차를 확인하지 않고 요청을 처리해서 공격자에 의해 패스워드가 변경되는 것이다. 정상적인 절차 확인은 패스워드 변경을 원하는 사용자가 패스워드 변경을 눌러서 패스워드 변경 페이지로 들어왔는지, 공격자가 작성한 자동화된 요청인지를 확인하는 과정인데 확인하지 않았다. 

 

패스워드 변경이 주요 기능임에도 불구하고 인증 여부 및 요청 처리에 필요한 값 전달 여부만 확인하고 요청을 처리(사용자가 요청한 것인지 자동화된 코드가 요청한 것인지 확인하지 않고 요청을 처리)하는 문제가 있다.

 

 

2) 방어 기법

(1) 텍스트 기반의 토큰을 이용한 요청 절차 검증

 

  1. 신청 페이지가 호출되었을 때 서버에서 임의의 값을 생성하고 세션에 저장
  2. 사용자 화면에 전달
  3. 서버가 가지고 있는 값과 사용자 요청을 통해서 전달된 값을 비교
    • 일치하는 경우에만 정상적인 절차를 통한 요청으로 확인하고 처리

 

<input type="hidden" value="abcd" />

 

서버에서 임의의 값을 생성하고 사용자 화면에 전달하는데. 보이지 않게 하기 위해 hidden으로 전달한다.

abcd는 텍스트 기반의 토큰으로 토큰 전달 과정에 사용자가 관여하지 않는다. 이 토큰은 사용자가 변경 신청 페이지에서 변경 신청을 요청할 때 전달되고, 변경 처리 페이지에서 값을 확인하고 처리한다. 

즉, 이 텍스트 기반의 토큰은 변경 신청 페이지에서 먼저 가지고 있고, 이 토큰을 서버에 전달하게 되는 것이다.

 

이때 공격자가 변경 신청 페이지를 먼저 호출해 토큰을 추출한 후 변경 처리 페이지를 호출하는 방식을 사용한다면 공격이 가능해진다.

 

(2) CAPTCHA

기계는 인식할 수 없으나 사람은 쉽게 인식할 수 있는 텍스트, 이미지를 통해 사람과 기계를 구별하는 프로그램

 

  • 텍스트 기반 토큰의 자동화 처리가 가능한 문제점 해결
  • 요청 과정에 사용자의 입력 추가
  • 자동화된 요청을 방지하기 위한 수단
  • 사용자와의 인터렉션을 통한 요청

 

 

 

  • reCAPTCHA
    • CAPTCHA 이미지가 복잡해 사람도 이해하기 어려워지는 문제점 해결

 

(3) 재인증

 

  • 패스워드 변경할 때 재인증을 위해서 현재 패스워드도 함께 입력

 

인증 방법

 

  1. 지식
    • 패스워드
  2. 소유
    • 주민등록증
    • 인증서
    • 스마트폰
    • OTP
  3. 특징
    • 필기체 서명
    • 정맥
    • 지문
    • 홍채

 

생물학적 특징을 이용한 방법을 바이오 인증, 2개 이상의 이중 방법을 결합한 인증을 Multi factor 인증, 2개의 인증 방법을 결합한 인증을 Tow factor 인증이라고 한다.

 

 

4. Bee box 실습

 

 

 

로그인한 사용자의 패스워드를 변경을 신청하는 페이지다.

 

1) 개발자도구 이용 내용 분석

 

 

<h1>CSRF (Change Password)</h1>
    <p>Change your password.</p>
    <form action="/bWAPP/csrf_1.php" method="GET">
        
        <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>
    <br>

 

 

새 패스워드로 1234를 입력하면 아래와 같은 형태의 요청이 발생한다.

http://bee.box/bWAPP/csrf_1.php?password_new=1234&password_conf=1234&action=change 

 

 

2) 공격 코드 삽입

 

위 요청이 자동으로 발생할 수 있도록 게시판에 공격 코드를 삽입한다.

 

 

Submit 후 로그아웃하고 다시 로그인 하면 원래 패스워드였던 bug로 안 되고 1234로 로그인이 가능한 것을 확인할 수 있다. 이는 csrf_1.php에서 요청 절차와 요청 주체를 확인하지 않고 전달된 파라미터에 의존해서 요청을 처리했기 때문에 발생한 문제다.

 

 

3) 취약한 코드 확인

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

 

코드가 긴 관계로 밑에 주석과 함께 추가한다.

 

더보기
더보기
<?php
include("security.php");
include("security_level_check.php");
include("selections.php");
include("connect_i.php");

$message = "";

				// 요청 처리에 필요한 값이 요청 파라미터로 전달되었는지 확인
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"];  // 요청 처리에 필요한 값을 서버의 세션으로부터 추출
            
            $password_new = mysqli_real_escape_string($link, $password_new); // SQLi 취약점 방어
            $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 . "'";
                // 로그인한 사용자의 패스워드를 요청 파라미터의 값으로 변경 
                // 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		// 보안 등급이 1 또는 2인 경우 
            {
                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 . "'";	
                    // 로그인한 사용자의 아이디의 패스워드가 요청 파라미터로 전달한 현재 패스워드가 일치하는 데이트를 조회
                    // Debugging
                    // echo $sql;    

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

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

                    }
                
                }
                
            }
                           
        } 
    
    }
    
}

 

 

5. Django CSRF 토큰 발행

 프레임워크에서 CSRF 토큰 발행 및 검증 처리

 

 

1) config\settings.py

MIDDLEWARE에 CSRF 항목 활성화

 

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',	
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

 

 

중간에 CSRF가 들어가 있는 것을 확인할 수 있다. 전역적으로 CSRF 차단 기능을 활성화하는 기능이다.

 

 

2) {% csrf_token %}

템플릿에 [% csrf_token %}을 추가함으로 차단 기능 활성화

 

{% extends 'base.html' %}

{% block content %}
<div class="container">
    <h5 class="my-3 border-bottom pb-2">질문 등록</h5>
    <form method="post" class="post-form my-3">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{% endblock %}

 

 

3) 토큰 확인

페이지를 요청할 때마다 토큰이 재발행되고 토큰값을 변경해서 요청하면 오류 발생

 

 

 

4) CSRF 토큰 검증 비활성화

CSRF 취약점이 발생할 수 있으므로 유의

 

(1) 전역적으로 CSRF 토큰 검증 비활성화

 

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
#   'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

 

 

(2) 부분적으로 CSRF 토큰 검증 비활성화

 

from django.shortcuts import render, get_object_or_404, redirect
from .models import Question
from .forms import QuestionForm
from django.utils import timezone
import subprocess
from django.views.decorators.csrf import csrf_exempt # 추가

# 함수 생략

@csrf_exempt					# 추가
def question_create(request):
    if request.method == 'GET':
        form = QuestionForm()
        return render(request, 'pybo/question_form.html', { 'form': form })
    elif request.method == 'POST':
        # POST 방식으로 전달된 요청인 경우, 요청 본문을 통해 전달된 입력값을 저장 
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')

 

 

views.py 함수에 @csrf_exempt 데코레이터를 추가하면 CSRF 토큰이 없거나 변조되어도 함수 처리가 가능해 지문 등록이 가능한 것을 확인할 수 있다.

댓글