Team <= 5.0.13 - Missing Authorization
Description
The Team plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 5.0.13. This makes it possible for unauthenticated attackers to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=5.0.13What Changed in the Fix
Changes introduced in v5.0.14
Source Code
WordPress.org SVN## Vulnerability Summary The **Team – Team Members Showcase Plugin** (versions <= 5.0.13) contains a **Missing Authorization** vulnerability. The plugin registers several AJAX handlers—including `tlp_md_popup_single`, `tlp_team_smart_popup`, and `rtGetSpecialLayoutData`—using both `wp_ajax_` and `w…
Show full research plan
Vulnerability Summary
The Team – Team Members Showcase Plugin (versions <= 5.0.13) contains a Missing Authorization vulnerability. The plugin registers several AJAX handlers—including tlp_md_popup_single, tlp_team_smart_popup, and rtGetSpecialLayoutData—using both wp_ajax_ and wp_ajax_nopriv_ hooks. While these handlers implement a nonce check, they fail to verify if the requesting user has sufficient capabilities (e.g., current_user_can(...)) to access the data or trigger the action.
Specifically, the SinglePopup controller (app/Controllers/Frontend/Ajax/SinglePopup.php) and the SpecialLayout controller (app/Controllers/Frontend/Ajax/SpecialLayout.php) do not check the post_status of the requested team member, allowing unauthenticated attackers to retrieve sensitive metadata (emails, phone numbers, bios) for members that may be in "draft" or "private" status. Furthermore, the reliance on a common nonce exposed to all users effectively renders the CSRF protection moot for unauthenticated endpoints.
Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Actions:
tlp_md_popup_single(viaRT\Team\Controllers\Frontend\Ajax\SinglePopup)tlp_team_smart_popup(viaRT\Team\Controllers\Frontend\Ajax\SmartPopup)rtGetSpecialLayoutData(viaRT\Team\Controllers\Frontend\Ajax\SpecialLayout)
- HTTP Parameters:
action:tlp_md_popup_singleorrtGetSpecialLayoutDatasecurity: The WordPress nonce (obtained from the frontend).id(for popups) ormemberId(for layouts): The ID of the team member post.scID(for layouts): The ID of a Team Shortcode post (post typeteam-sc).
- Authentication: None required (unauthenticated).
- Preconditions: The attacker must obtain a valid nonce, which is automatically localized and exposed on any page where a Team shortcode or widget is active.
Code Flow
- Registration: In
SinglePopup::init(), the action is registered for both logged-in and guest users:add_action( 'wp_ajax_tlp_md_popup_single', [ $this, 'response' ] ); add_action( 'wp_ajax_nopriv_tlp_md_popup_single', [ $this, 'response' ] ); - Nonce Verification: The
response()method checks a nonce usingFns::getNonce()andFns::nonceText():if ( ! wp_verify_nonce( Fns::getNonce(), Fns::nonceText() ) ) { ... } - Missing Auth Check: Immediately following the nonce check, the code proceeds to fetch the post without any
current_user_can()check orpost_statusvalidation (inSinglePopup):if ( isset( $_REQUEST['id'] ) && $post_id = absint( $_REQUEST['id'] ) ) { $post = get_post( absint( $_REQUEST['id'] ) ); if ( $post && $post->post_type == rttlp_team()->post_type ) { // Processing metadata for any team post... } } - Information Leakage: Metadata like
email,telephone,mobile,ttp_my_resume(Resume URL), andttp_hire_me(Hire URL) are fetched and returned in the JSON response.
Nonce Acquisition Strategy
The plugin localizes the nonce for its AJAX requests. To obtain it:
- Create Content: Create a Team Member and a Page containing the default shortcode to ensure scripts are enqueued.
wp post create --post_type=team --post_title="Secret Member" --post_status=draft --post_content="Draft info"wp post create --post_type=page --post_title="Team Page" --post_status=publish --post_content="[tlpteam]"
- Navigate: Use
browser_navigateto visit the "Team Page". - Extract: Use
browser_evalto extract the nonce. Based on common RadiusTheme patterns, the nonce is likely stored in a global JS object likert_team_varsortlp_team_vars.- Try:
browser_eval("tlp_team_vars.nonce")or search the page source fornonce. - Alternatively, check for the localized script variable name in the HTML source:
grep -i "nonce".
- Try:
Exploitation Strategy
Step-by-Step Plan
- Setup Member: Create a team member with sensitive metadata and set its status to
draft. - Setup Shortcode: Create a Team Shortcode (
team-sc) to satisfy thescIDparameter requirements for layout tests. - Obtain Nonce: Navigate to a page with the shortcode and extract the
securitytoken. - Execute Information Disclosure (Single Popup):
- Call
tlp_md_popup_singleviahttp_request. - Request the ID of the
draftmember. - Observe the metadata in the response.
- Call
- Execute Information Disclosure (Special Layout):
- Call
rtGetSpecialLayoutDataviahttp_request. - Parameters:
action=rtGetSpecialLayoutData,memberId=[ID],security=[NONCE].
- Call
HTTP Request Payload (Example)
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=tlp_md_popup_single&security=82c41a9e3b&id=123
Test Data Setup
- Team Member:
MEMBER_ID=$(wp post create --post_type=team --post_title="John Doe" --post_status=draft --post_content="Confidential Bio" --format=ids) wp post meta update $MEMBER_ID email "john.private@example.com" wp post meta update $MEMBER_ID telephone "+1-555-0199" - Shortcode (for layout action):
SC_ID=$(wp post create --post_type=team-sc --post_title="Grid Layout" --post_status=publish --format=ids) # Ensure fields are selected in meta so they display wp post meta update $SC_ID ttp_selected_field 'a:3:{i:0;s:5:"email";i:1;s:9:"telephone";i:2;s:4:"name";}' - Public Page:
wp post create --post_type=page --post_title="Showcase" --post_status=publish --post_content="[tlpteam id='$SC_ID']"
Expected Results
- The
tlp_md_popup_singlerequest should return a JSON object containing theConfidential Bio,john.private@example.com, and+1-555-0199. - The
successflag in the response should betrue(if SmartPopup) or theerrorflag should befalse(if SinglePopup), even though the requester is unauthenticated and the member is adraft.
Verification Steps
- Verify Response Content:
- Check if
datafield in the JSON response contains the stringjohn.private@example.com.
- Check if
- Check Status Bypass:
- Run
wp post get [MEMBER_ID] --field=post_statusto confirm the member is indeeddraft. - If the AJAX response still returns the member's data, the authorization bypass is confirmed.
- Run
Alternative Approaches
Summary
The Team plugin for WordPress fails to validate the post status of team members in multiple AJAX endpoints, allowing unauthenticated attackers to view sensitive profile data (emails, phone numbers, resumes) for members set to draft or private status.
Vulnerable Code
// app/Controllers/Frontend/Ajax/SinglePopup.php:49 public function response() { $html = $htmlCInfo = null; $error = true; if ( ! wp_verify_nonce( Fns::getNonce(), Fns::nonceText() ) ) { // ... } if ( isset( $_REQUEST['id'] ) && $post_id = absint( $_REQUEST['id'] ) ) { global $post; $post = get_post( absint( $_REQUEST['id'] ) ); if ( $post && $post->post_type == rttlp_team()->post_type ) { $error = false; --- // app/Controllers/Frontend/Ajax/SpecialLayout.php:53 public function response() { $memberId = ! empty( $_REQUEST['memberId'] ) ? absint( $_REQUEST['memberId'] ) : null; // ... if ( ! wp_verify_nonce( Fns::getNonce(), Fns::nonceText() ) ) { // ... } if ( $memberId ) { $name = get_the_title( $memberId );
Security Fix
@@ -49,7 +49,7 @@ if ( isset( $_REQUEST['id'] ) && $post_id = absint( $_REQUEST['id'] ) ) { global $post; $post = get_post( absint( $_REQUEST['id'] ) ); - if ( $post && $post->post_type == rttlp_team()->post_type ) { + if ( $post && $post->post_type == rttlp_team()->post_type && $post->post_status == 'publish') { $error = false; setup_postdata( $post ); $settings = get_option( rttlp_team()->options['settings'] ); @@ -50,8 +50,8 @@ ] ); } - - if ( $memberId ) { + $post = get_post( $memberId ); + if ( $memberId && $post && $post->post_status == 'publish' ) { $name = get_the_title( $memberId ); $designation = wp_strip_all_tags( get_the_term_list(
Exploit Outline
1. **Nonce Acquisition**: Access any public page on the WordPress site where the Team plugin enqueues its scripts. Extract the nonce from the localized JavaScript variable, typically found in the HTML source as `tlp_team_vars.nonce`. 2. **Target Identification**: Determine the Post ID of a target Team Member post (e.g., through ID enumeration or guessing). 3. **Data Retrieval**: Send an unauthenticated POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `tlp_md_popup_single` (or `rtGetSpecialLayoutData`) - `security`: [The extracted nonce] - `id`: [Target Member Post ID] 4. **Sensitive Disclosure**: The server will return a JSON object containing the HTML of the member's profile, including private metadata like email addresses, phone numbers, and full biographies, even if the post status is set to 'draft'.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.