一对一免费咨询: 13913005726 025-66045668

WordPress核心功能SQL注入漏洞分析

威胁响应中心研究员对Wordpress核心功能SQL注入漏洞(编号为CVE-2015-5623和CVE-2015-2213)进行了详细的分析

0x00 漏洞概述

在twitter上看到Wordpress核心功能出现SQL注入漏洞,想学习下,就深入的跟了下代码,结果发现老外留了好大的一个坑。虽然确实存在注入问题,但是却没有像他blog中所说的那样可以通过订阅者这样的低权限来触发SQL注入漏洞。

这个Wordpress漏洞系列的文章目前更新了两个部分,一个是通过权限绕过实现订阅者权限写一篇文章到回收站,另一个就是通过写入的这篇文章来实现SQL注入漏洞。这两个漏洞的描述,TSRC的phithon写的文章其实已经很清楚了,我这里从我分析的角度来介绍这两个漏洞的形成、利用以及phithon省略掉的原文部分内容。

0x01 越权提交文章

_wpnonce的获取

在讲越权漏洞之前,我们需要介绍一下Wordpress后台的_wpnonce参数,这个参数主要是用来防止CSRF攻击的token。后台大多数敏感功能都会通过,当前用户信息、功能名称、操作对象id等内容生成token,所以我们很难在没有token的时候进行某些功能的使用。这个CSRF的防护机制间接地导致之后SQL注入很难再低权限情况下触发(因为看不到token),后面讲SQL注入漏洞时,我们会详细的聊这个问题有多坑。

之所以要把_wpnonce的获取放在前面讲,是因为,我们需要一个让我们可以使用编辑提交文章功能的token。这个功能的token我们可以通过访问后台post.php的post-quickdraft-save功能来获取,严格的来说这个获取方式也算是一个信息泄露漏洞,官方也在新版本中进行了修补。下面我我们来看下这个token泄露的原因。部分代码如下:

case 'post-quickdraft-save':

// Check nonce and capabilities

$nonce = $_REQUEST['_wpnonce'];

$error_msg = false;

// For output of the quickdraft dashboard widget

require_once ABSPATH . 'wp-admin/includes/dashboard.php';

if ( ! wp_verify_nonce( $nonce, 'add-post' ) )

$error_msg = __( 'Unable to submit this form, please refresh and try again.' );

if ( ! current_user_can( 'edit_posts' ) )

$error_msg = __( 'Oops, you don’t have access to add new drafts.' );

if ( $error_msg )

return wp_dashboard_quick_press( $error_msg );

从上面代码我们可以看出这个功能在发现出现错误时,会将错误信息通过wp_dashboard_quick_press这个函数打印到页面上,而这个函数生成的页面的代码中有这样一行:

PHP wp_nonce_field( 'add-post' ); 1 wp_nonce_field('add-post');

生成了add-post功能的_wpnonce,也就是说,即使我们做了一些被禁止的操作,返回页面中也会出现这个_wpnonce。

越权提交与条件竞争

如phithon文章所说,由于GP使用逻辑混乱而导致的验证绕过。我们来看post.php文件在开始检验文章是否存在的代码:

if ( isset( $_GET['post'] ) )

$post_id = $post_ID = (int) $_GET['post'];

elseif ( isset( $_POST['post_ID'] ) )

$post_id = $post_ID = (int) $_POST['post_ID'];

else

$post_id = $post_ID = 0;

global $post_type, $post_type_object, $post;

if ( $post_id )

$post = get_post( $post_id );

门户网站设计公司

if ( $post ) {

$post_type = $post-post_type;

$post_type_object = get_post_type_object( $post_type );

}

可以看到在先提取GET中的post参数作为文章id来提取文章信息,如果没有GET的post参数,再去用POST的post_ID参数来提取。而真正检验用户是否对文章具有编辑权限,则是在edit_post中进行的,而这里的判断参数是从POST中提取的:

function edit_post( $post_data = null ) {

global $wpdb;

if ( empty($post_data) )

$post_data = &$_POST;

// Clear out any data in internal vars.

unset( $post_data['filter'] );

$post_ID = (int) $post_data['post_ID'];

$post = get_post( $post_ID );

$post_data['post_type'] = $post-post_type;

$post_data['post_mime_type'] = $post-post_mime_type;

if ( ! empty( $post_data['post_status'] ) ) {

$post_data['post_status'] = sanitize_key( $post_data['post_status'] );

if ( 'inherit' == $post_data['post_status'] ) {

unset( $post_data['post_status'] );

}

}

$ptype = get_post_type_object($post_data['post_type']);

if ( !current_user_can( 'edit_post', $post_ID ) ) {

if ( 'page' == $post_data['post_type'] )

wp_die( __('You are not allowed to edit this page.' ));

else

wp_die( __('You are not allowed to edit this post.' ));

}

我们继续看这段代码最后的if判断,判断当前用户是否有编辑这篇文章的权限。这个判断最终的操作是在map_meta_cap这个函数中进行的:

case 'edit_post':

case 'edit_page':

$post = get_post( $args[0] );

if ( empty( $post ) )

break;

if ( 'revision' == $post-post_type ) {

$post = get_post( $post-post_parent );

}

可以看出如果文章是不存在的,那么就会break出switch,在函数结束时返回$caps变量,而$caps在函数开始的时候已经被定义为一个空的数组了,也就是说,这里会返回一个空数组。下面我们来向前走,返回到调用map_meta_cap的has_cap函数中,看看后续的操作

public function has_cap( $cap ) {

if ( is_numeric( $cap ) ) {

_deprecated_argument( __FUNCTION__, '2.0',
__('Usage of user levels by plugins and themes is deprecated. Use roles and capabilities instead.') );

$cap = $this-translate_level_to_cap( $cap );

}

$args = array_slice( func_get_args(), 1 );

$args = array_merge( array( $cap, $this-ID ), $args );

许昌网站建设公司 $caps = call_user_func_array( 'map_meta_cap', $args );

// Multisite super admin has all caps by definition, Unless specifically denied.

if ( is_multisite() && is_super_admin( $this-ID ) ) {

if ( in_array('do_not_allow', $caps) )

return 网站建设营销全案公司 false;

return true;

}

$capabilities = apply_filters( 'user_has_cap', $this-allcaps, $caps, $args, $this );

$capabilities['exist'] = true; // Everyone is allowed to exist

foreach ( (array) $caps as $cap ) {

if ( empty( $capabilities[ $cap ] ) )

return false;

}

return true;

}

看最后那个foreach,它检测$caps中的各个元素在$capabilities中是否有不存在的,如果有那么就return false,但是$caps是个空数组,这样很容易我们就获得了一个true,权限校验成功绕过。那么这样我们可以得到的一个信息就是,我们可以通过这个缺陷去尝试update一个并不存在的文章。

那么现在问题柳州网站建设公司来了,直接update一个不存在的文章是没有意义的,因为数据库执行SQL是肯定会报错,我们要怎么才能成功创建一篇文章呢?

在校验权限之后,数据库执行操作之前,post.php中有这样一段代码:

if ( isset( $post_data['tax_input'] ) ) {

foreach ( (array) $post_data['tax_input'] as $taxonomy = $terms ) {

// Hierarchical taxonomy data is already sent as term IDs, so no conversion is necessary.

if ( is_taxonomy_hierarchical( $taxonomy ) ) {

continue;

}

if ( ! is_array( $terms ) ) {

$comma = _x( ',', 'tag delimiter' );

if ( ',' !== $comma ) {

$terms = str_replace( $comma, ',', $terms );

}

$terms = explode( ',', trim( $terms, " \n\t\r\x0B," ) );

}

$clean_terms = array();

foreach ( $terms as $term ) {

// Empty terms are invalid input.

if ( empty( $term ) ) {

continue;

}

$_term = get_terms( $taxonomy, array(

'name' = $term,

'fields' = 'ids',

'hide_empty' = false,

) );

如果在POST的数据中存在名为tax_input的数组参数,那么就对参数中的值使用逗号进行切割,然后对分割出来的每个内容进行一次select查询。那么这里我们可以想象一下,如果我们post_ID中填写的是对当前最新文章ID+1的数值,并且在tax_input这个参数添加特别多的内容,导致它不停地select查询,我们是不是在这个停滞的时间当中插入一篇文章(这篇文章的ID刚好是最新文章ID+1),那么后面的update是不是就有意义了?


 


 南京牧狼文化传媒有限公司简介:


      牧狼传媒,牧者之心,狼者之性,以牧之谦卑宽容之心待人,以狼之团结无畏之性做事!


  公司注册资金100万,主营众筹全案服务、网站营销全案服务、网站建设、微信小程序开发、电商网店设计、H5页面设计、腾讯社交广告投放以及电商营销推广全案等相关业务,致力于为客户提供更有价值的服务,创造让用户满意的效果!


  为百度官方及其大客户、苏宁易购、金山WPS秀堂、美的、创维家电、新东方在线、伊莱克斯、宝丽莱等国内国外知名品牌服务过,服务经验丰富!同时,公司也是南京电子商务协会会员单位、猪八戒网官方认证签约服务商、江苏八戒服务网联盟、南京浦口文化产业联合会会员单位,可以为您提供更好的服务!


  主营项目:众筹全案服务、网站营销全案服务、网站建设、微信小程序开发、电商网店设计、H5页面设计、腾讯社交广告投放、竞价托管、网站优化、电商代运营等


  合作客户:百度、苏宁易购、饿了么、美的、创维家电、新东方在线、宝丽莱、金山WPS秀堂、伊莱克斯


  资质荣誉:百度商业服务市场2017年度最佳图片服务商、南京电子商务协会会员单位、猪八戒网官方认证签约服务商、江苏八戒服务网联盟、南京浦口文化产业联合会会员单位、八戒通TOP服务商、"易拍即合杯"H5创意大赛"三等奖"。



致力于为客户创造更多价值
13913005726 025-66045668
需求提交
电话咨询
在线咨询