E-Learning Platforms Getting Schooled – Multiple Vulnerabilities in WordPress’ Most Popular Learning Management System Plugins

April 29, 2020

Research by: Omri Herscovici and Sagi Tzadik

Overview

The COVID-19 pandemic has changed the way we live and work. “Sheltering in place” requires many people to work from home, thereby necessitating the use of virtual environments. The pandemic has also affected students globally, who are now at home learning via virtual classrooms online. This, in turn, has required many educational establishments to quickly integrate new Learning Management Systems into their platforms.

Learning Management Systems are E-Learning platforms used for delivering educational courses and training programs remotely. Not only university students use LMS; it’s for anyone interested in online learning.

In light of the increasing popularity of these platforms, we decided to audit the security of a few of them. Despite the somewhat shady reputation of WordPress plugins, they are still heavily used and are an integral part of most WordPress websites. This seems to be especially true in the case of Learning Management Systems, in which WordPress websites are the majority of the independent websites offering this service.

The 3 leading WordPress LMS Plugins are: LearnPress, LearnDash, and LifterLMS. These platforms can transform any WordPress website into a fully functioning and easy to use LMS. The 3 systems are installed on more than 100,000 different educational platforms and include universities such as the University of Florida, University of Michigan, University of Washington as well as hundreds of online academies. The impact multiplies as it affects all of the students in all of these establishments.

We focused only on these 3 systems since they seemed to the most impactful in this category. The LMS we researched invested quite a lot of effort in their security and some even implemented a bug bounty program. And indeed, the bugs we found were not trivial and required reaching interesting sinks. Therefore, in addition to notifying the developers, we also decided to share some of them with the security community.

Our approach was to see if a motivated student can accomplish the childhood dream of every hacker – take control of his educational institution, get test answers and even change students’ grades. As we assume some of our readers might not be familiar with web application exploitation, we sorted the bugs in an increasing level of complexity, to make things easier to follow.

Demo

https://www.youtube.com/watch?v=c-kisVHlQ3U

LearnPress
Vulnerable Versions: <= 3.2.6.7

LearnPress is WordPress’ most popular LMS plugin, as it enables website managers to easily create and sell online courses. According to BuiltWith, it is the second most popular internet platform in the Learning Management System category, and the most popular one in the United States – more than Moodle (which is the dominating open-source learning platform worldwide), with a 24,000 installation base versus 23,000. Overall, according to the official WordPress plugin website, it has 80,000+ installations, and the company developing the plugin states it is used in over 21,000 schools.

CVE-2020-6010: SQL Injection

TL;DR: This vulnerability is a Time-Based Blind SQL Injection which is very trivial to identify and exploit. It was a surprise to find it as we would have expected that by now prepared statements were a part of the norm. Although these types of vulnerabilities are usually easy to spot, they should not be underestimated as they have a large impact on the system’s integrity and can even result in the platform’s takeover.

The method _get_items of the class LP_Modal_Search_Items is vulnerable to SQL Injection. The method fails to sufficiently sanitize user-supplied data (the GET/POST parameter current_items) before using it in an SQL query.

An authenticated user can trigger this vulnerability by calling the Ajax method learnpress_modal_search_items which executes the following chain:
LP_Admin_Ajax::modal_search_itemsLP_Modal_Search_Items::get_items
LP_Modal_Search_Items::_get_items.

Although the technical details of this vulnerability are not particularly interesting, we decided to include it as a point for comparison to the one we found in LearnDash.

 

CVE-2020-11511: Becoming a Teacher

TL;DR: This vulnerability is a good example of legacy code forgotten behind resulting in a privilege escalation in the current design of the system.

The function learn_press_accept_become_a_teacher can be used to upgrade a registered user to a teacher role, resulting in a privilege escalation. Unexpectedly, the code doesn’t check the permissions of the requesting user, therefore letting any student call this function.

function learn_press_accept_become_a_teacher() {
   $action  = ! empty( $_REQUEST['action'] ) ? $_REQUEST['action'] : '';
   $user_id = ! empty( $_REQUEST['user_id'] ) ? $_REQUEST['user_id'] : '';
   if ( ! $action || ! $user_id || ( $action != 'accept-to-be-teacher' ) ) {
       return;
   }
   if ( ! learn_press_user_maybe_is_a_teacher( $user_id ) ) {
       $be_teacher = new WP_User( $user_id );
       $be_teacher->set_role( LP_TEACHER_ROLE );
       delete_transient( 'learn_press_become_teacher_sent_' . $user_id );
       do_action( 'learn_press_user_become_a_teacher', $user_id );
       $redirect = add_query_arg( 'become-a-teacher-accepted', 'yes' );
       $redirect = remove_query_arg( 'action', $redirect );
       wp_redirect( $redirect );
   }
}
add_action( 'plugins_loaded', 'learn_press_accept_become_a_teacher' );
...

This function is invoked once the activated plugins have been loaded, meaning that it can be called by simply supplying the action and user_id parameters to /wpadmin/, without even having to log in.

Just prior to releasing the blog we found that this vulnerability was a duplicate and was also discovered by Wordfence.

Both of the vulnerabilities we reported received the same treatment from the author – the vulnerable functions were completely purged from the new patched version. A classic case of “the best code is no code at all”.

LearnDash
Vulnerable Versions: < 3.1.6

LearnDash is a leading WordPress LMS plugin. According to BuiltWith, over 33,000 websites currently run LearnDash. LearnDash also stated to have been integrated into Fortune 500 companies as well as leading universities such as the University of Florida, University of Michigan, and University of Washington.

 
CVE-2020-6009: Unauthenticated Second-Order SQL Injection

TL;DR: This vulnerability is easy to spot but much harder to exploit, as it is not a simple case of incorporating untrusted user input directly into an SQL query. But this too, like any other SQL Injection, could have easily been prevented by the use of prepared statements.

The function learndash_get_course_groups in the ld-groups.php file is vulnerable to Second-Order SQL Injection. This function fails to sufficiently sanitize user-supplied data (although it is queried from the database, so one might assume it is safe) before using it in an SQL query. This vulnerability can be triggered without authentication.

Let’s have a look at the vulnerable function learndash_get_course_groups:

function learndash_get_course_groups( $course_id = 0, $bypass_transient = false ) {
...
        $sql_str = $wpdb->prepare("SELECT DISTINCT REPLACE(meta_key, 'learndash_group_enrolled_', '') FROM ". $wpdb->postmeta ." WHERE meta_key LI​KE %s AND post_id = %d and meta_value != ''", 'learndash_group_enrolled_%', $course_id );
...
        $col = $wpdb->get_col( $sql_str );
...
           $sql_str = "SELECT ID FROM $wpdb->posts WHERE post_type='groups' AND post_status = 'publish' AND ID IN (" . implode( ',', $col ) . ')';
           $course_groups_ids = $wpdb->get_col( $sql_str );
...

The function queries the table wp_postmeta for meta_keys of the format learndash_group_enrolled_%, for a specific post ($course_id). It then removes the learndash_group_enrolled_ prefix and uses the rest of the value in another SQL query. This means that if we could find a way to insert a malicious record into wp_postmeta, where we control the value of meta_key, we would have an SQL injection.

Conveniently enough, the file ipn.php, which is responsible for handling IPN transactions, contains the following code:

...
// log transaction
ld_ipn_debug( 'Starting Transaction Creation.' );
$transaction = $_REQUEST; // <--- controlled input!
$transaction['user_id'] = $user_id;
$transaction['course_id'] = $course_id; // <--- controlled input!
$transaction['log_file'] = basename($ipn_log_filename);
$course_title = '';
$course       = get_post( $course_id );
if ( ! empty( $course) ) {
   $course_title = $course->post_title;
}
ld_ipn_debug( 'Course Title: ' . $course_title );
$post_id = wp_insert_post( array('post_title' => "Course {$course_title} Purchased By {$email}", 'post_type' => 'sfwd-transactions', 'post_status' => 'publish', 'post_author' => $user_id) );
ld_ipn_debug( 'Created Transaction. Post Id: ' . $post_id );
foreach ( $transaction as $k => $v ) {
   update_post_meta( $post_id, $k, $v ); // <--- this creates the malicious post meta that is later queried and used in another SQL query.
}
...

PayPal IPN is PayPal’s way to notify websites about PayPal transactions. Workflow:

  1. PayPal sends a POST request containing information about the transaction to the website’s IPN listener. The data in this request is signed with the verify_sign parameter.
  2. The website’s IPN listener sends the complete message back to PayPal using HTTPS, adding the parameter cmd=_notify-validate.
  3. PayPal responds with either VERIFIED or INVALID.

You can read more about PayPal IPN here. PayPal’s IPN Simulator can be used to craft IPN requests in a sandbox environment.

The code snippet from ipn.php effectively allows us to create a record in the table wp_postmeta (via the function update_post_meta) with a meta_key and meta_value of our choice, which is later exploited to an SQL Injection by invoking learndash_get_course_groups.

Now that we can insert a malicious record in wp_postmeta, we need to find a way to invoke learndash_get_course_groups. Once again, ipn.php can be used to achieve this goal.

ipn.php naturally calls learndash_get_course_groups via the following call chain:
ipn.phpld_update_course_accesslearndash_update_course_users_groupslearndash_get_course_groups

We describe the exploitation process for PayPal’s sandbox environment for convenience sake, but this vulnerability exists in websites using the live environment as well.

The first step to exploit this vulnerability requires us to use the IPN Simulator to craft a valid IPN request. For the $course_id needed in ipn.php (item_number in the IPN request), we use ID=1 (as it is never validated that this post is an actual course). Then, we extend the IPN request with the following parameters:
debug=1&learndash_group_enrolled_1)INJECTION_POINT%23=1

There are three points that should be noted about this request:

  1. debug=1 causes the creation of a log file in the path /wp-content/uploads/learndash/paypal_ipn/ which contains the id of the new post we just created which has a malicious meta_key. We need this post id for later use. The log file name that is created is simply time().log, which can be guessed / enumerated. The post number can also be enumerated.
  2. Usually, an injection occurs in the value part of the key=value pair (either in GET query parameters or POST parameters). In this case, our injection occurs in the key part of the key=value pair. We cannot use spaces there because PHP replaces them with underscores when accessing $_REQUEST. However, we can use /**/ instead of spaces, as it is a valid character for the key part and is a valid replacement for spaces in SQL queries (SELECT 1 is the same as SELECT/**/1).
  3. We append %23 to the end of our injection point, which is an encoded representation of #, to comment out the rest of the original query.

We now have to use the post id we found in point #1, and send an IPN request with item_number=post_id. This triggers the SQL Injection.

This is what the executed query looks like:

SELECT ID FROM wp_posts WHERE post_type='groups' AND post_status = 'publish' AND ID IN (1)[MALICIOUS SQL QUERY]#)

LifterLMS
Vulnerable Versions: < 3.37.15

LifterLMS is a leading LMS WordPress plugin. According to BuiltWith, approximately 17,000 websites use this plugin, including WordPress agencies and educators, along with various school and educational establishments.

 
CVE-2020-6008: Arbitrary File Write

TL;DR: An interesting example of Arbitrary File Write vulnerability, exploiting the dynamic nature of PHP applications – requiring us to reach an interesting sink in an unexpected way.

Since WordPress enables plugins to register new actions to its admin-ajax handler, the LifterLMS registered its own handle() function:

public static function handle() {
   // Make sure we are getting a valid AJAX request
   check_ajax_referer( self::NONCE );
   // $request = self::scrub_request( $_REQUEST );
   $request = $_REQUEST;
   $response = call_user_func( 'LLMS_AJAX_Handler::' . $request['action'], $request );
...

The function invokes call_user_func in LLMS_AJAX_Handler, based on the sent action variable. One of the available functions in LLMS_AJAX_Handler is export_admin_table:

public static function export_admin_table( $request ) {
   require_once 'admin/reporting/class.llms.admin.reporting.php';
   LLMS_Admin_Reporting::includes();
   $handler = 'LLMS_Table_' . $request['handler'];
   if ( class_exists( $handler ) ) {
  	$table = new $handler();
  	$file  = isset( $request['filename'] ) ? $request['filename'] : null;
  	return $table->generate_export_file( $request, $file );
...

The function first creates a new class based on the handler variable sent to it, and then calls its generate_export_file function with the filename variable also sent in the request. The generate_export_file is an inherited function that should create a CSV file based on the information pulled in the correspondent LLMS_Table class we created using the handler variable.

However, the code fails to verify that the extension in the filename variable is indeed a CSV. The reason is because when no $type is sent, it defaults to CSV.

public function generate_export_file( $args = array(), $filename = null, $type = 'csv' ) {
   if ( 'csv' !== $type ) {
  	return false;
   }
..
$file_path   = LLMS_TMP_DIR . $filename;
…
$handle = @fopen( $file_path, 'a+' );
...

At this point, an attacker can intercept a standard Ajax request, and use the ajax_nonce variable, created organically, to reach the generate_export_file with a filename that creates PHP files in an arbitrary location.

This bug enables an attacker to write PHP files, but without any control over the content. However, one of the LLMS_Tables we can use is LLMS_Tables_Course_Students.

The registered student can easily check what courses id he is registered to, and send it in the Ajax request. This outputs his name to the generated file.

The user can go to his profile page and change his first name to a desired PHP code, i.e TEST <?php phpinfo(); /*.

The WordPress input filter mechanism doesn’t allow for both opening and closing angle brackets (< >) in the same input, but since PHP is a forgiving language, we can use only the opening angle bracket anywhere in the file, and comment out the rest of the content using (/*).

At this point, simply browsing to the generated PHP file executes the PHP code written in the user’s first name – effectively achieving code execution on the server.

Impact

  • Stealing personal information just like in any other compromised platform that has registered users (names, emails, usernames, passwords, etc…)
  • The platforms involve payment, therefore, financial schemes are also applicable in the case of modifying the website without webmaster’s information
  • Student specific:
    • changing grades for themselves
    • changing grades for peers
    • certificate forgery
    • retrieving the test beforehand
    • retrieve tests answers
    • escalate their privileges to that of a teacher

Summary

We focused on 3 of the most common LMS plugins: LearnPress, LearnDash and LifterLMS, and found vulnerabilities ranging from Privilege Escalation through SQL Injection up to full Remote Code Execution.

In total, we found 4 vulnerabilities that were assigned CVE-2020-6008, CVE-2020-6009 and CVE-2020-6010 and one duplicate CVE-2020-11511.

These vulnerabilities allow students and sometimes even unauthenticated users to gain sensitive information, edit personal records, and even take control of the LMS platforms.

Due to the recent increase in the popularity of E-Learning platforms, these are urgent issues. The developers have since released fixes to the platforms.

We urge users to upgrade to the latest versions of these platforms:

Check Point IPS protects against this threat:

WordPress LearnDash Plugin SQL Injection (CVE-2020-6009)
WordPress LearnPress Plugin Privilege Escalation
WordPress LearnPress Plugin SQL Injection
WordPress LifterLMS Plugin Arbitrary File Write (CVE-2020-6008)