Post

[CVE-2023-4300] 취약점 분석 보고서

Import XML and RSS Feeds 2.1.4 버전 이하에서 발생하는 file Upload 취약점

Import XML and RSS Feeds 플러그인 취약점 분석 (CVE-2023-4300)

취약점 개요

Import XML and RSS Feeds 플러그인의 2.1.4 버전 이전 버전에서 발견된 취약점은 파일 업로드 기능의 미흡한 확장자 검증으로 인해 발생한다. 공격자가 악성 PHP 파일을 업로드할 수 있어, 이를 통해 원격 코드 실행(RCE)이 가능하다.

  • CVSS 점수: 7.2 (높음)
  • 취약점 종류: File Upload
  • 취약점 영향: Remote Code Execution (RCE)
  • 취약 버전: 2.1.3 및 이전
  • CVE ID: CVE-2023-4300

패치 분석 (버전 2.1.2 vs 2.1.3)

코드 Diff

다음은 moove_save_import_template 함수의 2.1.22.1.3 버전 간의 변경사항이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
function moove_save_import_template() {
    $nonce = isset( $_POST['nonce'] ) ? sanitize_key( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( $nonce && wp_verify_nonce( $nonce, 'moove_xml_admin_nonce_field' ) && current_user_can( 
-       'edit_posts' ) ) :
+       'manage_options' ) ) :

        if ( $_POST && is_array( $_POST ) ) :
            $type = sanitize_text_field( $_POST['type'] );
            $extension = sanitize_text_field( $_POST['extension'] );
+           $allowed_extensions = apply_filters('uat_allowed_extenstions', array( 'xml', 'rss' ) );

+           if ( $extension && in_array( strtolower( $extension ), $allowed_extensions ) ) :
                $filename = uniqid( strtotime() );
+               $filename = function_exists('uniqid') ? uniqid( strtotime('now') ) : 'uat_tpl_' . strtotime('now');
                if ( $type === 'upload' ) :
                    $xml = wp_unslash( $_POST['file'] );
                    $filename = $filename . "." . $extension;
                    $target_dir = dirname( __FILE__ ) . "/uploads/" . $filename;
                    file_put_contents( $target_dir, $xml, FILE_APPEND | LOCK_EX );
                    $filename = plugins_url( basename( dirname( __FILE__ ) ) ) . '/uploads/' . $filename;
                else :
                    $filename = sanitize_text_field( $_POST['url'] );
                endif;

                if ( isset($_POST['form_data'] ) && is_array( $_POST['form_data'] ) ) :
                    // Create post object
                    $import_data = array(
                        'post_title'    => wp_strip_all_tags( $_POST['name'] ),
                        'post_status'   => 'publish',
                        'post_type'     => 'moove_feed_importer'
                    );

                    // Insert the post into the database
                    $post_id = wp_insert_post( $import_data );
                    if ( $post_id ) :
                        foreach ( $_POST['form_data'] as $field_name => $field_value ) :
                            add_post_meta( $post_id, 'import_'.  $field_name , $field_value );
                        endforeach;
                        add_post_meta( $post_id, 'import_xml_url', $filename );
                        add_post_meta( $post_id, 'import_type', wp_strip_all_tags( $_POST['type'] ) );
                        add_post_meta( $post_id, 'import_limit', wp_strip_all_tags( $_POST['limit'] ) );
                        add_post_meta( $post_id, 'import_selected_node', sanitize_text_field( $_POST['selected_node'] ) );
                        $slug = get_post_field( 'post_name', get_post( $post_id ) );
                        $update_post = array(
                            'ID'           => $post_id,
                            'post_title'   => $slug
                        );
                        wp_update_post( $update_post );
                        echo json_encode( array( 'success' => 'true', 'message' => 'Post created', 'slug' => $slug, 'template_id' => $post_id  ) );
                        die();
                    else :
                        echo json_encode( array( 'success' => 'false', 'message' => 'Post not created!' ) );
                        die();
                    endif;
                else :
                    echo json_encode( array( 'success' => 'false', 'message' => 'Form data empty!' ) );
                    die();
                endif;
+           else :
+               echo json_encode( array( 'success' => 'false', 'message' => 'Unsupported extension, please check your feed!' ) );
+               die();
            endif;            
        else :
            echo json_encode( array( 'success' => 'false', 'message' => 'POST not set!' ) );
            die();
        endif;
    else :
        echo json_encode( array( 'success' => 'false', 'message' => 'Check nonce!' ) );
        die();
    endif;
}

주요 변경사항

  1. 사용자 권한 강화
    • 사용자 권한이 'edit_posts'에서 'manage_options'로 변경됐다. 이를 통해 관리자만 해당 기능을 사용할 수 있도록 제한했다.
  2. 허용된 확장자 목록 추가
    • $allowed_extensions 배열을 추가하여 xmlrss 확장자만 허용되도록 했다.
    • apply_filters 함수를 사용해 확장자 목록을 동적으로 필터링할 수 있도록 설계했다.
  3. 확장자 검증
    • 업로드된 파일의 확장자가 허용된 목록에 포함되어 있는지 확인하는 조건문이 추가됐다.
    • 허용되지 않은 확장자가 입력될 경우 오류 메시지가 반환된다.
  4. 파일명 생성 방식 수정
    • uniqid 함수가 존재하지 않는 경우를 대비해 대체 파일명 생성 방식을 추가했다. 이로 인해 파일명이 항상 고유하도록 보장한다.

코드 분석

1
2
3
4
5
6
7
8
9
if ( $_POST && is_array( $_POST ) ) :
				$type = sanitize_text_field( $_POST['type'] );
				$extension = sanitize_text_field( $_POST['extension'] );
				$filename = uniqid( strtotime() );
				if ( $type === 'upload' ) :
					$xml = wp_unslash( $_POST['file'] );
					$filename = $filename . "." . $extension;
					$target_dir = dirname( __FILE__ ) . "/uploads/" . $filename;
					file_put_contents( $target_dir, $xml, FILE_APPEND | LOCK_EX );
  • moove_save_import_template 함수에서 사용자가 전송한 file 데이터를 sanitize_text_field로 확장자 검증을 하고 있다.
  • sanitize_text_field는 HTML 태그나 특수 문자를 제거하지만, 확장자 형식의 유효성 검증에는 적합하지 않아 php와 같은 실행 가능한 파일 확장자를 업로드 할 수 있다.

sanitize_text_filed란?

  • 잘못된 UTF-8 확인
  • 단일 < 문자를 엔터티로 변환
  • 모든 태그 제거
  • 줄 바꿈, 탭 및 추가 공백 제거
  • 퍼센트로 인코딩된 문자 제거

사용자의 입력이나 데이터베이스에서 문자열을 정리하는 함수로 위 기능을 통해 특수 문자나 태그, Unicode 등을 방지하여 XSS를 방어할 수 있지만 별도의 확장자에 대한 제한이 없어 php 파일을 업로드 할 수 있다.

1
2
$filename = plugins_url( basename( dirname( __FILE__ ) ) ) . '/uploads/' . $filename;
add_post_meta( $post_id, 'import_xml_url', $filename );
  • moove_save_import_template 함수에서 plugins_url으로 url을 생성하고 있다.
  • filename 변수는 plugins_url 함수를 통해 디렉토리 내의 업로드된 파일에 접근할 수 있는 전체 url이 저장된다.
  • filename은 ‘import_xml_url’이라는 키 이름으로 특정 게시물의 메타 데이터로 저장된다.
1
'feedurl' => get_post_meta( $template_id, 'import_xml_url', true ),
  • 특정 게시물의 메타 데이터는 template_view.php 파일에서 사용자에게 보여진다.
  • 해당 메타데이터에 저장된 url을 통해 업로드한 파일에 접근이 가능하고, 결과적으로 RCE가 가능하다.

POC

XML, RSS 파일 업로드

image.png

처음 파일을 업로드할 때 XML, RSS 파일 형식만 허용되기 때문에 정상적인 XML 파일을 업로드한다.

image.png

php 파일 업로드를 시도 할 때 *.xml , *.rss 형식만을 허용하는 것을 확인할 수 있다.

SAVE AS TEMPLATE

image.png

해당 CVE에서 취약점이 발생한 SAVE AS TEMPLATE 기능이다.

image.png

패킷을 확인해보면 앞서 살펴봤던 moove_save_import_template 함수로 file, extension 등을 전송하는 것을 확인할 수 있고, moove_save_import_template 함수는 sanitize_text_field 함수를 통해 확장자를 검증하기 때문에 php 파일 업로드가 가능하다. 해당 실습에서는 간단한 웹쉘을 업로드했다.

WebShell 실행

image.png

SAVE AS TEMPLATE 기능으로 저장된 template을 선택 하면 해당 파일 경로를 얻을 수 있다.

image.png

wp-content/plugins/import-xml-feed/uploads/67399dca21640.php?cmd=pwd 앞서 확인한 경로로 접근 후 pwd 명령 실행 시 성공적으로 실행된 것을 확인할 수 있다.

추가 취약점: CVE-2023-4521

CVE-2023-4521은 이전 취약점(CVE-2023-4300)과 밀접하게 연관되어 있으며, 이전 취약점에 의해 업로드된 테스트 PHP 파일(Webshell)이 제거되지 않아 발생한다. 패치는 되었으나, 이미 시스템에 남아 있는 Webshell 파일로 인해 여전히 RCE가 가능한 상태가 유지된다.


취약점 개요

CVE-2023-4521의 핵심은 “웹쉘 파일의 잔존”이다. Import XML and RSS Feeds 플러그인의 이전 취약점(CVE-2023-4300)을 통해 업로드된 악성 PHP 파일이 플러그인 디렉토리에 그대로 남아 있는 경우, 패치 이후에도 공격자는 여전히 이를 통해 원격 코드 실행(RCE)을 수행할 수 있다.

  • 취약점 종류: 잔존 파일(Remnant File)로 인한 취약점
  • 취약점 영향: 원격 코드 실행 (RCE)
  • 취약 버전: CVE-2023-4300 취약점 발생 후 웹쉘 제거되지 않은 상태의 2.1.4 이하
  • CVE ID: CVE-2023-4521

취약점 원인

  1. 웹쉘 파일이 삭제되지 않음
    • CVE-2023-4300의 패치를 통해 새로운 파일 업로드는 방지되었으나, 이전에 업로드된 악성 파일은 디렉토리에 그대로 남아 있었다.
    • 공격자가 업로드한 웹쉘 파일은 URL을 통해 접근 가능하며, 이를 통해 원격 명령 실행(RCE)이 가능하다.

패치 로그 분석

image.png

wordpress plugin Repository에서 Moove Agency를 검색해보면 여러 패치 로그들이 나온다.

image.png

그 중 CVE-2023-4521으로 인한 패치 버전인 2.1.5버전을 확인해보면 2.1.4 버전에서 업로드 파일이 제거된 로그가 있는 것을 확인할 수 있다.

image.png

CVE-2023-4300으로 인한 패치 버전인 2.1.4버전을 찾아보면 총 3개의 파일들이 업로드 되어 있다. 해당 파일들은 당시 CVE-2023-4300의 테스트를 위한 웹쉘 파일으로 해당 파일을 이용해서 RCE가 가능하다는 것을 알 수 있다.

This post is licensed under CC BY 4.0 by the author.