Multiple Critical Vulnerabilities Fixed In LearnPress Plugin Version <= 4.1.7.3.2

Published 24 January 2023
Table of Contents

If you're a LearnPress user, please update the plugin to at least version 4.2.0.

✌️ Our users are protected from this vulnerability. Are yours?

Web developers

Automatically mitigate vulnerabilities in real-time without changing code.

See pricing
Plugin developers

Identify vulnerabilities in your plugins and get recommendations for fixes.

Request audit
Hosting companies

Protect your users, improve server health and earn additional revenue.

Patchstack for hosts

Introduction to the LearnPress plugin vulnerability

The plugin LearnPress (versions 4.1.7.3.2 and below), which has over 100,000 active installations is a comprehensive WordPress LMS Plugin for WordPress. This is one of the most popular WordPress LMS Plugins which can be used to easily create & sell courses online. We can create a course curriculum with lessons & quizzes included which is managed with an easy-to-use interface for users.

LearnPress plugin

This plugin suffers from multiple critical vulnerabilities. These vulnerabilities allow any unauthenticated users to inject a SQL query to the database and perform local file inclusion. We also found another SQL injection that would need a user with at least "Contributor" role to be exploited. The described vulnerability was fixed in version 4.2.0.

The security vulnerability in LearnPress

Unauthenticated Local File Inclusion (CVE-2022-47615)

https://patchstack.com/database/vulnerability/learnpress/wordpress-learnpress-plugin-4-1-7-3-2-local-file-inclusion

The vulnerable code responsible for this vulnerability is located on inc/rest-api/v1/frontend/class-lp-rest-courses-controller.php function list_courses . This function is used to handle API request to lp/v1/courses/archive-course .

$template_pagination_path = $request['template_pagination_path'] ?? '';
if ( ! isset( $request['no_pagination'] ) ) {
    if ( ! empty( $template_pagination_path ) ) {
        $response->data->pagination = include $template_pagination_path;
    } else {
        $response->data->pagination = learn_press_get_template_content(
            'loop/course/pagination.php',
            array(
                'total' => $total_pages,
                'paged' => $filter->page,
            )
        );
    }
}
// End Pagination

// Content items
ob_start();
$template_path_item = urldecode( $request['template_path_item'] ?? '' );
$template_path      = urldecode( $request['template_path'] ?? '' ); // For wrapper all items, no foreach
$args_custom        = json_decode( wp_unslash( $request['args_custom'] ?? '' ), true );

// For custom template return all list courses no foreach
if ( ! empty( $template_path ) ) {
    if ( is_array( $args_custom ) && ! empty( $args_custom ) ) {
        extract( $args_custom );
    }

    if ( file_exists( $template_path ) ) {
        include $template_path;
    }
} else {
    // For custom template return all list courses foreach
    if ( ! empty( $template_path_item ) ) {
        if ( isset( $request['args_custom'] ) ) {
            $args_custom = json_decode( $request['args_custom'], true );
        }

        $template_path_item = urldecode( $template_path_item );
    }

    // Todo: tungnx - should rewrite call template
    foreach ( $courses as $course ) {
        $post = get_post( $course->ID );
        setup_postdata( $post );

        if ( ! empty( $template_path_item ) ) {
            if ( $args_custom ) {
                extract( $args_custom );
            }

            if ( file_exists( $template_path_item ) ) {
                include $template_path_item;
            }

There are 3 variable that unauthenticated user able to control to achieve LFI, $template_pagination_path , $template_path and $template_path_item .

Unauthenticated SQL Injection (CVE-2022-45808)

https://patchstack.com/database/vulnerability/learnpress/wordpress-learnpress-wordpress-lms-plugin-plugin-4-1-7-3-2-sql-injection

Besides our team findings, we received a report from our alliance member (Fadilah Agung Nugraha) that could trigger SQL injection as an unauthenticated user. The underlying code that is responsible for the vulnerability is located in inc/databases/class-lp-db.php function execute . The function accepts LP_Filter $filter as the first parameter, which could contain several data point for filtering purpose, such as "order by", "group by", etc.

The execute function could be seen called from several locations throughout the plugin, especially in the REST API process flow. This particular code responsible for the SQL injection:

// Order by
$ORDER_BY = '';
if ( ! $filter->return_string_query && $filter->order_by ) {
    $ORDER_BY .= 'ORDER BY ' . $filter->order_by . ' ' . $filter->order . ' ';
    $ORDER_BY  = apply_filters( 'lp/query/order_by', $ORDER_BY, $filter );
}

The code then will concatenate the $ORDER_BY variable as a whole query and then perform the SQL query:

// Query
$query = "SELECT $FIELDS FROM $COLLECTION AS $ALIAS_COLLECTION
$INNER_JOIN
$WHERE
$GROUP_BY
$ORDER_BY
$LIMIT
";

if ( $filter->return_string_query ) {
    return $query;
} elseif ( ! empty( $filter->union ) ) {
    $query  = implode( ' UNION ', array_unique( $filter->union ) );
    $query .= $GROUP_BY;
    $query .= $ORDER_BY;
    $query .= $LIMIT;
}

if ( ! $filter->query_count ) {
    // Debug string query
    if ( $filter->debug_string_query ) {
        return $query;
    }

    $result = $this->wpdb->get_results( $query );
}

// Query total rows
$query       = str_replace( array( $LIMIT, $ORDER_BY ), '', $query );
$query_total = "SELECT COUNT($filter->field_count) FROM ($query) AS $ALIAS_COLLECTION";
$total_rows  = (int) $this->wpdb->get_var( $query_total );

The above code then will trigger the SQL Injection.

Authenticated SQL Injection (CVE-2022-45820)

https://patchstack.com/database/vulnerability/learnpress/wordpress-learnpress-plugin-4-1-7-3-2-auth-sql-injection-sqli-vulnerability

There exists an authenticated SQL injection in 2 shortcodes of the plugin, learn_press_recent_courses and learn_press_featured_courses. The vulnerable code that handles those 2 shortcode are:

public function get_recent_courses( $args = array() ) {
    global $wpdb;

    $limit = ! empty( $args['limit'] ) ? $args['limit'] : - 1;
    $order = ! empty( $args['order'] ) ? $args['order'] : 'DESC';

    if ( $limit <= 0 ) {
        $limit = 0;
    }

    $query = apply_filters(
        'learn-press/course-curd/query-recent-courses',
        $wpdb->prepare(
            "
            SELECT DISTINCT p.ID
                FROM $wpdb->posts AS p
                WHERE p.post_type = %s
                AND p.post_status = %s
                ORDER BY p.post_date {$order}
                LIMIT %d
        ",
            LP_COURSE_CPT,
            'publish',
            $limit
        )
    );

    return $wpdb->get_col( $query );
}
public function get_featured_courses( $args = array() ) {
    global $wpdb;

    $limit    = ! empty( $args['limit'] ) ? $args['limit'] : - 1;
    $order_by = ! empty( $args['order_by'] ) ? $args['order_by'] : 'post_date';
    $order    = ! empty( $args['order'] ) ? $args['order'] : 'DESC';

    if ( $limit <= 0 ) {
        $limit = 0;
    }

    $query = apply_filters(
        'learn-press/course-curd/query-featured-courses',
        $wpdb->prepare(
            "
            SELECT DISTINCT p.ID
            FROM {$wpdb->posts} p
            LEFT JOIN {$wpdb->postmeta} as pmeta ON p.ID=pmeta.post_id AND pmeta.meta_key = %s
            WHERE p.post_type = %s
                AND p.post_status = %s
                AND pmeta.meta_value = %s
            ORDER BY p.{$order_by} {$order}
            LIMIT %d
        ",
            '_lp_featured',
            LP_COURSE_CPT,
            'publish',
            'yes',
            $limit
        )
    );

    return $wpdb->get_col( $query );
}

We can control the value of $args from the shortcode declaration. For example, in this case, the user that has at least Contributor role is able to trigger SQL Injection with using this shortcode on a drafted post:

[learn_press_recent_courses order=",(select sleep(10))" limit="1"]

The patch in LearnPress

Unauthenticated Local File Inclusion

Since these issues are mainly because the code tries to include some "template" from user input, the developer decided to remove the inclusion process completely. The patch can be found here:

LearnPress plugin
LearnPress plugin

Unauthenticated SQL Injection

Since the vulnerable part of the injection is located in the $ORDER_BY parameter, simply using a whitelist and sanitization on the variable would be sufficient. The patch can be seen below:

LearnPress plugin

Authenticated SQL Injection

Since the vulnerable part of the injection is located in the $order_by and $order parameter, simply using a whitelist and sanitization on the variable would be sufficient. The patch can be seen below:

LearnPress plugin
LearnPress plugin

Disclosure Timeline

30-11-2022 - We found the authenticated SQL Injection vulnerability and reached out to the plugin vendor.
02-12-2022 - We found the unauthenticated Local File Inclusion vulnerability.
04-12-2022 - We receive report of unauthenticated SQL Injection vulnerability from our alliance member (Fadilah Agung Nugraha).
05-12-2022 - We reached out to the plugin vendor again and gave them the additional information.
20-12-2022 - LearnPress plugin version 4.2.0 published to patch the reported issues.
20-01-2023 - Added the vulnerabilities to the Patchstack vulnerability database.
24-01-2023 - Published the article.

🤝 You can help us make the Internet a safer place

Plugin developer?

Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.

Get started for free
Hosting company?

Protect your users too! Improve server health and earn added revenue with proactive security.

Patchstack for hosts
Security researcher?

Report vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.

Learn more

The latest in Security Advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.
crossmenu