Post

[CVE-2024-2830] 취약점 분석 보고서

💡 요약

CVE-2024-2830은 WordPress의 Tag and Category Manager - AI Autotagger 플러그인에서 발견된 저장형 크로스 사이트 스크립팅(Stored Cross-Site Scripting, XSS) 취약점입니다. 해당 취약점은 플러그인의 st_tag_cloud 숏코드에서 사용자 입력에 대한 충분한 검증과 출력 이스케이프 처리가 이루어지지 않아 발생합니다.

1. 취약점 개요

  • 취약점 번호: CVE-2024-2830
  • 영향 받는 버전: Tag and Category Manager – AI Autotagger (TaxoPress) 3.13.0 이하
  • 취약점 유형: Stored XSS
  • CVSS: 6.4
  • 취약점 패치 버전: 3.20.0

2. Shortcode 란?

  • WordPress 에서 [ ] 로 묶인 특수 태그로, 코드 없이도 동적인 콘텐츠를 쉽게 추가할 수 있게 해주는 기능입니다.
    • ex. [shortcode_tag], [shortcode_tag]content[/shortcode_tag]
  • Shortcode는 주로 사용자가 제공한 속성 값이나 콘텐츠에서 입력을 받아 처리합니다. 이 입력값이 적절히 검증되지 않거나 HTML 이스케이프 처리가 없을 경우 XSS 취약점이 발생할 수 있습니다.

3. 취약점 상세 분석

3.1 class.client.tagcloud.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function __construct() {
		if ( 1 === (int) SimpleTags_Plugin::get_option_value( 'allow_embed_tcloud' ) ) {
			add_shortcode( 'st_tag_cloud', array( __CLASS__, 'shortcode' ) );
			add_shortcode( 'st-tag-cloud', array( __CLASS__, 'shortcode' ) );
		}
	}
	
public static function shortcode( $atts ) {
    $atts = shortcode_atts( array( 'param' => '' ), $atts );
		extract( $atts );

		$param = html_entity_decode( $param );
		$param = trim( $param );

		if ( empty( $param ) ) {
			$param = 'title=';
		}

		return self::extendedTagCloud( $param, false );
	}

__construct() 함수

Shortcode로 [st_tag_cloud], [st-tag-cloud]를 등록합니다. 이 두 Shortcode 모두 shortcode() 함수를 콜백 함수로 사용합니다.

shortcode() 함수

사용자 제공 속성($atts)와 기본 속성을 병합하고, 이때 기본 속성은 param=’’ 입니다. Shortcode에 다른 속성이 제공되지 않으면 param은 빈 문자열로 초기화 됩니다.

1
$atts = shortcode_atts( array( 'param' => '' ), $atts );

extract() 함수를 통해 $atts 배열의 키를 변수로 변환합니다. 또한, $attsparam 키가 있다면 $param 변수로 변환합니다.

1
extract( $atts );

$param값을 디코딩하여 HTML 엔터티를 원래 문자로 변환합니다.

1
$param = html_entity_decode( $param );

$param에서 앞뒤 공백을 제거합니다.

1
$param = trim( $param );

$param이 비어 있다면 기본값 title= 을 설정합니다.

1
2
3
if ( empty( $param ) ) {
    $param = 'title=';
}

최종적으로 $param값을 extendedTagCloud 함수에 전달하여 태그 클라우드를 생성합니다.

1
return self::extendedTagCloud( $param, false );
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
class SimpleTags_Client_TagCloud {
			...
			...
			// Get terms
			$terms = self::getTags( $args, $taxonomy );
			extract( $args ); // Params to variables
	
			// If empty use default xformat !
			if ( empty( $xformat ) ) {
				$xformat = $defaults['xformat'];
			}
	
			$xformat = taxopress_sanitize_text_field($xformat);
	
	        //remove title if in settings
	        if((int)$hide_title > 0){
	            $title = '';
	        }
        
      $term = $terms_data[ $term_name ];
			$scale_result = (int) ( $scale <> 0 ? ( ( $term->count - $minval ) * $scale + $minout ) : ( $scale_max - $scale_min ) / 2 );
			$output[] = SimpleTags_Client::format_internal_tag( $xformat, $term, $rel, $scale_result, $scale_max, $scale_min, $largest, $smallest, $unit, $maxcolor, $mincolor );
		}

		return SimpleTags_Client::output_content( 'st-tag-cloud', $format, $title, $output, $copyright, '', $wrap_class, $link_class, $before, $after );
...

$title은 태그 데이터를 가져오는 getTags() 호출의 결과에 따라 두 가지 경로로 SimpleTags_client::output_content()함수로 전달됩니다.

3.2 class.client.php

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
	public static function output_content( $html_class = '', $format = 'list', $title = '', $content = '', $copyright = true, $separator = '', $div_class = '', $a_class = '', $before = '', $after = '') {
		if ( empty( $content ) ) {
			return ''; // return nothing
		}

		if ( $format == 'array' && is_array( $content ) ) {
			return $content; // Return PHP array if format is array
		}

		if ( is_array( $content ) ) {
			switch ( $format ) {
				case 'list' :
					$output = ''. $before .' <ul class="' . $html_class . '">' . "\n\t" . '<li>' . implode( "</li>\n\t<li>", $content ) . "</li>\n</ul> {$after}\n";
					break;
				case 'ol' :
					$output = ''. $before .' <ol class="' . $html_class . '">' . "\n\t" . '<li>' . implode( "</li>\n\t<li>", $content ) . "</li>\n</ol> {$after}\n";
					break;
				default :
					$output = '<div class="' . $html_class . '">'. $before .' ' . "\n\t" . implode( "{$separator}\n", $content ) . " {$after}</div>\n";
					break;
			}
		} else {
			$content = trim( $content );
			switch ( $format ) {
				case 'string' :
					$output = $content;
					break;
				case 'list' :
					$output = ''. $before .' <ul class="' . $html_class . '">' . "\n\t" . '<li>' . $content . "</li>\n\t" . "</ul> {$after}\n";
					break;
				default :
					$output = '<div class="' . $html_class . '">'. $before .' ' . "\n\t" . $content . " {$after} </div>\n";
					break;
			}
		}

$title은 선행 및 후행 공백 제거(trim()), 새로운 줄 및 탭 문자 연결 과정을 거쳐 최종적으로 HTML 출력의 일부로 포함되어 $title값을 포함한 최종 HTML을 반환합니다.

따라서 $param$args$title 로 이어지는 데이터 흐름에서 사용자 입력에 대한 유효성 검사가 이루어지지 않아 Stored XSS 취약점이 발생합니다.

4. PoC

  1. $param 값이 검증 없이 shortcode의 인자로 전달
  2. HTML 엔터티가 html_entity_decode() 로 인해 복원되기 때문에 <> 같은 태그 문자 삽입 가능 위 두 가지 요소 때문에 Contributor 권한 사용자는 ShortCode 를 추가할 때 Stored XSS 공격을 수행할 수 있게 됩니다.

페이로드

1
[st_tag_cloud param="title=<script>alert('XSS')</script>"]

4.1 공격 시나리오

  1. Contributor 권한 사용자가 Shortcode를 추가할때 param 속성에 XSS 페이로드를 포함합니다.

image.png

  1. 내부적으로 Shortcode를 처리하면서 html_entity_decode() HTML 엔터디가 복원되어 결과적으로 <script>alert(’XSS’)</script> 가 저장됩니다.

  2. 해당 게시글을 확인해보면 성공적으로 XSS 취약점이 발생하는 것을 확인할 수 있습니다.

image.png

5. 패치 내용 및 변경사항 분석

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
public function __construct() {
-	if ( 1 === (int) SimpleTags_Plugin::get_option_value( 'allow_embed_tcloud' ) ) {
-		add_shortcode( 'st_tag_cloud', array( __CLASS__, 'shortcode' ) );
-		add_shortcode( 'st-tag-cloud', array( __CLASS__, 'shortcode' ) );
-	}
-}
-
-/**
- * Replace marker by a tag cloud in post content, use ShortCode
- *
- * @param array $atts
- *
- * @return string
- */
-public static function shortcode( $atts ) {
-    $atts = shortcode_atts( array( 'param' => '' ), $atts );
-	extract( $atts );
-
-	$param = html_entity_decode( $param );
-	$param = trim( $param );
-
-	if ( empty( $param ) ) {
-		$param = 'title=';
-	}
-
-	return self::extendedTagCloud( $param, false );
}

위 diff를 통해 shortcode() 함수에서 입력 검증이 부족해 취약점이 발생했고, 이후 패치버전에서 해당 함수가 삭제되었음을 알 수 있습니다.

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