Wordpress4.2.3提权与SQL注入漏洞(CVE-2015-5623)分析
这是这几天一直关注的漏洞了,wordpress上个礼拜发布的4.2.4版本,其中提到修补了可能存在的SQL漏洞和多个XSS。
Check point也很快发出了分析,我也来分析与复现一下最新的这个漏洞。
0x01 GP混用造成的越权漏洞
首先,说明一下背景。wordpress中用户权限分为订阅者、投稿者、作者、编辑和管理员。权限最低的是订阅者,订阅者只有订阅文章的权限,wordpress开启注册后默认注册的用户就是订阅者。国内很多知名网站,如Freebuf,用户注册后身份即为“订阅者”。
我们先看到一个提权漏洞,通过这个提权漏洞,我们作为一个订阅者,可以越权在数据库里插入一篇文章。
Wordpress检查用户权限是调用current_user_can函数,我们看到这个函数:
调用的has_cap方法,跟进
再次跟进map_meta_cap函数:
可以见到,这个函数是真正检查权限的。出错误的代码在检查’edit_post’和’edit_page’的部分:
可见,这里当$post不存在的时候,直接break出switch逻辑了,后面所有检查的代码都没有执行。
$post是要编辑的文章的ID,也就是说,如果我要编辑一篇不存在的文章,这里不检查权限直接返回。
正常情况下是没有问题的,因为不存在的文章也没有编辑一说了。
我们再看到后台编辑文章的部分:/wp-admin/post.php
这里首先获取$_GET[‘post’],找不到才获取$_POST[‘post_ID’],也就是可以说此时的$post_ID是来自GET的。
但我们后面调用current_user_can函数时传入的post_ID却是来自POST的:
这里就是一个逻辑问题,当我们在GET参数中传入正确的postid(这样在get_post的时候不会产生错误),而在POST参数中传入一个不存在的postid,那么就能够绕过检查edit_post权限的步骤。
但是这个逻辑错误暂时不能造成严重的危害,因为实际上编辑文章的代码在edit_post函数中,而这个函数取的post_ID来自$_POST。
0x02获取_wpnonce绕过CSRF防御
wordpress对于CSRF漏洞的防御措施是使用_wpnonce(也就是token),而且它的token很严格,不同的操作有不同的token。
比如我们这里,如果想调用edit_post函数,需要经过以下逻辑:
check_admin_referer就是检查_wpnonce的函数,当$post_type==’postajaxpost’的时候,此时_wpnonce的名字就是“add-postajaxpost”。
那么怎么获取名字为”add-postajaxpost”的_wpnonce呢?
看到上面一点的位置:
有个post-quickdraft-save操作。这个操作是用来临时储存草稿的,只要用户访问这个操作,就会在数据库post表中插入一个status为auto-draft的新文章。
如上图画出来的步骤,因为我们不知道名字为”add-post”的_wpnonce,所以进入到wp_dashboard_quick_press函数,跟进:
见上图,很幸运的是,在这个函数中wordpress居然自己把此时的_wpnonce输出在表单里了。
所以,只要我们访问一次post-quickdraft-save,就可以获得add-post的_wpnonce,从而绕过check_admin_referer函数。
0x03 竞争漏洞导致的逻辑漏洞
这一节实际上是这个提权洞的真正核心,在我们拿到_wpnonce后,进入edit_post函数。
我们目的是去update一篇文章,但刚才0x01中说到,如果要绕过权限检查的函数,需要传入一个“不存在”的文章id。那么即使可以执行update,我们也不可能修改已经存在的文章呀?
这里实际上涉及到一个由竞争造成的逻辑漏洞。看到edit_post函数代码:
上面两个图应该很直观了。在0x01中说到的current_user_can被绕过以后,到最终执行update语句中间,这一段代码的执行时间是真空的。
比如我们传入的tax_input=1,2,3,4…10000,那么实际上那条查询语句就要执行10000次,这是需要执行很长时间的。(在我自己的虚拟机上测试,执行10000次这条语句,大概需要5~10秒左右)
那么假设在这段时间内,有新插入的文章,那么我们之前那个“不存在”的id,不就可能可以存在了吗(只需要把id设置为最新一篇文章id+1)?
但有个问题是,我们怎么在这段时间内插入一篇新的文章?因为在0x02中为了获取_wpnonce,已经执行过post-quickdraft-save了。执行post-quickdraft-save可以在数据库插入一篇status为auto-draft的文章,但每个用户最多只会插入一篇文章。
在check-point的原文中,它提到的方法是,等待一个星期,wordpress会自动将这篇文章删除,而_wpnonce会多保留一天,这样在这天我们再次执行post-quickdraft-save又可以插入一篇文章了。
我自己想了一下,其实没必要这么麻烦。如果我们能够再注册一个身份为订阅者的账号,就可以再插入一篇文章了,所以我的POC是不需要等待一个礼拜的。
这三个漏洞组合起来,造成了一个提权漏洞。针对第一篇文章描述的提权漏洞,我写了一个EXP,执行后订阅者就可以在垃圾桶内插入一篇文章:
访问文章编辑页面可以看到这篇文章:
0x04 untrash文章时造成的SQL注入漏洞
那么,仅仅是一个这样的提权漏洞,实际上没有太大意义。Check-point第二篇文章里提到了一个因为这个提权漏洞导致的SQL注入。
先说这个注入的原理。
/wp-includes/post.php
如上图。Wordpress很多地方执行SQL语句使用的预编译,但仅限于直接接受用户输入的地方。而上图中明显是一个二次操作,先用get_post_meta函数从数据库中取出meta,之后以字符串拼接的方式插入SQL语句。
这个地方造成一个二次注入。
我们来看看第一次是如何入库的。首先wp_trash_post是将文章删除的方法,其中删除文章后又调用wp_trash_post_comments将文章下的评论也删除了:
跟进wp_trash_post_comments函数:
如上图,可以看到这个“comment_approved”其实也是从数据库中取出来的。所以这个注入我称之为“三次注入”。
那么我再继续跟进,看看最早的comment_approved是从哪来的。
实际上看到图中的SQL语句就大概知道了,这个comment_approved是comments(评论)表的一个字段,我分别看了新增评论、修改评论两个函数,发现修改评论的函数(edit_comment)中,有涉及到这个字段:
所以,这一连串操作最后造成的结果就是一个SQL注入漏洞。
总结一下1234,整个利用过程如下:
利用快速草稿插入文章->越权编辑文章->插入评论->修改评论(恶意数据入库)->删除文章(恶意数据进入另一个库)->反删除文章(恶意数据被取出,直接插入SQL语句导致注入)
0x05 原文隐藏的部分与真实利用过程的研究
这里不得不提到check-point的原文,原文的第二篇全文只字未提wordpress的token也就是_wpnonce,但wordpress后台几乎所有操作都需要特定的_wpnonce。在第一步中我们通过一处泄露点获取了“add-postajaxpost”的_wpnonce,但实际上后面的每一步(增加、编辑评论、trash文章、untrash文章)都需要不同的_wpnonce,那么这些_wpnonce我们怎么获得?
经过我的分析,最后实在找不到在订阅者权限下怎么获得“增加评论”和“反删除文章”的_wpnonce,而“修改评论”、“删除文章”的_wpnonce倒是可以在后台找到。
另外,虽然前台也可以增加评论,但前台增加评论会检查所属文章是否是草稿、状态是否是public或private,我们没法给这篇文章以及其派生的预览文章增加评论。
所以我把基础账号的权限提升一下,提升到可以发表文章与编辑文章的作者权限,再来对注入漏洞进行复现。
首先发表一篇文章,并在该文下回复一条评论:
我们再来修改这条评论:
在文章编辑页面找到trash的_wpnonce,将该评论所属的文章丢入垃圾箱:
再找到反删除的_wpnonce,从垃圾箱里反删除这篇文章:
查看SQL执行记录,发现已经注入成功,引入单引号:
最后,我来总结一下这个漏洞。
这个漏洞有两个核心点,一是利用一个竞争条件逻辑错误,造成的一个越权漏洞;二是利用一个三次操作,导致最后的SQL注入漏洞。
这两个核心技术点都是很有代表性的,通篇学习下来,不得不佩服洞主的思路和对wordpress的研究深度。
但我也很遗憾,没能分析出在最低权限下怎样去注入,主要还是_wpnonce的获取导致漏洞利用上出现了一些问题。
另外,该SQL注入有一个不得不说的鸡肋点,在污染数据第一次入库的时候,数据库中保存这个数据的字段只有20字符长度,所以导致最后我们的注入点是一个“存在长度限制”的注入点,对于这个长度限制的利用,我也暂时没有想出更好的方法。
虽然存在长度限制,但因为注入点出现在update语句的评论表中,所以通过这个漏洞,可以将一整个站的评论全部置为0,对于像Freebuf这类社交性质的网站来说危害还是巨大的。
所以,我对这个SQL注入的评价是:利用上(可能)比较鸡肋,但思路是绝对一流的,值得学习。
本文专属二维码,扫一扫还能分享朋友圈