[ 환경 ]
DVWA | v1.9 |
Burp Suite | Community Edition v2024.11.2 |
Impossible level은 보안조치가 된 소스코드로 CSRF 보호, Brute Force 방지, 계정 잠금과 같은 추가적인 보안 메커니즘을 포함하고 있다.
if( isset( $_POST[ 'Login' ] ) && isset ($_POST['username']) && isset ($_POST['password']) ) {
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
$user = $_POST[ 'username' ];
$user = stripslashes( $user );
$user = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $user ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = $_POST[ 'password' ];
$pass = stripslashes( $pass );
$pass = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $pass ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
$pass = md5( $pass );
$data = $db->prepare( 'SELECT failed_login, last_login FROM users WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();
if( ( $data->rowCount() == 1 ) && ( $row[ 'failed_login' ] >= $total_failed_login ) ) {
$last_login = strtotime( $row[ 'last_login' ] );
$timeout = $last_login + ($lockout_time * 60);
$timenow = time();
if( $timenow < $timeout ) {
$account_locked = true;
}
}
$data = $db->prepare( 'SELECT * FROM users WHERE user = (:user) AND password = (:password) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR);
$data->bindParam( ':password', $pass, PDO::PARAM_STR );
$data->execute();
$row = $data->fetch();
if( ( $data->rowCount() == 1 ) && ( $account_locked == false ) ) {
$avatar = $row[ 'avatar' ];
$failed_login = $row[ 'failed_login' ];
$last_login = $row[ 'last_login' ];
echo "<p>Welcome to the password protected area <em>{$user}</em></p>";
echo "<img src=\"{$avatar}\" />";
sleep( rand( 2, 4 ) );
echo "<pre><br />Username and/or password incorrect.<br /><br/>Alternative, the account has been locked because of too many failed logins.<br />If this is the case, <em>please try again in {$lockout_time} minutes</em>.</pre>";
$data = $db->prepare( 'UPDATE users SET failed_login = (failed_login + 1) WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
$data = $db->prepare( 'UPDATE users SET last_login = now() WHERE user = (:user) LIMIT 1;' );
$data->bindParam( ':user', $user, PDO::PARAM_STR );
$data->execute();
generateSessionToken();
이 코드는 보안 기능이 강력하지만 몇가지 개선점을 반영하면 더욱 안전한 로그인 시스템을 구축할 수 있다.
다음은 앞서 살펴본 코드를 기반으로 개선한 코드의 예제로 주요 보안 및 기능적 향상 방안을 설명하겠다.
기존 코드에서 md5()를 사용해 비밀번호를 해싱한다. md5는 오래된 해싱 알고리즘으로, 보안성이 약하며 충돌 가능성이 높다. 이를 개선하기 위해 password_hash()와 password_verify()를 사용하여 안전한 해싱과 검증을 구현한다.
데이터베이스에 비밀 번호를 저장할 때
$hashed_password = password_hash($pass, PASSWORD_BCRYPT);
비밀번호 검증 시
if (password_verify($pass, $row['password'])) {
// 비밀번호 검증 성공
} else {
// 비밀번호 검증 실패
}
기존 코드에서는 CSRF 토큰 검증을 수행하지만, 토큰 재생성 및 만료 관리는 코드에 포함되지 않았다. 이를 개선하기 위해 CSRF 토큰 생성 시 고유성과 만료 시간을 관리하는 코드를 추가한다. 유효 기간이 지난 토큰은 무효화하고 새로 생성한다.
function generateCSRFToken() {
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_token_expiry'] = time() + 3600; // 1시간 유효
return $token;
}
function validateCSRFToken($user_token) {
if (!isset($_SESSION['csrf_token']) || !isset($_SESSION['csrf_token_expiry'])) {
return false;
}
if (time() > $_SESSION['csrf_token_expiry']) {
return false; // 토큰 만료
}
return hash_equals($_SESSION['csrf_token'], $user_token);
}
기존 코드에서는 사용자 입력값을 출력할 때 htmlspecialchars( )로 이스케이프 처리하지 않아 XSS 공격에 취약하다. 이를 개선하기 위해 출력 시 항상 htmlspecialchars()를 사용하여 특수 문자를 이스케이프 처리한다.
echo "<p>Welcome to the password protected area <em>" . htmlspecialchars($user, ENT_QUOTES, 'UTF-8') . "</em></p>";
echo "<img src=\"" . htmlspecialchars($avatar, ENT_QUOTES, 'UTF-8') . "\" />";
기존 코드에서는 실패 횟수를 기록하고 계정을 잠그는 방식은 좋지만, IP 기반의 제한 기능이 없다. 이를 개선하기 위해 IP 주소를 기록하고 실패 횟수가 많은 경우 IP 기반 제한을 추가하는 것이 좋다.
function logFailedLoginAttempt($user, $ip_address) {
global $db;
$stmt = $db->prepare('INSERT INTO login_attempts (user, ip_address, attempt_time) VALUES (:user, :ip, NOW())');
$stmt->bindParam(':user', $user, PDO::PARAM_STR);
$stmt->bindParam(':ip', $ip_address, PDO::PARAM_STR);
$stmt->execute();
}
function isIPBlocked($ip_address, $threshold, $timeframe) {
global $db;
$stmt = $db->prepare('SELECT COUNT(*) FROM login_attempts WHERE ip_address = :ip AND attempt_time > (NOW() - INTERVAL :timeframe MINUTE)');
$stmt->bindParam(':ip', $ip_address, PDO::PARAM_STR);
$stmt->bindParam(':timeframe', $timeframe, PDO::PARAM_INT);
$stmt->execute();
$count = $stmt->fetchColumn();
return $count >= $threshold;
}
기존 코드에서는 실패 메시지에서 계정이 잠겼는지, 비밀번호가 틀렸는지를 명확히 구분하지 않고 제공하면 정보 노출 가능성이 있다. 이를 개선하기 위해 실패 메시지를 일반화하여 정보 노출을 방지하는 것이 좋다.
echo "<pre><br />Invalid login credentials. Please try again later.</pre>";
기존 코드에서는 Prepared Statements를 사용하고 있지만, 사용자 입력값을 이중으로 필터링 (stripslashes() mysqli_real_ escape_string ())하는 불필요한 처리가 포함되어 있다. 이를 개선하기 위해 Prepared Statements로 사용자 입력값을 안전하게 처리하므로 추가적인 필터링은 필요하지 않니다. 기존 필터링을 제거하고 다음과 같이 간단히 작성한다.
$data = $db->prepare('SELECT * FROM users WHERE user = :user AND password = :password LIMIT 1;');
$data->bindParam(':user', $user, PDO::PARAM_STR);
$data->bindParam(':password', $pass, PDO::PARAM_STR);
$data->execute();
기존 코드에서는 실패 시 로그는 기록하지만 성공적인 로그인 시도에 대한 로그는 포함되어 있지 않이 때문에 성공적인 로그인 기록을 추가하는것이 좋다.
function logSuccessfulLogin($user, $ip_address) {
global $db;
$stmt = $db->prepare('INSERT INTO login_logs (user, ip_address, login_time) VALUES (:user, :ip, NOW())');
$stmt->bindParam(':user', $user, PDO::PARAM_STR);
$stmt->bindParam(':ip', $ip_address, PDO::PARAM_STR);
$stmt->execute();
}
기존 코드에서는 세션 하이재킹 방지를 위한 추가적인 조치가 포함되어 있지 않기 때문에 로그인 성공 시 세션 재재성을 통해 하이재킹을 방지한다.
session_regenerate_id(true); // 세션 ID 재생성
DVWA : Command Injection - Medium level (0) | 2024.12.29 |
---|---|
DVWA : Command Injection - Low level (0) | 2024.12.29 |
DVWA : Brute Force - High level (0) | 2024.12.29 |
DVWA : Brute Force - Medium level (0) | 2024.12.28 |
DVWA : Brute Force - Low level (0) | 2024.12.28 |