WordPress REST API 内容注入漏洞分析
2017-02-10
WordPress是一种使用PHP语言开发的博客平台,用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站。也可以把 WordPress当作一个内容管理系统来使用。
WordPress 在4.7.0版本之后将REST API插件集成到默认功能之中。REST
API为WordPress的使用者提供了一个方便快捷的管理接口。在WordPress
4.7.0-4.7.1版本中存在着一个越权漏洞,成功的利用这个漏洞,可以绕过管理员权限对文章进行增删改查操作。
影响版本
WordPress 4.7.0-4.7.1
漏洞分析
在正式的漏洞分析开始前,先来简单介绍下REST API的使用。官网给出的介绍如下
具体使用详情请参照REST API Handbook
https://developer.wordpress.org/rest-api/
在使用api对文章进行操作之前,需要对操作进行授权,授权方式有三种:cookie、oauth和简单认证。如果不进行授权直接通过api对文章进行修改操作,会返回一个401,如下图所示
如果想成功利用漏洞,必须绕过权限管理。我们跟踪下后台update处权限管理代码
public function update_item_permissions_check( $request ) { $post = get_post( $request['id'] ); $post_type = get_post_type_object( $this->post_type ); if ( $post && ! $this->check_update_permission( $post ) ) { return new WP_Error( 'rest_cannot_edit' __( 'Sorry you are not allowed to edit this post.' ) array( 'status' => rest_authorization_required_code() ) ); } if ( ! empty( $request['author'] ) && get_current_user_id() !== $request['author'] && ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( 'rest_cannot_edit_others' __( 'Sorry you are not allowed to update posts as this user.' ) array( 'status' => rest_authorization_required_code() ) ); } if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) { return new WP_Error( 'rest_cannot_assign_sticky' __( 'Sorry you are not allowed to make posts sticky.' ) array( 'status' => rest_authorization_required_code() ) ); } if ( ! $this->check_assign_terms_permission( $request ) ) { return new WP_Error( 'rest_cannot_assign_term' __( 'Sorry you are not allowed to assign the provided terms.' ) array( 'status' => rest_authorization_required_code() ) ); } return true; }
如上图所示,在这里if语句中,前半部分$post为真,后半部分check_update_permission()函数对$post的权限进行判断,结果为假,导致返回Sorry
you are not allowed to edit this post信息。
这个漏洞的挑战就在于,如何成功的绕过update_item_permissions_check()模块,使其最终return true。
我们回头看update_item_permissions_check()函数,
注意到$post = get_post( $request[‘id’] );这一行,这一行的作用是判断提交的文章id是否存在。
function get_post( $post = null $output = OBJECT $filter = 'raw' ) { if ( empty( $post ) && isset( $GLOBALS['post'] ) ) $post = $GLOBALS['post']; if ( $post instanceof WP_Post ) { $_post = $post; } elseif ( is_object( $post ) ) { if ( empty( $post->filter ) ) { $_post = sanitize_post( $post 'raw' ); $_post = new WP_Post( $_post ); } elseif ( 'raw' == $post->filter ) { $_post = new WP_Post( $post ); } else { $_post = WP_Post::get_instance( $post->ID ); } } else { $_post = WP_Post::get_instance( $post ); } if ( ! $_post ) return null; $_post = $_post->filter( $filter ); if ( $output == ARRAY_A ) return $_post->to_array(); elseif ( $output == ARRAY_N ) return array_values( $_post->to_array() ); return $_post; }
可见,如果id对应的文章不存在,则返回null。
如果我们输入的url是这种形式
http://192.168.3.112/wordpress/index.php/wp-json/wp/v2/posts/1/?id=1grq
get_post()函数返回值一定为null,这样会使得$post值为null,回头来看update_item_permissions_check()函数,这时update_item_permissions_check()函数的返回值竟然为true了!
既然update_item_permissions_check()函数的返回值为true,说明我们绕过了update_item_permissions_check()权限验证,但是id为‘1grq’,根本不存在这样数字加字母组合的文章id,那怎么才能对指定文章进行越权操作呢?
有趣的事情发生了,让我们来看update_item()函数,这个函数是用来update通过权限验证的数据,我们看看它是怎么定义的
注意第二行,这里将$request[‘id’]进行了int类型的数值转换,在php中,$request[‘id’]
=”1grq”会被转换为数值类型1,如下图演示
所以这里的$id又由‘1grq’变回1了,get_post()函数也会找到对应id为1的文章了。
漏洞利用
目前已经有研究员在GitHub上给出相应的poc,链接如下
https://gist.github.com/leonjza/2244eb15510a0687ed93160c623762ab
官方修补方案分析
在4.7.2版本中,如果get_post()判定$post结果为 false,则直接返回$post,避免$post进入下层if判定绕过权限检查。
修补防御
升级wordpress至最新版本(4.7.2)。