[CVE Study] CVE-2022-21662

코코·2023년 12월 3일
0

CVE_Study

목록 보기
1/3
post-thumbnail

오늘은 CVE-2022-21662를 분석해보고자 한다❗




개요

CVE-2022-21662는 Post-Slugs를 통해 Stored XSS를 수행할 수 있는 취약점이다.


우선 본격적으로 분석하기에 앞서 Post-Slugs가 무엇인지부터 알아보자.
Post-Slug란 CMS에서 각 블로그 게시물에 대해 고유하게 읽을 수 있는 웹 주소를 생성하는데 사용된다.

예를 들어 Wordpress의 게시물의 타이틀이 "coco" 라고 생각해보자.
그러면 해당 게시물의 URL이 "http://localhost/w/wordpress/2023/12/03/coco/" 이런 식으로 생성된다. 즉, 게시물의 타이틀 이름을 따서 URL을 생성하는 기능을 뜻한다.



CVE-2022-21662

환경 구성

해당 CVE가 발생하는 환경은 WordPress 5.8.3 이하버전으로 Wordpress 5.8.1을 다운로드받는다.
환경구성은 XAMPP를 먼저 설치하고, Wordpress를 ZIP파일로 다운받아 htdocs 폴더에 압축을 해제하여 넣는다.


설치성공 👏


시작하기에 앞서, Wordpress가 자동 업데이트되지 않게 설정해준다.

wp-config.php 파일에 아래의 구문을 추가해주면 된다.

/* Auto update off*/
define( 'WP_AUTO_UPDATE_CORE', false );

또한 게시글 작성 권한을 가진 사용자를 하나 추가해주자❗
글쓴이 역할인 사용자 "user"를 추가해준 모습이다.




CVE-2022-21662

자 이제 하나씩 분석을 시작해보자!

글을 작성하면 include 폴더 안에 post.php에 선언된 wp_insert_post 함수가 실행된다.

wp_insert_post

함수를 잘 살펴보면, sanitize_title 함수를 사용하여 필터링을 수행하고 있다.

	/*
	 * Create a valid post name. Drafts and pending posts are allowed to have
	 * an empty post name.
	 */
	if ( empty( $post_name ) ) {
		if ( ! in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true ) ) {
			$post_name = sanitize_title( $post_title );
		} else {
			$post_name = '';
		}
	} else {
		// On updates, we need to check to see if it's using the old, fixed sanitization context.
		$check_name = sanitize_title( $post_name, '', 'old-save' );

		if ( $update && strtolower( urlencode( $post_name ) ) == $check_name && get_post_field( 'post_name', $post_ID ) == $check_name ) {
			$post_name = $check_name;
		} else { // new post, or slug has changed.
			$post_name = sanitize_title( $post_name );
		}
	}

해당 함수는 formatting.php에 선언되어 있으며, 주석처리 된 설명에는 다음과 같이 적혀있다.

Sanitizes a string into a slug, which can be used in URLs or HTML attributes.
문자열을 슬러그(Slug)로 변환하여 URL이나 HTML 속성에서 사용할 수 있도록 정제합니다.




그러면, 바로 formatting.php에 선언된 sanitize_title 함수를 살펴보자.

sanitize_title

function sanitize_title( $title, $fallback_title = '', $context = 'save' ) {
	$raw_title = $title;

	if ( 'save' === $context ) {
		$title = remove_accents( $title );
	}

	/**
	 * Filters a sanitized title string.
	 *
	 * @since 1.2.0
	 *
	 * @param string $title     Sanitized title.
	 * @param string $raw_title The title prior to sanitization.
	 * @param string $context   The context for which the title is being sanitized.
	 */
	$title = apply_filters( 'sanitize_title', $title, $raw_title, $context );

	if ( '' === $title || false === $title ) {
		$title = $fallback_title;
	}

	return $title;
}

우선 일차적으로 remove_accents 함수를 사용하여 title의 1차 필터링을 수행한다.
또한 아래 부분에 apply_filter 함수를 통해 sanitize_title 훅을 실행시킨다.



default-filters.php에 정의된 add_filter를 확인해보면, sanitize_title_with_dashes 라는 함수를 추가로 호출하는 것을 알 수 있다.



sanitize_title_with_dashes

function sanitize_title_with_dashes( $title, $raw_title = '', $context = 'display' ) {
	$title = strip_tags( $title );
	// Preserve escaped octets.
	$title = preg_replace( '|%([a-fA-F0-9][a-fA-F0-9])|', '---$1---', $title );
	// Remove percent signs that are not part of an octet.
	$title = str_replace( '%', '', $title );
	// Restore octets.
	$title = preg_replace( '|---([a-fA-F0-9][a-fA-F0-9])---|', '%$1', $title );

	if ( seems_utf8( $title ) ) {
		if ( function_exists( 'mb_strtolower' ) ) {
			$title = mb_strtolower( $title, 'UTF-8' );
		}
		$title = utf8_uri_encode( $title, 200 );
	}

해당 함수를 살펴보면 strip_tags를 통해 HTML 태그와 PHP 태그 제거한다.
이후 preg_replace 함수를 통해 %20 형태로 들어온 값들을 ---$20---으로 바꾸고, 다시 ---$20---의 값을 %20으로 바꿔준다.


그러면 해당 함수의 아래부분에서 wp_unique_post_slug 함수를 호출하는 부분이 존재한다.


  • wp_unique_post_slug
function wp_unique_post_slug( $slug, $post_ID, $post_status, $post_type, $post_parent ) {
	if ( in_array( $post_status, array( 'draft', 'pending', 'auto-draft' ), true )
		|| ( 'inherit' === $post_status && 'revision' === $post_type ) || 'user_request' === $post_type
	) {
		return $slug;
	}


    		...


			$suffix = 2;
			do {
				$alt_post_name   = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
				$post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_ID ) );
				$suffix++;
			} while ( $post_name_check );
			$slug = $alt_post_name;
		}

해당 함수 안에서 _truncate_post_slug 함수를 실행시킨다. 해당 함수를 잘 살펴보자!



_truncate_post_slug

해당 함수는 마찬가지로 post.php선언되어있다.

/**
 * Truncate a post slug.
 *
 * @since 3.6.0
 * @access private
 *
 * @see utf8_uri_encode()
 *
 * @param string $slug   The slug to truncate.
 * @param int    $length Optional. Max length of the slug. Default 200 (characters).
 * @return string The truncated slug.
 */
function _truncate_post_slug( $slug, $length = 200 ) {
	/* 200보다 커야 if문안으로 ... */
    if ( strlen( $slug ) > $length ) {
		$decoded_slug = urldecode( $slug );
		if ( $decoded_slug === $slug ) {
			$slug = substr( $slug, 0, $length );
		} else {
			$slug = utf8_uri_encode( $decoded_slug, $length );
		}
	}

	return rtrim( $slug, '-' );
}

디코딩을 진행하고, 디코딩 된 값이 slug와 같지 않으면 utf8_uri_encode 함수를 통해 다시 인코딩을 진행한다.
특이한 점은 urldecode 함수를 호출하여 디코딩했지만 인코딩은 urlencode 함수가 아닌 utf8_uri_encode 함수로 수행한다.


utf8_uri_encode 함수는 유니코드 문자만 인코딩을 진행하는 함수이다.
따라서 200자 이상의 title에 페이로드를 인코딩하여 넣을 경우 utf8_uri_encode 함수로 인코딩되어 들어가는 것이다.


예를 들어, 를 인코딩하면 이대로 출력된다고 한다.


  1. 똑같은 제목을 가진 게시물을 업로드
    • wp_unique_post_slog 함수를 호출하기 위함
  2. utf8_uri_encode 함수 호출
    • URL로 인코딩 된 제목 설정 && 길이가 200자 이상

위의 방법대로 게시물을 업로드하면 스크립트가 그냥 삽입될 것이다.





똑같은 title을 가진 게시물 등록.

이 후 다시 글 관리 페이지로 들어가보면 ...

Script 실행.




※ 참고
👉 https://themeisle.com/blog/post-slug/#gref
👉 https://www.sonarsource.com/blog/wordpress-stored-xss-vulnerability/

profile
화이팅!

0개의 댓글