[KVE-2026-0610]그누보드5 자동 로그인 취약점 수정

This commit is contained in:
thisgun committed 2026-04-16 01:31:19 +00:00
1 parent 7e65a297bf
commit 4d5c597665
7 files changed
+152 -14

No files matched your search

+23
View File
@@ -351,6 +351,29 @@ if (defined('G5_USE_SHOP') && G5_USE_SHOP) {
}
}
// 자동 로그인 토큰 테이블 생성 (KVE-2026-0610: 추측 가능한 자동 로그인 쿠키 위조 방지)
// 다중 디바이스 지원을 위해 회원당 여러 토큰을 별도 테이블로 관리
if (!isset($g5['member_auto_login_table'])) {
$g5['member_auto_login_table'] = G5_TABLE_PREFIX.'member_auto_login';
}
if (!sql_query(" DESC `{$g5['member_auto_login_table']}` ", false)) {
sql_query(" CREATE TABLE IF NOT EXISTS `{$g5['member_auto_login_table']}` (
`al_id` int(11) NOT NULL auto_increment,
`mb_id` varchar(20) NOT NULL default '',
`al_token` varchar(64) NOT NULL default '',
`al_user_agent` varchar(255) NOT NULL default '',
`al_ip` varchar(45) NOT NULL default '',
`al_created` datetime DEFAULT NULL,
`al_last_used` datetime DEFAULT NULL,
`al_expire` datetime DEFAULT NULL,
PRIMARY KEY (`al_id`),
UNIQUE KEY `al_token` (`al_token`),
KEY `mb_id` (`mb_id`),
KEY `al_expire` (`al_expire`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ", true);
$is_check = true;
}
$is_check = run_replace('admin_dbupgrade', $is_check);
$db_upgrade_msg = $is_check ? 'DB 업그레이드가 완료되었습니다.' : '더 이상 업그레이드 할 내용이 없습니다.<br>현재 DB 업그레이드가 완료된 상태입니다.';
+26 -3
View File
@@ -90,10 +90,33 @@ if($config['cf_use_point']) {
if (isset($auto_login) && $auto_login) {
// 3.27
// 자동로그인 ---------------------------
// 쿠키 한달간 저장
$key = md5($_SERVER['SERVER_ADDR'] . $_SERVER['SERVER_SOFTWARE'] . $_SERVER['HTTP_USER_AGENT'] . $mb['mb_password']);
// 예측 불가능한 랜덤 토큰을 생성하여 g5_member_auto_login 테이블에 저장.
// 다중 디바이스 지원: 디바이스마다 별도의 토큰 행이 생성되며 서로 독립적으로 동작.
$auto_token = get_random_token_string(32); // 64자 hex - 쿠키에는 원본
$auto_token_hash = hash('sha256', $auto_token); // 64자 hex - DB에는 해시
$auto_ua = isset($_SERVER['HTTP_USER_AGENT']) ? substr($_SERVER['HTTP_USER_AGENT'], 0, 255) : '';
$auto_ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '';
$auto_now = G5_TIME_YMDHIS;
$auto_expire = date('Y-m-d H:i:s', G5_SERVER_TIME + 86400 * 31);
// 만료된 토큰 정리 (해당 회원 한정)
sql_query(" delete from {$g5['member_auto_login_table']}
where mb_id = '{$mb['mb_id']}'
and al_expire < '{$auto_now}' ");
// 새 토큰 INSERT (해시값만 저장)
sql_query(" insert into {$g5['member_auto_login_table']}
set mb_id = '{$mb['mb_id']}',
al_token = '{$auto_token_hash}',
al_user_agent = '".addslashes($auto_ua)."',
al_ip = '".addslashes($auto_ip)."',
al_created = '{$auto_now}',
al_last_used = '{$auto_now}',
al_expire = '{$auto_expire}' ");
set_cookie('ck_mb_id', $mb['mb_id'], 86400 * 31);
set_cookie('ck_auto', $key, 86400 * 31);
set_cookie('ck_auto', $auto_token, 86400 * 31); // 쿠키에는 원본 토큰
// 자동로그인 end ---------------------------
} else {
set_cookie('ck_mb_id', '', 0);
+12
View File
@@ -11,6 +11,18 @@ session_unset(); // 모든 세션변수를 언레지스터 시켜줌
session_destroy(); // 세션해제함
// 자동로그인 해제 --------------------------------
// DB에 저장된 해당 디바이스의 자동 로그인 토큰만 무효화
// (다른 디바이스의 자동 로그인은 유지)
// DB에는 토큰의 SHA256 해시가 저장되어 있으므로 비교 시에도 해시값 사용
$logout_token = get_cookie('ck_auto');
$logout_mb_id = get_cookie('ck_mb_id');
if (isset($g5['member_auto_login_table']) && $logout_token && $logout_mb_id && preg_match('/^[a-f0-9]{64}$/', $logout_token)) {
$logout_mb_id = substr(preg_replace("/[^a-zA-Z0-9_]*/", "", $logout_mb_id), 0, 20);
$logout_token_hash = hash('sha256', $logout_token);
sql_query(" delete from {$g5['member_auto_login_table']}
where mb_id = '{$logout_mb_id}'
and al_token = '{$logout_token_hash}' ");
}
set_cookie('ck_mb_id', '', 0);
set_cookie('ck_auto', '', 0);
// 자동로그인 해제 end --------------------------------
+27 -9
View File
@@ -165,6 +165,11 @@ if (file_exists($dbconfig_file)) {
sql_set_charset(G5_DB_CHARSET, $connect_db);
if(defined('G5_MYSQL_SET_MODE') && G5_MYSQL_SET_MODE) sql_query("SET SESSION sql_mode = ''");
if (defined('G5_TIMEZONE')) sql_query(" set time_zone = '".G5_TIMEZONE."'");
// 자동 로그인 토큰 테이블 - 기존 dbconfig.php에 정의되지 않은 경우 fallback
if (!isset($g5['member_auto_login_table'])) {
$g5['member_auto_login_table'] = G5_TABLE_PREFIX.'member_auto_login';
}
} else {
?>
@@ -556,17 +561,30 @@ if (isset($_SESSION['ss_mb_id']) && $_SESSION['ss_mb_id']) { // 로그인중이
$tmp_mb_id = substr(preg_replace("/[^a-zA-Z0-9_]*/", "", $tmp_mb_id), 0, 20);
// 최고관리자는 자동로그인 금지
if (strtolower($tmp_mb_id) !== strtolower($config['cf_admin'])) {
$sql = " select mb_password, mb_intercept_date, mb_leave_date, mb_email_certify, mb_datetime from {$g5['member_table']} where mb_id = '{$tmp_mb_id}' ";
$row = sql_fetch($sql);
if($row['mb_password']){
$key = md5($_SERVER['SERVER_ADDR'] . $_SERVER['SERVER_SOFTWARE'] . $_SERVER['HTTP_USER_AGENT'] . $row['mb_password']);
// 쿠키에 저장된 키와 같다면
$tmp_key = get_cookie('ck_auto');
if ($tmp_key === $key && $tmp_key) {
// 쿠키 값을 g5_member_auto_login 테이블의 토큰과 비교
// 다중 디바이스 지원을 위해 회원당 여러 토큰을 별도 테이블에 저장
$tmp_key = get_cookie('ck_auto');
// 토큰 형식 검증 (64자 hex) - 빈 값/잘못된 형식 차단
if ($tmp_key && preg_match('/^[a-f0-9]{64}$/', $tmp_key)) {
$tmp_key_hash = hash('sha256', $tmp_key); // 쿠키 값을 해시해서 DB와 비교
$sql = " select al.al_id, al.al_expire,
m.mb_intercept_date, m.mb_leave_date, m.mb_email_certify, m.mb_datetime
from {$g5['member_auto_login_table']} al
inner join {$g5['member_table']} m on m.mb_id = al.mb_id
where al.mb_id = '{$tmp_mb_id}'
and al.al_token = '{$tmp_key_hash}'
and al.al_expire > '".G5_TIME_YMDHIS."' ";
$row = sql_fetch($sql);
if (isset($row['al_id']) && $row['al_id']) {
// 차단, 탈퇴가 아니고 메일인증이 사용이면서 인증을 받았다면
if ($row['mb_intercept_date'] == '' &&
$row['mb_leave_date'] == '' &&
(!$config['cf_use_email_certify'] || preg_match('/[1-9]/', $row['mb_email_certify'])) ) {
// 마지막 사용 시간 갱신
sql_query(" update {$g5['member_auto_login_table']}
set al_last_used = '".G5_TIME_YMDHIS."'
where al_id = '{$row['al_id']}' ");
// 세션에 회원아이디를 저장하여 로그인으로 간주
set_session('ss_mb_id', $tmp_mb_id);
if(function_exists('update_auth_session_token')) update_auth_session_token($row['mb_datetime']);
@@ -576,9 +594,9 @@ if (isset($_SESSION['ss_mb_id']) && $_SESSION['ss_mb_id']) { // 로그인중이
exit;
}
}
// $row 배열변수 해제
unset($row);
}
// $row 배열변수 해제
unset($row);
}
}
// 자동로그인 end ---------------------------------------
+22
View File
@@ -554,6 +554,28 @@ CREATE TABLE IF NOT EXISTS `g5_member` (
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `g5_member_auto_login`
-- KVE-2026-0610: 자동 로그인 토큰 저장 (다중 디바이스 지원)
--
DROP TABLE IF EXISTS `g5_member_auto_login`;
CREATE TABLE IF NOT EXISTS `g5_member_auto_login` (
`al_id` int(11) NOT NULL auto_increment,
`mb_id` varchar(20) NOT NULL default '',
`al_token` varchar(64) NOT NULL default '',
`al_user_agent` varchar(255) NOT NULL default '',
`al_ip` varchar(45) NOT NULL default '',
`al_created` datetime DEFAULT NULL,
`al_last_used` datetime DEFAULT NULL,
`al_expire` datetime DEFAULT NULL,
PRIMARY KEY (`al_id`),
UNIQUE KEY `al_token` (`al_token`),
KEY `mb_id` (`mb_id`),
KEY `al_expire` (`al_expire`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-- --------------------------------------------------------
--
-- Table structure for table `g5_memo`
--
+1
View File
@@ -596,6 +596,7 @@ fwrite($f, "\$g5['board_new_table'] = G5_TABLE_PREFIX.'board_new'; // 게시판
fwrite($f, "\$g5['login_table'] = G5_TABLE_PREFIX.'login'; // 로그인 테이블 (접속자수)\n");
fwrite($f, "\$g5['mail_table'] = G5_TABLE_PREFIX.'mail'; // 회원메일 테이블\n");
fwrite($f, "\$g5['member_table'] = G5_TABLE_PREFIX.'member'; // 회원 테이블\n");
fwrite($f, "\$g5['member_auto_login_table'] = G5_TABLE_PREFIX.'member_auto_login'; // 자동 로그인 토큰 테이블\n");
fwrite($f, "\$g5['memo_table'] = G5_TABLE_PREFIX.'memo'; // 메모 테이블\n");
fwrite($f, "\$g5['poll_table'] = G5_TABLE_PREFIX.'poll'; // 투표 테이블\n");
fwrite($f, "\$g5['poll_etc_table'] = G5_TABLE_PREFIX.'poll_etc'; // 투표 기타의견 테이블\n");
+41 -2
View File
@@ -4344,10 +4344,49 @@ function get_token_encryption_key($str=''){
function get_random_token_string($length=6)
{
if(function_exists('random_bytes')){
return bin2hex(random_bytes($length));
// 사용 가능한 가장 안전한 CSPRNG를 우선순위대로 시도하여 $length 바이트의 무작위 값을 얻는다.
// PHP 5.2.17 ~ 8.x 호환. 모든 단계는 function_exists/defined로 가드되어 구버전에서도 안전.
$bytes = false;
// 1순위. PHP 7.0+ : random_bytes() — OS의 CSPRNG 직접 사용 (가장 안전)
if (function_exists('random_bytes')) {
$bytes = random_bytes($length);
}
// 2순위. PHP 5.3+ : openssl_random_pseudo_bytes() — OpenSSL 확장이 있으면 사용
elseif (function_exists('openssl_random_pseudo_bytes')) {
$strong = false;
$tmp = openssl_random_pseudo_bytes($length, $strong);
if ($tmp !== false && $strong === true) {
$bytes = $tmp;
}
}
// 3순위. Linux/Unix : /dev/urandom 직접 읽기 (PHP 5.2 호환, OS 차원의 CSPRNG)
if ($bytes === false && DIRECTORY_SEPARATOR === '/' && @is_readable('/dev/urandom')) {
$fp = @fopen('/dev/urandom', 'rb');
if ($fp !== false) {
$tmp = @fread($fp, $length);
@fclose($fp);
if ($tmp !== false && strlen($tmp) === $length) {
$bytes = $tmp;
}
}
}
// 4순위. mcrypt 확장 (PHP 5.2 호환, PHP 7.2에서 제거됨) — Windows 등에서 fallback
if ($bytes === false && function_exists('mcrypt_create_iv') && defined('MCRYPT_DEV_URANDOM')) {
$tmp = @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
if ($tmp !== false && strlen($tmp) === $length) {
$bytes = $tmp;
}
}
if ($bytes !== false) {
return bin2hex($bytes);
}
// 최후 수단: 약한 RNG (암호학적 안전성 없음, 기존 동작 호환을 위해 유지)
// 이 경로에 도달하면 시스템에 안전한 난수 소스가 전혀 없는 비정상 환경이므로 점검 필요.
$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$output = substr(str_shuffle($characters), 0, $length);