WordPress在5.2.3以及之前版本,存在着一处未授权页面查看漏洞,攻击者可以在未授权的情况下,查看所有私密页面或是已经删除至回收站的页面
这个漏洞,请注意我的描述用词,是私密页面查看,而非私密文章查看,这点很关键,在wordpress中,POST指的是文章,Page指的是页面,这两个不是同一个概念,如下图
页面:
用户可以单独建立一个固定页面,可以作为留言板,或者通知的单页面,发布之后是固定的网址。页面并不能被分类、亦不能拥有标签,但是它们可以有层级关系。可将页面附属在另一个页面之下
文章:
文章可以通过标签实现相关文章的链接,可以放评论和评论框来实现与用户的互动,页面没有。文章有栏目可以归档,还有标签,页面没有。编辑文章时可选不同的形式,页面没有。
利用这个漏洞,攻击者并不能查看未发布的文章,只能有一定几率查看私密或已以至回收站的页面
我们将自上而下,从wordpress入口到漏洞触发点,来分析该漏洞
首先来看位于\wp-includes\class-wp.php 中的WP类,该类为WordPress环境设置类
在WP类中,存在\$public_query_vars数组,该数组用来定义公共查询变量,如下图
在WP类中,存在main方法,该方法用来设置WordPress环境所需的所有变量,如下图
Wordpress启动时,会调用WP类中的main方法,进行环境遍历赋值,位于上图main方法737行处,可见调用parse_request方法
parse_request方法的作用是,解析请求(GET/POST)以找到正确的WordPress查询,根据请求设置查询变量。如下图
该方法中存在一处foreach循环,遍历WP类中定义的\$public_query_vars数组值,如下图
该处循环的作用,是寻找\$public_query_vars数组中的值,是否存在于GET/POST请求的参数中,如过在请求的参数中找到,就将其参数键与值赋值到WP环境变量中去,\$public_query_vars数组见上文
例如有如下payload请求
http://127.0.0.1/wordpress/?static=0&order=asc&kumamon=test
\$public_query_vars数组中存在”order”与” static”
而GET请求的参数中也存在这两个参数,于是程序会将GET中order与static的值赋值给\$this->query_vars[‘order’]与\$this->query_vars[‘static],如下图
\$public_query_vars数组中并无”kumamon”,因此GET请求中的kumamon变量不做处理
Wordpress的环境变量机制了解完毕后,接下来看下漏洞触发点
首先来看下
\wp-includes\class-wp-query.php中的parse_query方法
该方法也是在wordpress启动时入口处被一系列的调用加载进来的,执行顺序位于parse_request方法之后,也就是环境变量赋值之后
我们重点关注下\$qv变量,如下图红框处
该变量为\$this->query_vars引用而来的,而\$this->query_vars则是\$this->query_vars经过fill_query_vars方法处理之后的值
当我们的请求为http://127.0.0.1/wordpress/?static=0&order=asc
\$this->query_vars值如下,该值由parse_request方法得来
而fill_query_vars方法,是将其他并未从请求中传递与赋值的环境变量用空值赋值
我们这里仅仅通过请求赋值了static与order两个环境变量,因此\$this->query_vars值如下
\$qv为\$this->query_vars引用,因此其中值与上图一致
接下来,位于805行处,有如下if-else条件
由于我们通过GET请求传入static变量,已经将\$qv[‘static’]赋值为0,因此可以进入上图条件分支,使得\$this->is_page=true,
\$this->is_single=false
还记得之前所说,Page指的是页面吗?因此上文的这个if条件,是为了通过检查请求中是否有'static'、'pagename'、'page_id'值,来判断是否要进行页面(Page)处理,如果是,则将\$this->is_page设置为true
继续向下看,位于3043行处,存在如下if条件分支
此时\$this->is_page=true, \$this->is_single=false,所以if中(
\$this->is_single || \$this->is_page )处的值为true。
只要使得\$this->posts不为空,则可以进入此处if分支
\$this->posts值为如下sql语句的查询值
这里解释下,为什么sql语句WHERE中wp_posts.post_type = 'page' 且ORDER BY
wp_posts.post_date ASC
首先看下wp_posts.post_type = 'page'
由于上文设置了\$this->is_page=true,因此进入如下条件分支
此处设置了查询条件为wp_posts.post_type =
'page'。显然,\$this->is_page=true,是要在库中查找page类型的发布
ORDER BY wp_posts.post_date
ASC的原因是由于我们GET请求中传入的order参数为ASC,其order为环境变量,在这里被直接拿来拼接sql语句了
在了解了wordpress此时的查询语句,我们对照后台数据库中wp_posts表的内容分析下
wp_posts表中存储了wordpress所有发布的内容,并同过post_type对其进行类型区分
Post_type 为post的,代表其为文章(POST);而page,代表这是一个页面
因此我们的\$this->is_page=true,使得程序从该中查询所有page类型的发布内容,也就是说,把所有的页面都提取出来了
注意这里:这条查询,把所有的page都查询出来,不管其状态是发布(publish)或是private(私密)甚至是回收站(trash)。并且通过发布时间升序排列,至于需要设置升序排列的原因,后文会介绍。
继续回到漏洞点
此时我们已经搞清楚\$this->posts是什么了,如上文所说,\$this->posts存放着从wp_posts表中取出的所有页面(Page),并且通过时间顺序升序排列
这时候\$this->post[0]即为存放在数据库中最早发布的那篇页面(Page),由于我的示范页面没有删,所以这里的\$this->post[0]就是那篇示范页面
而\$this->post[2]就是我们的私密的页面,如下图,可见post_status为private
为什么要用ASC将我们最早发布的页面放在\$this->post[0]位置呢?原因如下
程序会检查\$this->post[0]的发布状态,如上图前两个红框,并判断\$this->post[0]的发布状态是否是public,若\$this->post[0]的发布状态不为public,则接下来进行登陆与身份验证
因此通过ASC升序排列,尽可能的把最早发布的页面排在\$this->post[0],这个\$this->post[0]大概率是wordpress示例页面或者是网站自行添加的说明页面,其状态大概率是public,因此通过这个技巧绕过了后续登陆校验的环节,直接把所有页面显示出来
我们新建一个私密测试页面,如下图
建立一个回收站页面测试,并把它丢到回收站,如下图
通过我们的payload,可见私密以及回收站的Page都可被查看
若我们没有设置order=asc,则\$this->post[0]为我最后丢掉回收站里的Page(默认是用发布时间降序排序,而丢到回收站的那篇是我测试时最后新建的),它的状态是trash而非public,因此wordpress触发登陆校验,如下图
漏洞修复其实很容易理解:
开发者把\$public_query_vars数组中的static给删了,这样就算请求中传入static的值,也会被忽略,记得上文http://127.0.0.1/wordpress/?static=0&order=asc&kumamon=test 中的kumamon参数吗?
其次,开发者把’’ !=\$qv[‘static’]这个条件也删除了,这样的话,只能通过pagename或者page_id查询单条page了,然而单条page在显示时,是需要验证其状态的,非public的单条page是不予显示的