[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]
- ex.
- 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
배열의 키를 변수로 변환합니다. 또한, $atts
에 param
키가 있다면 $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
$param
값이 검증 없이shortcode
의 인자로 전달- HTML 엔터티가
html_entity_decode()
로 인해 복원되기 때문에<
와>
같은 태그 문자 삽입 가능 위 두 가지 요소 때문에 Contributor 권한 사용자는 ShortCode 를 추가할 때 Stored XSS 공격을 수행할 수 있게 됩니다.
페이로드
1
[st_tag_cloud param="title=<script>alert('XSS')</script>"]
4.1 공격 시나리오
- Contributor 권한 사용자가 Shortcode를 추가할때 param 속성에 XSS 페이로드를 포함합니다.
내부적으로 Shortcode를 처리하면서
html_entity_decode()
HTML 엔터디가 복원되어 결과적으로<script>alert(’XSS’)</script>
가 저장됩니다.해당 게시글을 확인해보면 성공적으로 XSS 취약점이 발생하는 것을 확인할 수 있습니다.
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()
함수에서 입력 검증이 부족해 취약점이 발생했고, 이후 패치버전에서 해당 함수가 삭제되었음을 알 수 있습니다.