7 min read
Hacking Apple - SQL Injection to Remote Code Execution
Introduction
In our last blog post, we delved into the inner workings of Lucee and took a look at the source code of Masa/Mura CMS, and the vastness of the potential attack surface struck us. It became evident that investing time in understanding the code could pay off. After dedicating a week to our exploration, we stumbled upon several entry points for exploitation, including a critical SQL injection flaw that we were able to exploit within Apple's Book Travel portal.
In this blog post, we aim to share our insights and experiences, detailing how we identified the vulnerability sink, linked it back to its source, and leveraged the SQL injection to achieve Remote Code Execution (RCE).
Finding the sink
From playing around with the Masa/Mura CMS, we understood our attack surface - mainly the attack surface accessible on Apple's environment. Our primary focus was on JSON API, as it exposes some methods that are accessible within Apple's environment. Any potentially vulnerable sink we find should have its source in JSON API.
We deliberated on optimising our approach to streamline our source code review process. We explored the availability of static analyzers or CFM parsers capable of traversing through code while disregarding sanitizers.
For instance, this is how a safe parameterised SQL query is written via tag-based CFM:
cfml
1<cfquery>2select * from table where column=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.user_input#">3</cfquery>
And this is how an unsafe SQL query is written:
cfml
1<cfquery>2select * from table where column=#arguments.user_input#3</cfquery>
It would be great if we could parse and traverse through the code and only print cfquery
tags that have unsanitized input regardless of having the cfqueryparam
tag inside or not. We came across https://github.com/foundeo/cfmlparser which could let us do this.
Here's how we targeted SQL injection sink detection:
- Parse each CFM/CFC file.
- Go through each statement, select the statement if it's a tag and its name is
cfquery
. - Strip all tags (like cfqueryparam) inside the code block of cfquery and if it still has
arguments
in the codeblock then the input is not parameterized and the query is susceptible to an SQL injection, given no other validation is in place. - Print this query.
cfml
1<cfscript>2targetDirectory = "../mura-cms/";3files = DirectoryList(targetDirectory, true, "query");45for (file in files) {6if (FindNoCase(".cfc", file.name) or FindNoCase(".cfm", file.name)) {7fname = file.directory & "/" & file.name;8if (file.name != "dbUtility.cfc" && file.name != "configBean.cfc" && !FindNoCase("admin", file.directory) && !FindNoCase("dbUpdates", file.directory)) {9filez = new cfmlparser.File(fname);10statements = filez.getStatements();11info = [];12for (s in statements) {13if (s.isTag() && s.getName() == "cfquery" && FindNoCase("arguments", s.getStrippedInnerContent(true, true))) {14WriteOutput("Filename: <b>#fname#</b>");15WriteOutput("<br><br>" & s.getStrippedInnerContent(true, true));16WriteOutput("<br><br><br><br>");17}18}19}20}21}22</cfscript>
We started going through the result with a few things in mind, such as ignoring input like siteid
because JSON API validates it in advance.
One of the queries that had two other inputs was this:
Tracing sink to source
Looking at the function which had this query concluded that there's only one exploitable argument, that is, ContentHistID
. The argument columnid
is numeric and siteid
is validated by default.
cfml
1<cffunction name="getObjects" output="false">2<cfargument name="columnID" required="yes" type="numeric" >3<cfargument name="ContentHistID" required="yes" type="string" >4<cfargument name="siteID" required="yes" type="string" >56<cfset var rsObjects=""/>78<cfquery attributeCollection="#variables.configBean.getReadOnlyQRYAttrs(name='rsObjects')#">9select tcontentobjects.object,tcontentobjects.name,tcontentobjects.objectid, tcontentobjects.orderno, tcontentobjects.params, tplugindisplayobjects.configuratorInit from tcontentobjects10inner join tcontent On(11tcontentobjects.contenthistid=tcontent.contenthistid12and tcontentobjects.siteid=tcontent.siteid)13left join tplugindisplayobjects on (tcontentobjects.object='plugin'14and tcontentobjects.objectID=tplugindisplayobjects.objectID)15where tcontent.siteid='#arguments.siteid#'16and tcontent.contenthistid ='#arguments.contentHistID#'17and tcontentobjects.columnid=#arguments.columnID#18order by tcontentobjects.orderno19</cfquery>2021<cfreturn rsObjects>2223</cffunction>
The function getObjects
was called within the dspObjects
function in the core/mura/content/contentRendererUtility.cfc component.
The call stack was:
JSON API -> processAsyncObject -> object case: displayregion -> dspobjects() -> getobjects().
Triggering & Exploiting SQL injection
By default, Lucee escapes single quotes by adding a backslash before them when passed as input. This can be managed by using a backslash to escape one of the single quotes.
This should trigger the SQL injection:/_api/json/v1/default/?method=processAsyncObject&object=displayregion&contenthistid=x%5c'
However, it didn't. Upon revisiting the source code, we identified a crucial condition in the dspObjects
function. Before calling getObjects
, an if
condition must be satisfied: the isOnDisplay
property must be set to true in the Mura servlet event handler. Initially, we assumed that any property on the event handler could be set simply by passing the property name as a parameter, along with its value. This assumption was based on our debugging session within the codebase.
Our attempts to set the isOnDisplay
property in this manner were unsuccessful. It appears that somewhere in the code, this property is being overwritten.
After conducting some grep searches, we stumbled upon the standardSetIsOnDisplayHandler
function call within the processAsyncObjects
of the JSON API.
It appears that by simply passing the previewID
parameter with any value, we can set the previewID
property, which in turn will set the isOnDisplay
property to true.
/_api/json/v1/default/?method=processAsyncObject&object=displayregion&contenthistid=x%5c'&previewID=x
And it worked:
Since this was an error-based SQL injection, we could exploit it quite easily to achieve Remote Code Execution (RCE). Locally, we successfully performed RCE by following these steps:
- Reset an Admin user's password.
- Obtain the reset token and user ID via SQL injection.
- Use the password reset endpoint with exfiltrated info.
- Utilize plugin installation to upload CFM files.
However, on Apple's environment, we encountered only an Unhandled Exception error without any query-related information, turning this into a blind SQL injection. Fortunately, the token and user ID are UUIDs, making it relatively straightforward to exfiltrate them. With a bit of scripting, we were able to accomplish this task.
We promptly submitted our report to Apple, including Proof of Concept (PoC) demonstrating logging into an account while theoretically providing them with RCE details.
Detection via Nuclei
This SQL injection vulnerability can be identified by utilizing the below Nuclei template:
yaml
1id: CVE-2024-3264023info:4name: Mura/Masa CMS - SQL Injection5author: iamnoooob,rootxharsh,pdresearch6severity: critical7description: |8The Mura/Masa CMS is vulnerable to SQL Injection.9reference:10- https://blog.projectdiscovery.io/mura-masa-cms-pre-auth-sql-injection/11- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3264012impact: |13Successful exploitation could lead to unauthorized access to sensitive data.14remediation: |15Apply the vendor-supplied patch or update to a secure version.16metadata:17verified: true18max-request: 319vendor: masacms20product: masacms21shodan-query: 'Generator: Masa CMS'22tags: cve,cve2022,sqli,cms,masa,masacms2324http:25- raw:26- |27POST /index.cfm/_api/json/v1/default/?method=processAsyncObject HTTP/1.128Host: {{Hostname}}29Content-Type: application/x-www-form-urlencoded3031object=displayregion&contenthistid=x\'&previewid=13233matchers:34- type: dsl35dsl:36- 'status_code == 500'37- 'contains(header, "application/json")'38- 'contains_all(body, "Unhandled Exception")'39- 'contains_all(header,"cfid","cftoken")'40condition: and
We've also added template in nuclei-templates GitHub project.
Conclusion
In conclusion, our exploration of Masa/Mura CMS has been a rewarding journey, revealing critical vulnerabilities. The code review process begins by focusing on vulnerable SQL injection code patterns and then utilizing the CFM/CFC parser to search for specific patterns within the codebase, a similar approach to Semgrep. Once potential sinks were identified, we traced them back to the source, in this case, the JSON API of Mura/Masa CMS.
We responsibly disclosed these findings to Apple and the respective Masa and Mura CMS teams.
Apple's Response:
Apple responded and implemented a fix within 2 hours of the initial report, swiftly addressing the reported issue. As always, working with Apple has been a good collaboration.
Masa CMS:
Masa is an open-source fork of Mura CMS, they were quite transparent and released a new version of Masa CMS with fixes. The 7.4.6, 7.3.13 and 7.2.8 versions have the latest security patches including another critical pre-auth SQL injection which is assigned CVE (CVE-2024-32640).
Mura CMS:
Despite numerous attempts to reach out to the Mura team regarding these vulnerabilities, we received no response across multiple communication channels. With the 90-day standard deadline elapsed, we are now releasing this blog post detailing the reported vulnerability.
By leveraging Nuclei and actively engaging with the open-source community, or by becoming a part of the ProjectDiscovery Cloud Platform, companies can enhance their security measures, proactively address emerging threats, and establish a more secure digital landscape. Security represents a shared endeavor, and by collaborating, we can consistently adapt and confront the ever-evolving challenges posed by cyber threats.