研究人员在最常见的Learning Management Systems(LMS)插件LearnPress、LearnDash和LifterLMS中发现了多个安全漏洞,包括权限提升漏洞、SQL注入、远程代码执行漏洞。
研究人员共发现了4个漏洞,分别是CVE-2020-6008、CVE-2020-6009、CVE-2020-6010和CVE-2020-11511。攻击者利用这些漏洞可以让学生甚至非认证的用户获取敏感细腻些、编辑个人记录,甚至控制LMS平台。
是WordPress最主流的LMS插件,可以让网站管理员很容易地创建和销售在线课程。据BuiltWith消息,LearnPress是在线学习管理系统中排名第二的互联网平台,安装量超过24000。据WordPress插件网站官方消息,其安装量超过80000+,开发插件的公司称该插件在超过21000所学校使用。漏洞影响版本号低于3.2.6.7的LearnPress版本。
该漏洞是一个根据时间的盲注入漏洞,很难发现和利用。类LP_Modal_Search_Items的方法_get_items存在SQL注入漏洞。该方法在将用户提供的数据作为SQL查询之前没有进行正则处理。经过认证的用户可以调用Ajax方法learnpress_modal_search_items来触发该漏洞,该方法会执行以下链:
LP_Admin_Ajax::modal_search_items → LP_Modal_Search_Items::get_items →
LP_Modal_Search_Items::_get_items.
该漏洞是一个继承性质的权限提升漏洞。
函数learn_press_accept_become_a_teacher
可以用来将一个注册的用户升级为teacher
角色,导致权限提升。由于代码没有检查请求用户的权限,因此任意的学生角色都可以调用该函数:
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' );
...
该函数在激活的插件加载时就会被调用,也就是说提供action和user_id
参数给/wpadmin/
就可以被调用,甚至无需登入。
研究人员发现Wordfence也发现了该漏洞。
LearnDash是一个常用的WordPress LMS插件。据BuiltWith,目前有超过33000个网站运行着LearnDash。LearnDash广泛用于财富五百强公司和一些顶尖大学。漏洞影响版本号低于3.1.6的LearnDash版本。
该漏洞与其他SQL注入漏洞一样,通过使用预先准备的statement可以预防。漏洞位于ld-groups.php
文件中的learndash_get_course_groups
函数。该函数没有对用户提供的用作SQL查询的数据进行正则化处理。该漏洞可以在没有认证的情况下触发。
有漏洞的函数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 LIKE %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 );
...
函数查询表wp_postmeta来对特定的post ($course_id)查询learndash_groupenrolled%的meta_keys。然后移除learndash_groupenrolled prefix,并将剩余值用于另一个SQL查询中。也就是说如果可以找到一种方式插入恶意记录到wp_postmeta 中,就可以控制meta_key的值,也就实现了SQL 注入。
此外,负责处理IPN 交易的ipn.php
文件中含有以下代码:
...
// 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是PayPal通知网站PayPal交易的方式。
PayPal会发送一个含有关于交易信息的POST请求到网站的IPN监听器。请求中的数据会用verify_sign
参数来签名。
网站的IPN监听器会用HTTPS发送完整的消息回PayPal,添加参数cmd=_notify-validate
。
PayPal会响应VERIFIED
或 INVALID
。
然后就可以读取PayPal IPN。PayPal的 IPN模拟器可以在沙箱环境中构造IPN请求。
ipn.php中的代码可以允许攻击者用meta_key
和meta_value
在wp_postmeta
表中创建一条记录,之后调用learndash_get_course_groups
来利用SQL注入。
可以在wp_postmeta
中插入一条恶意记录,因此需要找到一种调用learndash_get_course_groups
的方法。研究人员想到了ipn.php
文件。
ipn.php
可以通过以下调用链来调用learndash_get_course_groups
:
ipn.php → ld_update_course_access → learndash_update_course_users_groups → learndash_get_course_groups
利用该漏洞的第一步是需要使用IPN Simulator来伪造一个有效的IPN请求。对ipn.php
中的$course_id
,研究人员使用ID=1
。然后,用以下参数扩展IPN请求:
debug=1&learndash_group_enrolled_1)INJECTION_POINT%23=1
在该请求中需要考虑3点:
debug=1
会引发路径/wp-content/uploads/learndash/paypal_ipn/
中的一个日志文件创建,该路径中含有一个含有恶意meta_key
的新post。日志文件名是一个简单的time().log
,可以通过枚举来猜测。
一般来说,注入发生在key=value
的value
部分,但是本例中的注入发生在key=value
的key
部分。而且无法使用空格,因为PHP在访问$_REQUEST
时会替换空格。但是可以使用/**/
来替代空格,这是key部分的一种有效字符,在SQL查询中也是空格的一种有效替换,比如SELECT 1
和SELECT/**/1
是等价的。
研究人员将%23
加到了注入点的后面,这是#
的编码表示。然后需要使用post id
,发送含有item_number=post_id
的IPN请求,这会触发SQL注入。
整个查询的执行如下所示:
SELECT ID FROM wp_posts WHERE post_type='groups' AND post_status = 'publish' AND ID IN (1)[MALICIOUS SQL QUERY]#)
LifterLMS是另外一个常用的 LMS WordPress插件。根据BuiltWith的统计,大约有17000个网站使用该插件,包括学校和教育机构。
该漏洞是一个任意文件写漏洞,利用了PHP的动态特征/因为WordPress可以让插件注册新的action到admin-ajax handler,LifterLMS可以注入自己的handle()函数:
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 );
...
该函数会根据sent action变量调用LLMS_AJAX_Handler
的call_user_func
。LLMS_AJAX_Handler
中一个可用的函数就是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 );
...
该函数首先会根据发送的handler变量来创建一个新的类,然后用请求中发送的filename变量来调用generate_export_file
函数。generate_export_file
是一个继承函数,应该根据用handler变量创建的对应LLMS_Table类的信息来创建一个CSV文件。
但是代码没有成功验证filename变量中的扩展。原因是没有发送$type
,因为默认是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+' );
...
此时,攻击者可以拦截一个标准的Ajax
请求,使用ajax_nonce
变量来访问generate_export_file
。
该漏洞使得攻击者可以写任意的PHP文件,但无法控制任何内容。研究人员发现有一个可以使用的LLMS_Tables
是LLMS_Tables_Course_Students
。
注册的学生可以检查注册了哪些courses id,然后在Ajax请求中发送。然后可以将名字输出到生成的文件中。
用户可以到profile页,并修改姓到期望的PHP代码,比如TEST <?php phpinfo(); /*
。
WordPress输入过滤器机制并没有在相同的输入中考虑< >,但是可以在文件中使用任意的<
,然后再其他部分使用注释符号/*
。
这样,只要浏览生成的PHP文件就会执行用户的姓中写的PHP代码,实现服务器上的代码执行。