<p>WordPress采用了一种功能丰富、易于扩展的角色和能力模型,其中每个用户都被指定一种角色,从权限最低的订阅者到有无限权力的超级管理员。</p><p>我们知道,即使订阅者也是有权访问WordPress管理员控制面板的,该面板位于/admin目录。相对于管理员而言,订阅者可以使用的面板选项极为有限,因为会受到相应权限的限制。</p><p>在默认情况下,订阅者只有“read_page”和“read_post”权限,可以读取文章和网页。 但是,对于管理员来说,他具有所有的权限,能够创建/编辑文章,上传文件并管理配置等。</p><p>每当用户试图利用current_user_can()函数完成一项动作时,WordPress都会检查他们的备案的权限。</p><p>函数current_user_can()在收到所请求的能力后,会将其映射为用户完成某一动作所需的实际权限。 为此,它需要以相应的能力作为参数来调用map_meta_cap()函数,该函数将返回一个数组,其中包含具备这种能力所需的实际权限。</p><p>然后,current_user_can()遍历数组,同时检查用户是否具备这些权限。</p><p>作为攻击者,我们首先假定只具有订阅者角色的权限。一般情况下,WordPress网站都允许用户免费注册,新用户的默认角色就是订阅者。 作为订阅者,我们只能够读取页面和文章,但是作为攻击者,我们想要做的事情肯定要比这些有趣的多。</p><p>为了了解能力是如何映射为实际权限的,我们不妨先来考察一下map_meta_cap()中的相关代码:</p><p><img alt="t01783b7ff32d91773a.png" src="https://images.seebug.org/contribute/3d0c4f9d-3c7b-4267-b237-c3be7cb86873-t01783b7ff32d91773a.png" data-image-size="571,458"><br></p><p>就像我们看到的,对于edit_post/edit_page能力,需要对相应文章进行必要的检查,即查看我们输入的文章ID是否存在(相应代码已经高亮显示)。 上述代码中,最有趣的部分是用来处理文章不存在的情况的,它没有给 $caps分配任何能力,并且数组返回值为空。谁曾想到,正由于没有设置任何能力,这个函数反而可以像不需要任何能力一样肆意而为,并且返回true。</p><p>这就意味着,在理论上我们可以通过一个无效的文章ID来绕过能力检查。为了利用这一点,我们需要这样的代码:一是它会调用这个检查函数,二是在调用之前无需验证该文章ID。 令人遗憾的是,真正符合要求的并不多见。不过,我们可以重点考察一下函数edit_post()。</p><p>这个函数可以用来更新文章。它首先会解析用于更新的输入内容,然后添加所需的元数据,插入格式,并且,所有这些工作都是在更新数据库中该文章对应数据项之前完成的。 该函数一旦解析完输入内容,就会调用wp_update_post()函数,该函数用于更新数据库中的相应的文章,以及其他次要事情,比如设置属性的缺省值。</p><p>尽管edit_post()没有验证我们的文章ID,但是wp_update_post()却会这么做。也就是说,即使我们绕过了第一次的权限检查,也逃不过第二次,因为我们的ID在数据库中根本就不存在。 虽然我们无法编辑数据库中实际的文章,却几乎通过了edit_post检查的所有代码。</p><p>下面,我们来考察代码中的一些有趣的东西:</p><p><img alt="2.png" src="https://images.seebug.org/contribute/c7bf727a-f341-4cc4-bba3-e2ec8ba79386-2.png" data-image-size="563,437"><br></p><p>可以看出,除了不能设置文章类型之外,我们可以控制$post_data中的任何值。 这就是说(尽管无法看到之前的代码),我们可以尝试更新文章的元数据或分类目录,从而进一步扩大我们的攻击面。</p><p>为此,我们首先需要能够达到这个函数。调用这个函数的代码有多处,但是它们不是检查其他能力,就是检查我们试图编辑的文章是否存在。 所以,我们唯一可以利用的只有管理页面post.php了,其中会用到下列代码:</p><p><img alt="3.png" src="https://images.seebug.org/contribute/8bd9289d-7fb2-4f7f-8b07-af611f0a2057-3.png" data-image-size="565,412"><br></p><p>看起来,这段代码对于我们来说非常合适,它既不检查能力,也不验证文章ID,这正是我们所需要的。然而,我们还面临着另外一个问题,即调用check_admin_referer()函数。 这个函数会对系统指定给我们的CSRF令牌进行检查。这个令牌使用的是服务器随机生成的哈希值和当前时间,因此,攻击者很难猜到,也很难修改它们。</p><p>为了访问edit_post(),我们首先需要得到这个令牌。观察代码我们发现,系统所预期的令牌格式为add-[POST_TYPE],其中POST_TYPE就是我们试图编辑的文章的类型。</p><p>我们试图通过一个不存在的文章ID绕过能力检查,来创建一个令牌动作“add-”,而不存在的文章是没有文章类型的。幸运的是,这里的代码首先会通过GET来获取该文章ID,并且只有当检索值为空时才会从post字段检索文章ID。 就是说我们有机会发送2个文章ID:把有效的文章ID放在GET参数中,把无效的文章ID放到POST参数中,这样,我们就能过使用两个令牌字符串来绕过能力检查。</p><p>因为我们已经可以使用令牌字符串“add-post”,所以只需要找到可以访问的、能够生成这个令牌的代码即可。虽然这个任务听上去很简单,但是由于我们缺乏能力,这严重限制了我们所能访问的其他动作。 再说一遍,目前我们所需的只是这样一个位置而已,而它正好在wp_dashboard_quick_press()函数中找到了。 这段代码可以在其他行动中为我们生成一个有效的管理令牌。</p><p>当一个缺乏必要能力的用户试图保存新的草稿的时候,这个函数就会被调用,具体看下列代码:</p><p><img alt="4.png" src="https://images.seebug.org/contribute/d05a3797-3a0e-4885-aa72-25ba5823434b-4.png" data-image-size="565,305"><br></p><p>很明显,如果用户没有提供正确的令牌,或者缺乏edit_post能力,这个函数就会被触发,这样我们就会收到一个有效的管理令牌add-post。</p><p>有了这个令牌,我们就能够在edit_post函数中肆意而为了,即使我们提供的是一个无效的文章ID。</p><p>截至目前为止,我们已经能够在并不存在的文章中创建和添加不受保护的元数据,并选择分类目录及其他次要事情了。尽管这些看起来不错,但是对于攻击者的目标来说还相距甚远,我们希望真正能够编辑实际存在的文章,这要求能够访问受到edit_post能力检查保护的代码。</p><p>下面,我们重新审视用于能力检查的代码:</p><p><img alt="5.png" src="https://images.seebug.org/contribute/f1a8993e-8eb7-4ac5-914e-f8fa36d3e8c6-5.png" data-image-size="566,453"><br></p><p>请注意我们高亮显示的代码——如果用户是文章的作者,并且文章目前标记为删除,这是就不需要任何能力了。这简直就是通向编辑权限的阳光大道。 但是,我们如何才能变成文章的作者,并将其状态设置为“trash”呢?</p><p>第一个问题比较容易,只要创建一篇文章,我们自然就会变成它的作者了。为此,我们只要调用wp_dashboard_quick_press()函数即可,这与创建令牌没有多大区别。 除了创建管理令牌之外,这个函数还检查先前的自动生成的草稿是否为当前用户所建。如果没有检测到草稿,那么这个函数就会调用get_default_post_to_edit(),该函数含有下列代码:</p><p><img alt="6.png" src="https://images.seebug.org/contribute/89feb3ca-4216-43fe-bf6e-572cdae90a92-6.png" data-image-size="564,347"><br></p><p>可以看出,上面的代码会获取文章的标题、内容,并摘录请求参数,然后,如果得到允许,就会在数据库中创建文章。调用wp_insert_post()时,参数中没有包括文章作者,因此,系统会假设作者就是当前用户。 尽管变量$create_in_db默认值为false,但是在我们这里,wp_dashboard_quick_press()将其赋值为true,因此,会在数据库中创建一份自动生成的草稿。</p><p>现在,我们在数据库中拥有了自己的文章,即我们是该文章的作者,下面我们将其状态改为“trash”。 下面,我们需要应用一些黑魔法。</p><p>当我们进入edit_post()函数时,我们要编辑的文章ID必须不存在于DB中,否则能力检查失败并且脚本将停止执行,但当我们进入wp_update_post()函数(它用于修改数据库中的文章) 时,文章ID必须有效且存在于数据库中,否则改函数将返回错误消息“post does not exist”。我们怎样才能同时满足这两个相互矛盾的条件呢?</p><p>实际上,非常简单。 我们可以利用竞争条件。</p><p>当我们进入edit_post()函数时,我们使用下一篇文章的ID。因为这些ID会自动递增,所以只要给出上一篇文章的ID,我们可以轻松猜出下一篇的。 这时候,我们的ID并不存在于数据库之中,所以我们能够通过能力检查,不会出现任何问题。然后,在这个函数执行期间,我们直接发送另一个请求,像前面介绍的那样新建一个快速草稿,这样就能在数据库中创建ID了。 一旦edit_post()函数想调用wp_update_post()从数据库中取出文章,这种情况就会发生,代码会一直呆在那里,这是毋庸置疑的。</p><p>但是问题是,发送两个不同的请求(编辑文章请求,然后是创建文章请求)的具体方式,在这种情况下,我们必须精心调整时机才能使这个策略奏效。显然,我们必须延迟脚本的运行。</p><p>下面,我们再来看看edit_post()的相关代码:</p><p><img alt="7.png" src="https://images.seebug.org/contribute/d1c942d9-f277-4c09-b15d-93588c83a53f-7.png" data-image-size="566,505"><br></p><p>我们看到,通过用逗号分割字符串,(我们可控的)变量$terms被转换成了一个数组。然后,代码会遍历这个数组,将每个元素视为一个分类目录名称,并尝试通过SELECT语句从数据库中取出它。</p><p>虽然每个SELECT语句在管理有序、功能强大的服务器上只要几微秒就能执行完毕,但是别忘了,我们实际上要查询16MB(PHP对POST参数的硬性规定)的检索词,需要执行大约1.6亿次这样的查询,所以要耗费很多的时间。对于我本机上的一个空数据库,采用1GB的RAM和i7处理器,仅执行了10万次左右的查询就导致了30秒的延迟 。</p><p>这给我们利用竞争条件提供了一种既有确定性又强大的手段,但是,事情还没有结束:为了能够有把握地充分利用安装在世界各地的所有WordPress上的这个漏洞,我们还有两项任务必须完成。</p><p>对于调用edit_post()的代码来说,需要用到管理令牌;而为了取得管理令牌,我们又必须调用wp_dashboard_quick_press(),因此在这个过程中,我们实际上会创建一个快速草稿。正如前面所说,wp_dashboard_quick_press()只有在没有草稿的情况下才会新建一个,同时,当edit_post()被延迟时,为了创建文章我们需要再次调用同一个函数,就是说,前面所说的过程是不会发生的。</p><p>那么,在已经存在一个草稿的情况下,我们如何新建草稿呢? 实际上,由wp_dashboard_quick_press()创建的草稿与普通的草稿是不同的,它叫做快速草稿(QuickDraft),它具有自动保存功能。 就这点而论,草稿只是临时存放文章文本的一种方式,以防止数据突然丢失。由于这些草稿被视为临时内容,因此会在一周内自动删除。</p><p>也就是说,为了利用这个漏洞,我们需要整整等上一周时间,因为一周后草稿才被删除。幸运的是,令牌通常延续24小时,这使得我们能够在预期删除日期的前一天获得令牌,并在草稿被删除后使用它来进入edit_post()函数。</p><p>最后,通过令牌验证、权限验证、基本管理员验证、文章ID验证后,这时只要发送一个HTTP参数就能轻轻松松地把文章状态改为“trash”。真是谢天谢地。</p><p>将这些绕过验证的手段结合起来,我们就可以利用成打的bug、有缺陷的权限验证系统以及验证系统中出现的各种错误的假设来获得部分编辑权限。当然,这根本就算不上什么严重漏洞,不过故事才刚刚开始,最终你会发现一个SQLi和XSS,这些我们放到后续的文章介绍。</p><p>POC</p><p>取得管理令牌/创建文章</p><p><img alt="8b.png" src="https://images.seebug.org/contribute/beb3a985-2de0-4be3-9846-e7ecbde62e5d-8b.png" data-image-size="564,119"><br></p><p>竞争条件<br></p><p><img alt="9.png" src="https://images.seebug.org/contribute/85f24599-eac5-46b1-aa9d-a505ccce71aa-9.png" data-image-size="565,158"><br></p><p><br></p><p>参考:</p><p>1、<a href="http://bobao.360.cn/learning/detail/569.html" rel="nofollow">http://bobao.360.cn/learning/detail/569.html</a></p><p>2、<a href="http://blog.checkpoint.com/2015/08/04/wordpress-vulnerabilities-1/" rel="nofollow">http://blog.checkpoint.com/2015/08/04/wordpress-vulnerabilities-1/</a></p>
暂无评论