# Wordpress4.2.3提权与SQL注入漏洞(CVE-2015-5623)分析

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-08-18T03:18:00Z
Canonical: https://wepostx.com/topics/984

> 乌云历史资料归档
>
> **原始作者：** 腾讯安全中心
> **原始编号：** superkieran-wooyundrops:715
> **原始发布时间：** 2015-08-18 11:18
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

这是这几天一直关注的漏洞了，wordpress上个礼拜发布的4.2.4版本，其中提到修补了可能存在的SQL漏洞和多个XSS。 Check point也很快发出了分析，我也来分析与复现一下最新的这个漏洞。

# 0x01 GP混用造成的越权漏洞

首先，说明一下背景。wordpress中用户权限分为订阅者、投稿者、作者、编辑和管理员。
权限最低的是订阅者，订阅者只有订阅文章的权限，wordpress开启注册后默认注册的用户就是订阅者。国内很多知名网站，如Freebuf，用户注册后身份即为“订阅者”。
我们先看到一个提权漏洞，通过这个提权漏洞，我们作为一个订阅者，可以越权在数据库里插入一篇文章。
Wordpress检查用户权限是调用current_user_can函数，我们看到这个函数：

![原文图片](/media/2016/06/364ec840aeac6f5b2bd10583d5e22674)

调用的has_cap方法，跟进

![原文图片](/media/2016/06/8fef528a76fb342336dfdd36ba6b7cd3)

再次跟进map_meta_cap函数：

![原文图片](/media/2016/06/7771caafa6f5076ca4e54f3a789b7b3c)

可以见到，这个函数是真正检查权限的。出错误的代码在检查’edit_post’和’edit_page’的部分：

![原文图片](/media/2016/06/037cc65bc7aeacbbf43d13c19097f3d4)

可见，这里当$post不存在的时候，直接break出switch逻辑了，后面所有检查的代码都没有执行。
$post是要编辑的文章的ID，也就是说，如果我要编辑一篇不存在的文章，这里不检查权限直接返回。
正常情况下是没有问题的，因为不存在的文章也没有编辑一说了。
我们再看到后台编辑文章的部分：/wp-admin/post.php

![原文图片](/media/2016/06/170ebf94e5daa046ae0530b15d90d43c)

这里首先获取$_GET[‘post’]，找不到才获取$_POST[‘post_ID’]，也就是可以说此时的$post_ID是来自GET的。
但我们后面调用current_user_can函数时传入的post_ID却是来自POST的：

![原文图片](/media/2016/06/26a16a11d64d7c6b5407903eb57fbba6)

这里就是一个逻辑问题，当我们在GET参数中传入正确的postid（这样在get_post的时候不会产生错误），而在POST参数中传入一个不存在的postid，那么就能够绕过检查edit_post权限的步骤。
但是这个逻辑错误暂时不能造成严重的危害，因为实际上编辑文章的代码在edit_post函数中，而这个函数取的post_ID来自$_POST。

# 0x02获取_wpnonce绕过CSRF防御

wordpress对于CSRF漏洞的防御措施是使用_wpnonce（也就是token），而且它的token很严格，不同的操作有不同的token。
比如我们这里，如果想调用edit_post函数，需要经过以下逻辑：

![原文图片](/media/2016/06/99f420eaaf1efc06b746d925f16f9bda)

check_admin_referer就是检查_wpnonce的函数，当$post_type==’postajaxpost’的时候，此时_wpnonce的名字就是“add-postajaxpost”。
那么怎么获取名字为”add-postajaxpost”的_wpnonce呢？
看到上面一点的位置：

![原文图片](/media/2016/06/c66251dd0a50fa61ec36c8dbe4541bfe)

有个post-quickdraft-save操作。这个操作是用来临时储存草稿的，只要用户访问这个操作，就会在数据库post表中插入一个status为auto-draft的新文章。
如上图画出来的步骤，因为我们不知道名字为”add-post”的_wpnonce，所以进入到wp_dashboard_quick_press函数，跟进：

![原文图片](/media/2016/06/1472f0208da89fb62b18abf21b1a24de)

见上图，很幸运的是，在这个函数中wordpress居然自己把此时的_wpnonce输出在表单里了。
所以，只要我们访问一次post-quickdraft-save，就可以获得add-post的_wpnonce，从而绕过check_admin_referer函数。

# 0x03 竞争漏洞导致的逻辑漏洞

这一节实际上是这个提权洞的真正核心，在我们拿到_wpnonce后，进入edit_post函数。
我们目的是去update一篇文章，但刚才0x01中说到，如果要绕过权限检查的函数，需要传入一个“不存在”的文章id。那么即使可以执行update，我们也不可能修改已经存在的文章呀？
这里实际上涉及到一个由竞争造成的逻辑漏洞。看到edit_post函数代码：

![原文图片](/media/2016/06/43200a81e88a95f4e983736f5eb919a7)

上面两个图应该很直观了。在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，执行后订阅者就可以在垃圾桶内插入一篇文章：

![原文图片](/media/2016/06/02a8a1db6c8c8f9e9b6b59afa54c82fc)

访问文章编辑页面可以看到这篇文章：

![原文图片](/media/2016/06/f274bbc483f322c72b3fc1e80cc5598e)

# 0x04 untrash文章时造成的SQL注入漏洞

那么，仅仅是一个这样的提权漏洞，实际上没有太大意义。Check-point第二篇文章里提到了一个因为这个提权漏洞导致的SQL注入。
先说这个注入的原理。

```text
/wp-includes/post.php
```

![原文图片](/media/2016/06/45d411ad3f8ef1405afba2f0fda4eb86)

如上图。Wordpress很多地方执行SQL语句使用的预编译，但仅限于直接接受用户输入的地方。而上图中明显是一个二次操作，先用get_post_meta函数从数据库中取出meta，之后以字符串拼接的方式插入SQL语句。
这个地方造成一个二次注入。
我们来看看第一次是如何入库的。首先wp_trash_post是将文章删除的方法，其中删除文章后又调用wp_trash_post_comments将文章下的评论也删除了：

![原文图片](/media/2016/06/ef1f9ee068df8c930bdba798e655e2e5)

跟进wp_trash_post_comments函数：

![原文图片](/media/2016/06/ac52e26786e15390a3093cf1e7e84b32)

如上图，可以看到这个“comment_approved”其实也是从数据库中取出来的。所以这个注入我称之为“三次注入”。
那么我再继续跟进，看看最早的comment_approved是从哪来的。
实际上看到图中的SQL语句就大概知道了，这个comment_approved是comments（评论）表的一个字段，我分别看了新增评论、修改评论两个函数，发现修改评论的函数（edit_comment）中，有涉及到这个字段：

![原文图片](/media/2016/06/c108b92d147703301d9f3efa5b7a9a0e)

所以，这一连串操作最后造成的结果就是一个SQL注入漏洞。
总结一下1234，整个利用过程如下：
利用快速草稿插入文章->越权编辑文章->插入评论->修改评论（恶意数据入库）->删除文章（恶意数据进入另一个库）->反删除文章（恶意数据被取出，直接插入SQL语句导致注入）

# 0x05 原文隐藏的部分与真实利用过程的研究

这里不得不提到check-point的原文，原文的第二篇全文只字未提wordpress的token也就是_wpnonce，但wordpress后台几乎所有操作都需要特定的_wpnonce。在第一步中我们通过一处泄露点获取了“add-postajaxpost”的_wpnonce，但实际上后面的每一步（增加、编辑评论、trash文章、untrash文章）都需要不同的_wpnonce，那么这些_wpnonce我们怎么获得？
经过我的分析，最后实在找不到在订阅者权限下怎么获得“增加评论”和“反删除文章”的_wpnonce，而“修改评论”、“删除文章”的_wpnonce倒是可以在后台找到。
另外，虽然前台也可以增加评论，但前台增加评论会检查所属文章是否是草稿、状态是否是public或private，我们没法给这篇文章以及其派生的预览文章增加评论。
所以我把基础账号的权限提升一下，提升到可以发表文章与编辑文章的作者权限，再来对注入漏洞进行复现。

![原文图片](/media/2016/06/e990f5b6e66924c051ef907e4d7a21c1)

首先发表一篇文章，并在该文下回复一条评论：

![原文图片](/media/2016/06/4b5c80fc9958bbd1b98f6c7a615573be)

我们再来修改这条评论：

![原文图片](/media/2016/06/9d04085b0788f04d916246dc2d0a0b35)

在文章编辑页面找到trash的_wpnonce，将该评论所属的文章丢入垃圾箱：

![原文图片](/media/2016/06/bf72e788d5fb6d27952a2c533a3a9a1d)

再找到反删除的_wpnonce，从垃圾箱里反删除这篇文章：

![原文图片](/media/2016/06/eb3ef6e1a094ea032e2bb1969e1e385d)

查看SQL执行记录，发现已经注入成功，引入单引号：

![原文图片](/media/2016/06/632ed7e122ab2b4cb67b52ff19ee6e5a)

最后，我来总结一下这个漏洞。
这个漏洞有两个核心点，一是利用一个竞争条件逻辑错误，造成的一个越权漏洞；二是利用一个三次操作，导致最后的SQL注入漏洞。
这两个核心技术点都是很有代表性的，通篇学习下来，不得不佩服洞主的思路和对wordpress的研究深度。
但我也很遗憾，没能分析出在最低权限下怎样去注入，主要还是_wpnonce的获取导致漏洞利用上出现了一些问题。
另外，该SQL注入有一个不得不说的鸡肋点，在污染数据第一次入库的时候，数据库中保存这个数据的字段只有20字符长度，所以导致最后我们的注入点是一个“存在长度限制”的注入点，对于这个长度限制的利用，我也暂时没有想出更好的方法。
虽然存在长度限制，但因为注入点出现在update语句的评论表中，所以通过这个漏洞，可以将一整个站的评论全部置为0，对于像Freebuf这类社交性质的网站来说危害还是巨大的。
所以，我对这个SQL注入的评价是：利用上（可能）比较鸡肋，但思路是绝对一流的，值得学习。

## Replies
