일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- 로그인페이지
- 게시판 만들기
- XSS
- JS
- Error based sql injection
- css
- php
- sql injection point
- union sql injection
- sql injection
- lord of sql injection
- 웹 개발
- cookie 탈취
- 웹 해킹
- 모의해킹
- 세션
- 문제 풀이
- 증적 사진
- Python
- 보안 패치
- 로그인
- CTF
- blind sql injection
- 과제
- 보고서
- Los
- csrf
- 웹개발
- MySQL
- file upload
- Today
- Total
Almon Dev
Stored XSS 보안 패치 본문
1. 개요
XSS 이란?
XSS(Cross-Site Scripting)는 웹 애플리케이션에서 사용자 입력값 검증이 미흡할 경우 발생하는 보안 취약점입니다. 공격자는 XSS 취약점을 악용해 자바스크립트, HTML 태그를 삽입하여 피해자의 브라우저에서 실행되도록 유도할 수 있으며, 이를 통해 세션 탈취, 피싱, 키로깅 등의 공격이 가능합니다. XSS의 종류는 Reflected XSS, Stored XSS, DOM-based XSS가 있습니다.
Stored XSS 이란?
Stored XSS는 악성 스크립트를 서버에 저장한 후, 페이지에 접속하는 모든 사용자의 브라우저에서 악성스크립트가 실행되는 공격 방식입니다.
Stored XSS 공격 흐름
1. 공격자가 게시글, 닉네임, 파일 이름, 댓글 등 사용자 입력값을 이용해 자바스크립트, HTML 태그를 삽입합니다.
2. 삽입된 자바스크립트, HTML 태그가 서버에 저장됩니다.
3. 사용자가 저장된 자바스크립트, HTML 태그가 포함된 페이지에 접속하면 악성 스크립트가 실행됩니다.
4. 사용자의 세션이 탈취되거나, 키로깅, CSRF 공격 연계 등의 공격이 가능합니다.
Stored XSS의 위험성
1. 피해자가 악성 페이지에 접속만 해도 스크립트가 실행됩니다.
2. 서버에 저장된 스크립트가 포함된 페이지에 접속하는 모든 사용자가 공격의 대상이 될 수 있습니다.
보안 패치의 목적 및 중요성
이번 보안 패치는 Stored XSS 취약점을 제거하여 사용자의 개인정보 보호 및 웹 애플리케이션 보안 강화를 목표로 합니다. 이를 통해 악성 스크립트 실행 차단, 사용자 정보 보호, 웹 사이트 신뢰성을 확보할 수 있습니다.
HTML Entity 이란?
HTML Entity는 HTML에서 특정 특수 문자를 안전하게 출력하기 위해 사용하는 문자 이스케이프 코드입니다. 예를 들어 <script> 태그를 그대로 출력할 경우 브라우저가 스크립트를 실행하기 때문에, 이를 방지하기 위해 HTML Entity로 변환하여 < script>로 출력해 실행되지 않도록 해야 합니다.
HTML 엔터티 코드표
문자 | HTML Entity |
< | < |
> | > |
" | " |
' | ' |
& | & |
2. 취약점 분석
Stored XSS 발생 원인
파일명, 닉네임 등의 입력값을 서버에서 적절히 검사하지 않아 악성 스크립트가 서버에 저장되어 Stored XSS가 발생합니다.
CASE 1. 프로필 사진 업로드
프로필 사진을 업로드할 때 파일명 검사가 미흡해 데이터베이스에 악성 스크립트가 저장되고, 해당 이미지를 수정하기 위해 input 태그에 파일명을 삽입하면 스크립트가 실행되는 케이스입니다.
mypage_update_proc.php
....
if($file['error'] == UPLOAD_ERR_OK) {
$file_name = basename($file['name']);
$path_info = pathinfo($file_name);
$file_extension = strtolower($path_info['extension']);
if (!in_array($file_extension, $extension)) {
die("파일 확장자를 확인해 주세요. \n'jpg', 'jpeg', 'png', 'gif'만 사용이 가능합니다.");
}
// 파일 이름을 검사하지 않고 SQL 쿼리에 삽입해 데이터베이스에 저장
$sql = $sql . ", profile_img_path='$file_name'";
$upload_path = $upload_dir . $file_name;
if(move_uploaded_file($file['tmp_name'], $upload_path)) {
$img_result = true;
}
}
....
mypage_update.php
<?php
....
$sql = "select profile_img_path, nickname, email, password, name from users where user_id='$user_id'";
....
$img_path = $result['profile_img_path'] ?? "almond-profile.jpg";
....
?>
<!DOCTYPE html>
<html lang="ko">
<body>
....
<label for="profile_img">프로필 : </label>
<input type="file" id="profile_img_input" name="profile_img" style="display: none;" accept=".jpg, .png, .gif, .jpeg">
<!-- 데이터베이스에 저장된 파일 경로를 그대로 가져와서 출력 -->
<img src="/upload/profile_img/<?=$img_path ?>" class="profile_img" id="profile_img" onclick="profileImgUpload()">
</body>
</html>
CASE 2. 닉네임 생성 및 수정
닉네임을 생성, 수정할 때 검사가 미흡해 데이터베이스에 악성 스크립트가 저장되고, 닉네임을 불러올 때 HTML Entity로 변환하는 과정을 누락하여 스크립트가 삽입돼 실행되는 케이스입니다.
sign_up_proc.php
<?php
....
$nick = $_POST['nickname'];
....
if (!$id_duple && !$pass_duple && !$nick_duple) {
$result = true;
$pass = password_hash($pass, PASSWORD_ARGON2ID);
// 닉네임을 검사하지 않고 SQL 쿼리에 삽입해 데이터베이스에 저장
runSQL("insert into users (name, user_id, nickname, email, password, login) values ('$name', '$id', '$nick', '$email', '$pass', '0')");
}
mypage_update_proc.php
<?php
....
$nickname = $_POST['nickname'] ?? null;
....
if ($nickname != null) {
// 닉네임을 검사하지 않고 SQL 쿼리에 삽입해 데이터베이스에 저장
$sql = $sql . ", nickname='$nickname'";
}
....
$result = runSQL($sql);
login_successful.php
<?php
....
$nickname = "";
....
$user_id = $token->sub;
$sql = "select nickname,profile_img_path from users where user_id='$user_id'";
$sql_result = runSQL($sql)->fetch_array();
$nickname = $sql_result["nickname"];
....
<!DOCTYPE html>
<html lang="ko">
<body>
....
// 닉네임을 HTML Entity로 변환하지 않고 HTML 코드에 삽입
<h2 class="profile_name"><?= $nickname; ?></h2>
<p class="profile_subs">구독자 <strong>0명</strong></p>
....
</body>
</html>
CASE 3. 게시글 작성
게시글 작성 시 파일 업로드 과정에서 파일 확장자 검사가 미흡해 데이터베이스에 악성 스크립트가 저장되고, 다운로드 기능을 위해 해당 파일 경로를 가져올 때 악성 스크립트가 HTML 코드에 삽입되어 스크립트가 실행되는 케이스입니다.
write_post.php
<?php
....
if (isset($_FILES['files'])) {
$files = $_FILES['files'];
$count = count($files['name']);
$files_path = "";
$upload_dir = "../upload/files/";
for ($i = 0; $i < $count; $i++) {
if (isset($files['name'][$i])) {
$file_name = basename($files['name'][$i]);
$path_info = pathinfo($file_name);
// 파일 확장자 추출
$file_extension = strtolower($path_info['extension']);
do {
// 파일명을 랜덤한 숫자로 변경
$random_number = mt_rand(1, 1000000);
$upload_path = $upload_dir . $random_number . '.' . $file_extension;
} while (file_exists($upload_path));
if (move_uploaded_file($files['tmp_name'][$i], $upload_path)) {
// 파일 확장자를 검사하지 않고 파일 경로에 삽입
$files_path = $files_path . "$file_name:$random_number.$file_extension;";
}else {
$fail_msg = $fail_msg . "[$file_name]";
}
}
}
// 파일 경로를 SQL 쿼리에 삽입해 데이터베이스에 저장
$sql = $sql . ", '$files_path');";
....
runSQL($sql);
read_post.js
....
// 파일 추가
if (post.file_path != null) {
const uploadDir = '/forum/upload/files/';
const filesName = post.file_path.split(';');
filesName.forEach((files) => {
if (files != '') {
const fileName = files.split(':');
const filePath = uploadDir + fileName[1];
// 데이터베이스에서 가져온 파일 경로를 HTML 태그에 삽입할 때 HTML Entity로 변환하는 과정이 누락됨
const tag = `<p class="content-text file-download"><a href="${filePath}" download="${fileName[0]}">${fileName[0]}</a></p>`;
contentWrapper.innerHTML += tag;
}
});
}
....
CASE 4. 게시글 수정
게시글 수정 시 파일 업로드 과정에서 파일 확장자 검사가 미흡해 데이터베이스에 악성 스크립트가 저장되고, 다운로드 기능을 위해 해당 파일 경로를 가져올 때 악성 스크립트가 HTML 코드에 삽입되어 스크립트가 실행되는 케이스입니다.
update_post.php
<?php
....
$sql = "update posts set category_id=$category_id, title='$title', content='$content'";
if (isset($_FILES['files'])) {
$files = $_FILES['files'];
$count = count($files['name']);
$files_path = "";
$upload_dir = "../upload/files/";
for ($i = 0; $i < $count; $i++) {
if (isset($files['name'][$i])) {
$file_name = basename($files['name'][$i]);
$path_info = pathinfo($file_name);
// 파일 확장자 추출
$file_extension = strtolower($path_info['extension']);
do {
// 파일명을 랜덤한 숫자로 변경
$random_number = mt_rand(1, 1000000);
$upload_path = $upload_dir . $random_number . '.' . $file_extension;
} while (file_exists($upload_path));
if (move_uploaded_file($files['tmp_name'][$i], $upload_path)) {
// 확장자를 검사하지 않고 파일 경로에 삽입
$files_path = $files_path . "$file_name:$random_number.$file_extension;";
}
}
// 확장자를 검사하지 않고 SQL 쿼리에 삽입해 데이터베이스에 저장
$sql = $sql . ", file_path='$db_file_path" . "$files_path'";
....
runSQL($sql);
read_post.js
....
// 파일 추가
if (post.file_path != null) {
const uploadDir = '/forum/upload/files/';
const filesName = post.file_path.split(';');
filesName.forEach((files) => {
if (files != '') {
const fileName = files.split(':');
const filePath = uploadDir + fileName[1];
// 데이터베이스에서 가져온 파일 경로를 HTML 태그에 삽입할 때 HTML Entity로 변환하는 과정이 누락됨
const tag = `<p class="content-text file-download"><a href="${filePath}" download="${fileName[0]}">${fileName[0]}</a></p>`;
contentWrapper.innerHTML += tag;
}
});
}
....
Stored XSS 예시
1. 파일 업로드
파일명, 확장자 검사가 미흡한 점을 악용하여 Stored XSS가 가능합니다.
1. 게시판에서 정상적인 파일을 업로드합니다.
2. 웹 프록시 툴을 이용하여 요청을 가로채 파일 확장자에 자바스크립트를 삽입합니다.
파일이름에 \"를 삽입할 수 없기 때문에 요청을 가로채 수정하는 것입니다.
3. 해당 파일의 다운로드 링크를 클릭하면 스크립트가 실행됩니다.
2. 닉네임 생성 및 수정
닉네임 생성, 수정 시 검사가 미흡한 점을 악용하여 Stored XSS가 가능합니다.
1. 닉네임에 onclick 이벤트 핸들러와 자바스크립트를 삽입하여 회원가입합니다.
2. 프로필 페이지에서 onclick 이벤트 핸들러가 삽입된 닉네임을 클릭합니다.
3. 자바스크립트가 실행됩니다.
3. 보안 패치 적용
보안 조치 방법
1. HTML Entity로 변경
2. 파일명 필터링
HTML Entity로 변경이 불가능하거나 변경 시 파일 접근에 문제가 발생하는 경우 파일명에서 한글, 숫자, 알파벳 등 안전한 문자만 사용이 가능하도록 검사 및 필터링하는 과정을 추가합니다.
PHP 예시)
// [알파벳, 숫자, 마침표(.), 밑줄(_), 하이픈(-)을]를 제외한 문자를 공백으로 치환
$file_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);;
수정 코드
1. 프로필 사진 업로드
프로필 사진 경로를 데이터베이스에 저장할 때 htmlspecialchars 함수를 이용해 특수문자를 HTML Entity로 변경하는 로직을 추가하였으나 img 태그의 src 속성을 이용해 이미지를 가져오는데 &나 ;같은 특수문자를 url 인코딩을 하지 않아 문제가 발생했습니다. 따라서 제가 작성한 보안 권고안과 다르게 a-z, A-Z, 0-9와 같이 알파벳과 숫자만 파일 이름에 사용 가능하도록 정규식을 이용해 처리하는 로직을 추가했습니다.
mypage_update_proc.php
....
if($file['error'] == UPLOAD_ERR_OK) {
$base_name = basename($file['name']);
$path_info = pathinfo($base_name);
$file_extension = strtolower($path_info['extension']);
// 파일의 이름 추출
$file_name = $path_info['filename'];
if (!in_array($file_extension, $extension)) {
die("파일 확장자를 확인해 주세요. \n'jpg', 'jpeg', 'png', 'gif'만 사용이 가능합니다.");
}
// 정규식으로 파일 이름에서 숫자와 알파벳을 제외한 문자 삭제
$file_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $file_name);
$sql = $sql . ", profile_img_path='$file_name'";
$upload_path = $upload_dir . $file_name;
if(move_uploaded_file($file['tmp_name'], $upload_path)) {
$img_result = true;
}
}
....
2. 닉네임 생성 및 수정
닉네임을 생성, 수정할 때 htmlspecialchars 함수를 이용해 특수문자를 HTML Entity로 변경한 뒤 데이터베이스에 저장하는 것으로 Stored XSS를 방지했습니다.
sign_up_proc.php
<?php
....
$nick = $_POST['nickname'];
....
if (!$id_duple && !$pass_duple && !$nick_duple) {
$result = true;
$pass = password_hash($pass, PASSWORD_ARGON2ID);
// 이름, 아아디, 닉네임 등 사용자 입력값을 HTML Entity로 치환
$name = htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
$id = htmlspecialchars($id, ENT_QUOTES, 'UTF-8');
$nick = htmlspecialchars($nick, ENT_QUOTES, 'UTF-8');
runSQL("insert into users (name, user_id, nickname, email, password, login) values ('$name', '$id', '$nick', '$email', '$pass', '0')");
}
mypage_update_proc.php
<?php
....
$nickname = $_POST['nickname'] ?? null;
....
if ($nickname != null) {
// 닉네임을 HTML Entity로 치환하는 로직을 추가
$nickname = htmlspecialchars($nickname, ENT_QUOTES, 'UTF-8');
$sql = $sql . ", nickname='$nickname'";
}
....
$result = runSQL($sql);
3. 게시글 파일 업로드
게시글 작성, 수정을 할 때 파일 업로드 기능에서 a-z, A-Z, 0-9와 같이 알파벳과 숫자만 파일 이름에 사용 가능하도록 정규식을 이용해 처리하는 로직을 추가했습니다.
write_post.php
<?php
....
if (isset($_FILES['files'])) {
$files = $_FILES['files'];
$count = count($files['name']);
$files_path = "";
$upload_dir = "../upload/files/";
for ($i = 0; $i < $count; $i++) {
if (isset($files['name'][$i])) {
$file_name = basename($files['name'][$i]);
// [알파벳, 숫자, 마침표(.), 밑줄(_), 하이픈(-)]을 제외한 문자를 모두 공백으로 치환합니다.
$file_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $file_name);
$path_info = pathinfo($file_name);
$file_extension = strtolower($path_info['extension']);
do {
// 파일명을 랜덤한 숫자로 변경
$random_number = mt_rand(1, 1000000);
$upload_path = $upload_dir . $random_number . '.' . $file_extension;
} while (file_exists($upload_path));
if (move_uploaded_file($files['tmp_name'][$i], $upload_path)) {
$files_path = $files_path . "$file_name:$random_number.$file_extension;";
}else {
$fail_msg = $fail_msg . "[$file_name]";
}
}
}
// 파일 경로를 SQL 쿼리에 삽입해 데이터베이스에 저장
$sql = $sql . ", '$files_path');";
....
runSQL($sql);
update_post.php
<?php
....
$sql = "update posts set category_id=$category_id, title='$title', content='$content'";
if (isset($_FILES['files'])) {
$files = $_FILES['files'];
$count = count($files['name']);
$files_path = "";
$upload_dir = "../upload/files/";
for ($i = 0; $i < $count; $i++) {
if (isset($files['name'][$i])) {
$file_name = basename($files['name'][$i]);
// [알파벳, 숫자, 마침표(.), 밑줄(_), 하이픈(-)]을 제외한 문자를 모두 공백으로 치환합니다.
$file_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $file_name);
$path_info = pathinfo($file_name);
// 파일 확장자 추출
$file_extension = strtolower($path_info['extension']);
do {
// 파일명을 랜덤한 숫자로 변경
$random_number = mt_rand(1, 1000000);
$upload_path = $upload_dir . $random_number . '.' . $file_extension;
} while (file_exists($upload_path));
if (move_uploaded_file($files['tmp_name'][$i], $upload_path)) {
$files_path = $files_path . "$file_name:$random_number.$file_extension;";
}
}
// 파일 경로를 SQL 쿼리에 삽입해 데이터베이스에 저장
$sql = $sql . ", file_path='$db_file_path" . "$files_path'";
....
runSQL($sql);
4. 보안 패치 결과
보안 패치 후 테스트 결과
프로필 사진 업로드
보안 패치 후 파일명을 수정해 자바스크립트를 삽입해도 알파벳과 숫자를 제외한 모든 문자가 삭제되므로 Stored XSS가 실행되지 않음을 확인했습니다.
1. 개인정보 수정 페이지에 접속합니다.
2. 프로필 사진을 선택하고 수정 요청을 보냅니다.
3. 요청을 가로채 파일명에 자바스크립트를 삽입합니다.
4. Stored XSS가 발생한 페이지에 접속합니다.
5. 스크립트가 실행되지 않음을 확인합니다.
6. 파일명에서 알파벳과 숫자를 제외한 문자가 모두 삭제된 것을 확인합니다.
닉네임 생성 및 수정
보안 패치 후 닉네임에 자바스크립트를 삽입해도 HTML Entity로 치환되기 때문에 Stored XSS가 실행되지 않음을 확인했습니다.
1. 닉네임에 스크립트를 삽입한 뒤 회원가입을 합니다.
2. MySQL 로그를 확인해 특수문자가 HTML Entity로 치환된 것을 확인합니다.
3. 개인정보 수정 페이지로 접속합니다.
4. 닉네임 입력값에 XSS에 사용되는 특수문자를 삽입하고 수정 요청을 보냅니다.
5. 닉네임이 HTML Entity로 치환된 것을 확인합니다.
게시글 파일 업로드
보안 패치 후 게시글의 생성 및 수정 과정에서 파일 업로드 시 파일명을 수정하여 자바스크립트를 삽입해도 숫자와 알파벳을 제외한 모든 문자가 삭제되기 때문에 Stored XSS가 실행되지 않음을 확인했습니다.
1. 게시글 작성 페이지에서 정상적인 파일을 선택하여 등록 요청을 보냅니다.
2. 요청을 가로채 파일 확장자에 자바스크립트를 삽입합니다.
3. 파일 확장자에서 알파벳과 숫자를 제외한 모든 문자가 삭제된 것을 확인합니다.
4. 다운로드 기능이 정상 작동하는 것을 확인합니다.
'웹 해킹 > 웹 개발' 카테고리의 다른 글
정보 누출 보안 패치 (0) | 2025.04.05 |
---|---|
디렉터리 인덱싱 보안 패치 (0) | 2025.04.04 |
SQL Injection 보안 패치 (0) | 2025.03.31 |
게시판 만들기 #10 (게시글 내부에 이미지 추가) (0) | 2025.02.13 |
게시판 만들기 #9 (게시글 검색과 정렬) (0) | 2025.01.31 |