Vulnerability Assessment/Web Application
DVWA : SQL Injection - Impossible level
DarkSoul.Story
2025. 1. 14. 12:02
반응형
[ 환경 ]
DVWA | v1.9 |
Burp Suite | Community Edition v2024.11.2 |
1. Source code analysis
CSRF 토큰 검증
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
- checkToken( ) 함수는 CSRF 토큰을 검증한다.
- 사용자가 요청에 포함한 user_token과 세션에 저장된 session_token을 비교한다,
- 두 토큰이 다르거나 세션에 유효한 토큰이 없으면 CSRF 공격으로 간주하고 index.php로 리디렉션한다.
사용자 입력값 가져오기
$id = $_GET[ 'id' ];
- 사용자가 입력한 id 값을 GET 요청에서 가져온다.
- 데이터베이스에서 조회할 id 값을 획득
숫자인지 확인
if(is_numeric( $id )) {
- is_numeric( ) 함수를 사용하여 입력값이 숫자인지 확인한다.
- id는 숫자로만 이루어져야 하므로, SQL Injection 등의 악성 입력을 방지하기 위한 기본적인 필터링
데이터베이스 조회
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
- PDO::prepare( )를 사용하여 SQL 쿼리를 준비한다.
- 쿼리: SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1
- :id는 플레이스홀더로, 사용자 입력값이 직접 SQL에 삽입되지 않는다.
- bindParam()으로 사용자 입력값($id)을 플레이스홀더 :id에 바인딩하며, 데이터 타입은 PDO::PARAM_INT로 명시
- execute()로 쿼리를 실행
- fetch()로 실행 결과에서 첫 번째 행을 가져온다.
- SQL Injection 공격 방지
- 사용자 ID에 해당하는 이름과 성을 데이터베이스에서 가져온다.
결과 행 수 확인
if( $data->rowCount() == 1 ) {
- rowCount()를 사용해 쿼리 결과 행의 개수를 확인한다.
- 반환된 결과가 하나일 경우에만 다음 로직을 실행한다.
- 조회 결과가 정확히 하나의 행인지 확인
- 다중 결과 반환이나 비정상적인 상황 방지
결과 처리 및 출력
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
- $row에서 first_name과 last_name 값을 가져온다.
- HTML <pre> 태그를 사용하여 사용자에게 결과를 출력
- ID, First name, Surname이 포함된 포맷으로 출력된다.
- 사용자 입력값 $id와 조회 결과인 $first, $last를 이스케이프 처리하지 않고 출력되므로, XSS(Cross-Site Scripting) 공격에 취약
CSRF 토큰 생성
generateSessionToken();
- 새로운 CSRF 토큰을 생성하고 세션에 저장
- 다음 요청에서도 CSRF 방어를 지속하기 위해 새로운 토큰 생성
2. Source code improvements
앞서 살펴본 소스코드는 SQL Injection 공격을 방어할 수 있는지만 XSS 공격에는 취약하다.
XSS 공격 가능성
사용자 입력값 $id를 그대로 출력하기 때문에 XSS 공격에 취약하다. 이를 해결하기 위해 출력 시 htmlspecialchars()를 사용하여 HTML 특수 문자를 이스케이프 처리한다.
echo "<pre>ID: " . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . "<br />";
echo "First name: " . htmlspecialchars($first, ENT_QUOTES, 'UTF-8') . "<br />";
echo "Surname: " . htmlspecialchars($last, ENT_QUOTES, 'UTF-8') . "</pre>";
사용자 피드백 부족
숫자가 아닌 값을 입력하거나 조회 결과가 없을 경우 사용자에게 아무런 피드백을 제공하지 않는다. 이를 해결하기 위해 입력값이 숫자가 아니거나 결과가 없을 경우 적절한 메시지를 출력한다.
if (!is_numeric($id)) {
echo "<pre>Invalid ID format. Please enter a numeric ID.</pre>";
} elseif ($data->rowCount() === 0) {
echo "<pre>No user found with the given ID.</pre>";
}
개선된 코드
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
if (!checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' )) {
die("<pre>Invalid CSRF token. Request denied.</pre>");
}
$id = $_GET[ 'id' ];
if (!is_numeric($id)) {
echo "<pre>Invalid ID format. Please enter a numeric ID.</pre>";
} else {
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
$row = $data->fetch();
if ($data->rowCount() == 1) {
$first = htmlspecialchars($row['first_name'], ENT_QUOTES, 'UTF-8');
$last = htmlspecialchars($row['last_name'], ENT_QUOTES, 'UTF-8');
echo "<pre>ID: " . htmlspecialchars($id, ENT_QUOTES, 'UTF-8') . "<br />First name: {$first}<br />Surname: {$last}</pre>";
} else {
echo "<pre>No user found with the given ID.</pre>";
}
}
}
generateSessionToken();
?>
요약
CSRF 보호
- checkToken() 함수로 요청의 유효성을 확인
- 요청마다 새 CSRF 토큰을 생성
XSS 방지
- 사용자 입력값과 출력 결과를 htmlspecialchars()로 이스케이프 처리
유효성 검사
- 입력값이 숫자인지 확인
- 조회 결과가 없는 경우 사용자에게 메시지를 제공
보안 강화
- SQL Injection 방지: Prepared Statements 사용
- 출력 값 필터링: 특수 문자 이스케이프
반응형