Hacker Targeting Vulnerability Fixed in WordPress Plugin LearnPress Late Last Month
On Monday, our Plugin Vulnerabilities Firewall plugin blocked a couple of exploit attempts on our website that we didn’t already have data to identify the WordPress plugin being targeted. In investigating that, we found they were attempts to exploit a remotedcode execution (RCE) vulnerability in the 90,000+ install WordPress plugin LearnPress, which was fixed on December 25 in version 4.2.5.8. The developer disclosed there was a security fix in that version, but barely. One of the changelog entries for that version reads “Fixed: security.”. The vulnerability allows an attacker to run arbitrary PHP code on the website.
This may be connected to CVE-2023-6634, though the record for it is lacking the information needed to be sure of that. If it is connected to that, the CVE Record is wrong, as it says “all versions up to, and including, 4.2.5.7” are vulnerable, but the code attempted to be exploited was added in 4.2.5.7.
Here was the log record for the first request blocked:
The first part of the URL, /wp-json/, tells you this is a request to the WordPress REST API. The second part, lp/v1/, is the route namespace, which is used by LearnPress. Next up the route, load_content_via_ajax. That relates to code in the plugins file /inc/rest-api/v1/frontend/class-lp-rest-ajax-controller.php.
In that file, this is the code that registers the function get_content() in the file to be accessible to anyone:
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public function __construct() { $this->namespace = 'lp/v1'; $this->rest_base = 'load_content_via_ajax'; parent::__construct(); } public function register_routes() { $this->routes = array( '/' => array( array( 'methods' => WP_REST_Server::ALLMETHODS, 'callback' => array( $this, 'get_content' ), 'permission_callback' => '__return_true', ), ), ); |
The relevant code from that function is this in the previous version of the plugin:
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | public function get_content( WP_REST_Request $request ): LP_REST_Response { $response = new LP_REST_Response(); try { $params = $request->get_params(); if ( empty( $params['callback'] ) || empty( $params['args'] ) ) { throw new Exception( 'Error: params invalid!' ); } // @var array $args $args = $params['args']; $callBack = $params['callback']; if ( $request->get_method() === 'GET' ) { $args = LP_Helper::json_decode( $params['args'], true ); $callBack = LP_Helper::json_decode( $params['callback'], true ); } if ( empty( $callBack['class'] ) || empty( $callBack['method'] ) ) { throw new Exception( 'Error: callback invalid!' ); } $class = $callBack['class']; $method = $callBack['method']; $data = null; if ( is_callable( [ $class, $method ] ) ) { $data = call_user_func( [ $class, $method ], $args ); } |
That code allows arbitrary code sent with a request to be run, so there was a remote code execution (RCE) vulnerability. The request blocked had code to make a request to another website. Seemingly, to try to confirm the vulnerability is exploitable on the website. We are not using the plugin, so even if our firewall hadn’t blocked the request, it wouldn’t have done anything.
The new version of the plugin added this code to limit what can be run to only things allowed by the plugin:
69 70 71 72 73 74 75 76 77 | // Security: check callback is registered. $allow_callbacks = apply_filters( 'lp/rest/ajax/allow_callback', [] ); $callBackStr = $class . ':' . $method; if ( ! in_array( $callBackStr, $allow_callbacks ) ) { throw new Exception( 'Error: callback is not register!' ); } |