作者:i9n0re

简介

看到了国外有大佬发了关于WordPress的一个非常有名的插件,contact form 7的漏洞,之前见到过很多WordPress站点使用这个插件,大佬写的比较笼统,一些详细的利用方式没有说的太明白.

漏洞成因

这个漏洞是由于插件开发者对WordPress的使用不当造成的,其实跟WordPress的逻辑有一定的关系,导致了可以发布普通文章的用户,可以绕过权限认证,进行发表原本插件作者只允许管理员创建和修改的自定义类型的"post"。

漏洞作者发现了contact from 7插件存在这样的缺陷,导致了任意一个可以发表普通文章的用户,可以新建一个contact,而且在5.0.3版本下,附件可以跨目录进行添加文件,进而可以读取网站的 wp-config.php

相关的技术点

nonce

首先我们了解下什么是nonce?nonce相当于csrf token是WordPress用来防御csrf问题的,并进行了相关的权限验证。

post_type

post_type是插件作者注册的自定义post类型,与WordPress的文章类似,插件作者要实现一个页面来进行管理文章类型。只有在后台的新建或者编辑页面当中可以获取到nonce随机数,提交的时候只有代入了nonce才能进行相应的操作。

漏洞详情

以contact form 7 v5.0.3为例。

插件作者只允许WordPress的editor才能新建和编辑contact。

如果是文章的发布者,就没有修改和创建权限,会显示下面的页面。

contact form 7也是一种自定义类型的 post ,数据里面都是存在了wp_posts表当中,通过 post_type进行区分。

-w260

正常情况,插件作者是通过 current_user_can('publish_pages') 进行权限的判定,也就是说editor以上的权限可以编辑,防止普通用户打开新建和修改文章的页面。

但是用户仍可以操作普通的文档,通过请求接口 wp-admin/post.php 的方式进行新建和编辑文章,只不过 post_type 变为了post等普通文档类型。由于插件作者在 register_post_type 的时候没有进行相关权限的配置,仅仅依靠了current_user_can('publish_pages')验证用户编辑权限,出现了安全问题。

接下来我们来看看普通文档的新建、编辑流程。

正常流程上,普通文档上传接口是请求 post-new.php后先生成一个 post,然后再进行编辑,请求post.php,设置参数 actioneditpost

// wp-admin/post.php
case 'editpost':
    check_admin_referer('update-post_' . $post_id);

    $post_id = edit_post();

    // Session cookie flag that the post was saved
    if ( isset( $_COOKIE['wp-saving-post'] ) && $_COOKIE['wp-saving-post'] === $post_id . '-check' ) {
        setcookie( 'wp-saving-post', $post_id . '-saved', time() + DAY_IN_SECONDS, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, is_ssl() );
    }

    redirect_post($post_id); // Send user on their way while we keep working

    exit();

通过函数 check_admin_referer 检测nonce是否合法。

// wp-includes/pluggable.php
    // Nonce generated 0-12 hours ago
    $expected = substr( wp_hash( $i . '|' . $action . '|' . $uid . '|' . $token, 'nonce'), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 1;
    }

    // Nonce generated 12-24 hours ago
    $expected = substr( wp_hash( ( $i - 1 ) . '|' . $action . '|' . $uid . '|' . $token, 'nonce' ), -12, 10 );
    if ( hash_equals( $expected, $nonce ) ) {
        return 2;
    }

漏洞利用的地方是通过传入action为post,调用下面这个逻辑。

// wp-admin/post.php
case 'post':
    check_admin_referer( 'add-' . $post_type );
    $post_id = 'postajaxpost' == $action ? edit_post() : write_post();
    redirect_post( $post_id );
    exit();

其中 post_type 是通过传入的 post_id 去数据库里面查询得到。

// wp-admin/post.php
if ( $post_id )
    $post = get_post( $post_id );

if ( $post ) {
    $post_type = $post->post_type;
    $post_type_object = get_post_type_object( $post_type );
}

可以看到,如果传入的 post_id为正常帖子创建请求,这个地方的 nonce 普通用户就可以通过页面进行获取了。

绕过nonce检测

可以看到 post_id 是通过 $_GET['post'] 或者 $_POST['post_ID']两种方式获取。

// wp-admin/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;

所以如果我们构建一个存在的并且post_typepost 的帖子ID作为参数传入的话, check_admin_referer 的参数变为了固定值 add-post 这样的话,如果我们拿到了nonce就可以绕过了检测。有同学会问,怎么得到这个nonce呢?通过跟代码,我发现在 dashboard 页面当中,下面这个功能里面就有我们需要的nonce,通过查看源代码,获取这个表单的input这样就绕过了检测。

 <input type="hidden" id="_wpnonce" name="_wpnonce" value="xxx" />

-w560

创建自定义类型的post

绕过了nonce检测后,我们来看 post 那个case,流程会进入到 write_post 函数,然后我们看到这个逻辑。

// wp-includes/post.php
    if ( !current_user_can( $ptype->cap->edit_posts ) ) {
        if ( 'page' == $ptype->name )
            return new WP_Error( 'edit_pages', __( 'Sorry, you are not allowed to create pages on this site.' ) );
        else
            return new WP_Error( 'edit_posts', __( 'Sorry, you are not allowed to create posts or drafts on this site.' ) );
    }

问题就出现在这,因为作者在注册post_type的时候没有进行权限限制,导致了权限提升。

v5.0.3 的插件配置:

//wp-content/plugins/contact-form-7/includes/contact-form.php
    public static function register_post_type() {
        register_post_type( self::post_type, array(
            'labels' => array(
                'name' => __( 'Contact Forms', 'contact-form-7' ),
                'singular_name' => __( 'Contact Form', 'contact-form-7' ),
            ),
            'rewrite' => false,
            'query_var' => false,
        ) );
    }

就导致了如果绕过了nonce检测,普通用户也就可以成功的创建只有editer权限才可以创建的 contact form 7了。

进一步利用

权限提升已经完成,下面就是利用了contact from 7 v5.0.3的一个问题。

-w816

当发送邮件的时候,可以携带附件,但是这个附件可以跨目录读取,导致了用户可以直接携带 wp-config.php 进行发送,实现敏感信息的泄露。

漏洞利用

理清了漏洞触发逻辑,利用方式就简单了,在后台登录页面,直接引用 poc.js

注意修改几个参数

  1. 修改get请求,query参数的post为已存在的帖子ID。
  2. 修改post参数中,_wpnonce为上文说的获取方式。
  3. 修改post参数中 meta_input[_mail][recipient] 参数为自己的收件箱。
  4. meta_input[_mail][attachments] 这个参数代表着想要获取的附件。
  5. 其他的标题,主题什么的参数看情况自己修改。

然后可以在控制台里面引用,会发现新建了一个表单,然后在帖子里面正常引用这个表单,再页面中使用,并点击发送后,在自己的收件箱当中收到 wp-config.php 的附件。

-w1279

漏洞修复

在注册 post_type 的时候,配置权限。

//wp-content/plugins/contact-form-7/includes/contact-form.php
    public static function register_post_type() {
        register_post_type( self::post_type, array(
            'labels' => array(
                'name' => __( 'Contact Forms', 'contact-form-7' ),
                'singular_name' => __( 'Contact Form', 'contact-form-7' ),
            ),
            'rewrite' => false,
            'query_var' => false,
            'capability_type' => 'page'
        ) );
    }

如果进行了这样的配置的话,在进行 write_post 这个函数逻辑的时候

// wp-includes/post.php
    if ( !current_user_can( $ptype->cap->edit_posts ) ) {
        if ( 'page' == $ptype->name )
            return new WP_Error( 'edit_pages', __( 'Sorry, you are not allowed to create pages on this site.' ) );
        else
            return new WP_Error( 'edit_posts', __( 'Sorry, you are not allowed to create posts or drafts on this site.' ) );
    }

这个判断才会生效,导致权限认证失败。

原文链接

poc.js


源链接

Hacking more

...