Wordpress Post Type을 이용한 Privilege Escalation 취약점(<= wordpress 5.0.0)

이번주 초에 RIPS 블로그에 wordpress 관련 취약점 내용이 올라와 빠르게 테스트 좀 했었고 관련 내용 정리해둡니다. https://blog.ripstech.com/2018/wordpress-post-type-privilege-escalation/

Wordpress Post Type

Post Type은 각 게시글의 타입을 의미하고 Wordpress가 지정한 포맷 이외에 타입들이 생성되고 추가될 수 있습니다. 기본 wordpress 페이지 요청에선 post, page 등으로 지정할 수 있고 이에 따라 어떤 형태의 글이 작성될지 결정되게 됩니다.

Default Post Type (https://codex.wordpress.org/Post_Types)

* Post (Post Type: 'post')
* Page (Post Type: 'page')
* Attachment (Post Type: 'attachment')
* Revision (Post Type: 'revision')
* Navigation Menu (Post Type: 'nav_menu_item')
* Custom CSS (Post Type: 'custom_css')
* Changesets (Post Type: 'customize_changeset')
* User Data Request (Post Type: 'user_request' )

Post Type of Plugins

플러그인을 만드는 경우 register_post_type() 함수로 Post type을 지정할 수 있습니다.

예를들면,…

function create_post_type(){
  register_post_type(’test’,array(
    ‘label’ => ’test’,
    ‘description’ => ’test Post Type’ 
  ));
}
add_action(‘init’,’create_post_type’);

일부(많을지도..) 플러그인들은 이런식으로 별도의 Post type을 생성하는데요, 이런 경우 wordpress 관리자 페이지에 글/페이지 이외에도 플러그인 특유의 글(?)을 쓸 수 있는 공간이 생깁니다.

Vulnerability!

플러그인에서 제공하는 기능을 수행할 때 플러그인 개발자가 구성한 코드에 따라 post.php를 타고 post type을 달고 나가서 기능 요청을 수행하게 됩니다.

예를들면 이렇게 Contact, NinJa form 같은 플러그인에서 템플릿이나 관련 기능을 구성할 떄 post.php를 타고 발생합니다.

그래서 post.php에는 게시글 작성 시 발생하는 post_type에 대해서 wordpress 자체에서 검증하는 로직이 있는데, 이 검증 로직 이후 post_type을 다시 체크하지 않고 post_id(게시글 번호) 값으로 게시글 작성/수정을 하게 됩니다. 결국은 post_type 검증 로직만 통과한 이후엔 어떤 플러그인인지 검증하지 않고 해당하는 post_id의 데이터를 수정(없으면 추가) 합니다. 분명 문제가 있는 부분이죠. (그나저나 찾아낸 rips도 참 대단하네.. 코드봐도 눈에 잘 안뵐거같은데)

wp-admin/post.php

if ( isset( $_GET['post'] ) )
    $post_id = $post_ID = $_GET['post'];
elseif ( isset( $_POST['post_ID'] ) )
    $post_id = $post_ID = $_POST['post_ID'];

if($post_id)
    // The post type is now 'post'
    $post_type = get_post_type($post_id);
else
    $post_type = $_POST['post_type'];

// 이 구간에서 post_type으로 받은 데이터가 wp에서 제공하는 타입인지 체크합니다.
$nonce_name = "add-" . $post_type;
if(!wp_verify_nonce($nonce_name))
    die("You are not allowed to create posts of this type!");

// 문제가 있다면 바로 코드를 종료하고, 없다면 게시글을 작성할 준비를 합니다.
$post_details = array(
  'post_title' => $_POST['post_title'],
  'post_content' => $_POST['post_content'],
  'post_type' => $_POST['post_type']
);

// 사용자로 부터 넘어온 post_id가 있느지 체크합니다(없으면 새글)
if(isset($_POST['post_ID'])) {
   
    unset($post_details['post_type']);
    $post_details['ID'] = $post_id;
    wp_update_post($post_details);
} else {
    // If we just set $_GET['post'] we will enter this branch and can set the
    // post type to anything we want it to be!
    wp_insert_post($post_details);
}
// 아무튼 이 순서대로만 보면 post_type에 대해 체크하는 과정은 맨 처음 한 과정이구 이후엔
// post_type이 어떻든 post_id 기준으로 새글/기존글 수정으로 넘어가 집니다.
// 고로 일반 사용자의 글 요청으로 플러그인이 작성한 게시글의 post_id을 덮거나
// 새로운 플러그인 요청을 만들 수 있겠죠.

[ 일반 게시글 작성 요청 ]

POST /wp-admin/post.php HTTP/1.1

post_type=post&action=editpost&~~~

[ 취약한 플러그인의 작성 요청 ]

POST /wp-admin/post.php HTTP/1.1

post_type=post&plugin_vulnapp_contents=thisisadminquery&form=%7B%22id%22%3A%22tmp-1545384518%22%2C%22show_publish_option

플러그인에 따라 영향력이 다르겠지만 중요 기능을 다루는 플러그인에서 해당 문제 발생 시 리스크가 높을 것 같습니다.

RIPS 팀에서도 추가로 발견해서 리포팅된게 Contacts Form7 인데(이 플러그인은 Contact form을 만들어주는 플러그인이고 템플릿을 지정할 수 있게 되어있습니다) 이 플러그인엔 Path traversal을 이용해서 시스템 파일을 가져올 수 있는 취약점이 존재했었습니다.

이를 활용하면 일반 사용자 권한으로 서버의 파일이나 wp-config.php 같은 소스코드 파일을 읽어와서 중요한 cred를 탈취하거나 정보를 획득하는데 사용될 수 있었죠.

마침 테스트 wp에 취약 버전으로 깔려있길래 함 테스트해봤는데… 굿 :)

How to protect?

  • Upate wordpress version >= 5.0.1
  • Vulnerable plugin remove or patch

우선 5버전대는 5.0.1 으로 wordpress 공식 배포가 나왔습니다. 근본적인 원인은 플러그인 요청이 post.php와 동일하게 사용되고 검증 과정이 부족한 것이기 때문에 core 업그레이드가 해결책이지만 플러그인을 적게 쓰는 경우 플러그인 개발 관점에서 대응도 가능합니다.

플러그인쪽 코드 에서 post type 정의 시 capbility_type 추가

register_post_type( 'example_post_type', array(
    'label' => 'Example Post Type',     
    
    'capability_type' => 'page'     // capability_type of page makes sure that
                                    // only editors and admins can create posts of
                                    // that type
));