作者:i9n0re
看到了国外有大佬发了关于WordPress的一个非常有名的插件,contact form 7的漏洞,之前见到过很多WordPress站点使用这个插件,大佬写的比较笼统,一些详细的利用方式没有说的太明白.
这个漏洞是由于插件开发者对WordPress的使用不当造成的,其实跟WordPress的逻辑有一定的关系,导致了可以发布普通文章的用户,可以绕过权限认证,进行发表原本插件作者只允许管理员创建和修改的自定义类型的"post"。
漏洞作者发现了contact from 7插件存在这样的缺陷,导致了任意一个可以发表普通文章的用户,可以新建一个contact,而且在5.0.3版本下,附件可以跨目录进行添加文件,进而可以读取网站的 wp-config.php
。
首先我们了解下什么是nonce?nonce相当于csrf token
是WordPress用来防御csrf问题的,并进行了相关的权限验证。
post_type
是插件作者注册的自定义post
类型,与WordPress的文章类似,插件作者要实现一个页面来进行管理文章类型。只有在后台的新建或者编辑页面当中可以获取到nonce
随机数,提交的时候只有代入了nonce才能进行相应的操作。
以contact form 7 v5.0.3为例。
editor
才能新建和编辑contact。如果是文章的发布者,就没有修改和创建权限,会显示下面的页面。
contact form 7也是一种自定义类型的 post
,数据里面都是存在了wp_posts
表当中,通过 post_type
进行区分。
正常情况,插件作者是通过 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,设置参数 action
为 editpost
。
// 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
普通用户就可以通过页面进行获取了。
可以看到 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_type
为 post
的帖子ID作为参数传入的话, check_admin_referer
的参数变为了固定值 add-post
这样的话,如果我们拿到了nonce就可以绕过了检测。有同学会问,怎么得到这个nonce呢?通过跟代码,我发现在 dashboard 页面当中,下面这个功能里面就有我们需要的nonce,通过查看源代码,获取这个表单的input这样就绕过了检测。
<input type="hidden" id="_wpnonce" name="_wpnonce" value="xxx" />
绕过了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的一个问题。
当发送邮件的时候,可以携带附件,但是这个附件可以跨目录读取,导致了用户可以直接携带 wp-config.php
进行发送,实现敏感信息的泄露。
理清了漏洞触发逻辑,利用方式就简单了,在后台登录页面,直接引用 poc.js。
注意修改几个参数
meta_input[_mail][recipient]
参数为自己的收件箱。meta_input[_mail][attachments]
这个参数代表着想要获取的附件。然后可以在控制台里面引用,会发现新建了一个表单,然后在帖子里面正常引用这个表单,再页面中使用,并点击发送后,在自己的收件箱当中收到 wp-config.php
的附件。
在注册 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.' ) ); }
这个判断才会生效,导致权限认证失败。