Share on social
April 2, 2025
Lorem ipsum
This software is critical, as it houses IT support tickets often containing credentials or internal documentation. When we first noticed the presence of this software on our customers'
Halo ITSM's code base does not do a good job at enforcing security controls, with ORM's not being consistently used and several areas of the code base being vulnerable to SQL injection. During our audit, we discovered a single
An interesting pattern we noticed in the pre-authentication attack surface was that several locations could have been vulnerable to SQL injection, only being prevented by using strongly typed objects enforcing the integer type. The one that we discovered pre-authentication was the only entry point for SQL with a loose type, where an assumed integer value could be transmitted as a string, which was later concatenated into a SQL query.
As we mentioned earlier, there were a lot of close calls that were prevented by strongly typed objects. An example of what we're talking about can be found below, found inside NetHelpDesk.API/Controllers/NotifyController.cs:

We can see that the input for this controller is taken from the request body, and is typed to a Device42Webhook. This object is typed like so:
namespace HaloClassLibrary.Models;
public class Device42Webhook
{
public string category { get; set; }
public string user { get; set; }
public string action { get; set; }
public Device42WebhookData data { get; set; }
public object resourceObject { get; set; }
}
Where Device42WebhookData is typed like so:
namespace HaloClassLibrary.Models;
public class Device42WebhookData
{
public int? id { get; set; }
public int? type_id { get; set; }
public string name { get; set; }
public string notes { get; set; }
}
Unfortunately, the data.id value is typed as an integer, which means that the following code is not vulnerable to SQL injection, despite the concatenation of a user input value inside a SQL query:
if (await _CommonFunctions.getOneFieldInt(this._context, "area", "aarea", "where adevice42id=" + device42Webhook.data.id.ToString(), -1, "", new bool?(false), null, "") > 0)
As shown earlier in this blog post, many potential injection points are prevented due to typing despite SQL queries being constructed with user input via concatenation.
There was a single injection point discovered in the pre-authentication attack surface that did not follow this paradigm, the PostNotify controller located in NetHelpDesk.API/Controllers/NotifyController.cs.
When following the logic of the controller, we start with its definition:
[HttpPost]
public async Task<IActionResult> PostNotify([FromBody] Dictionary<string, object> obj)
It's important to note that this controller has no decorators that enforce authentication, and most importantly, this controller takes in a dictionary object that is not typed.
The fact that the controller is taking in an untyped object is essential for exploiting this issue, as many other pre-authentication controllers that could have led to SQL injection often had objects with strict types, where integer typing would prevent SQL injection from being possible.
Following the logic of this controller:
if (obj.ContainsKey("publisherId") && obj["publisherId"].ToString() == "tfs")
{
return await this.PostAzureDevOps(JsonConvert.DeserializeObject<DevOpsWebhook>(JsonConvert.SerializeObject(obj)), false, true, false);
}
if (obj.ContainsKey("webhookEvent") && (obj["webhookEvent"].ToString() == "jira:issue_updated" || obj["webhookEvent"].ToString() == "comment_created" || obj["webhookEvent"].ToString() == "issue_commented" || obj["webhookEvent"].ToString() == "jira:issue_created"))
{
return await this.PostJira(obj);
}
if (obj.ContainsKey("integration") && obj["integration"].ToString() == "PagerDuty")
{
int? rpagerdutywebhooktype = controlobj.rpagerdutywebhooktype;
int num = 0;
if ((rpagerdutywebhooktype.GetValueOrDefault() == num) & (rpagerdutywebhooktype != null))
{
return await this.PostPagerDutyLegacy(obj, controlobj);
}
}
if (obj.ContainsKey("sessionid") && obj.ContainsKey("tracking0"))
{
return await this.PostLogMeIn(obj, controlobj, list);
}
To reach our sink, we need to provide a JSON object that contains sessionid and tracking0 - the SQL injection vulnerability exists in the PostLogMeIn function:
private async Task<IActionResult> PostLogMeIn(Dictionary<string, object> obj, Control controlobj, List<ModuleSetup> modules)
... omitted ...
if (obj.ContainsKey("lastactiontime") && !string.IsNullOrWhiteSpace(obj["lastactiontime"].ToString()))
{
int num = await _CommonFunctions.getOneFieldInt(this._context, "uname", "unum", "where ulogmeinid=" + obj["techid"].ToString(), -1, "", new bool?(false), null, "");
if (num > 0)
{
DateTime startdate;
DateTime.TryParse(obj["pickuptime"].ToString(), out startdate);
DateTime enddate;
DateTime.TryParse(obj["lastactiontime"].ToString(), out enddate);
RemoteSessionData remoteSessionData = new RemoteSessionData();
remoteSessionData.rsdthirdpartyid = obj["sessionid"].ToString();
remoteSessionData.rsdunum = new int?(num);
RemoteSessionData remoteSessionData2 = remoteSessionData;
remoteSessionData2.rsduname = await _CommonFunctions.getOneFieldString(this._context, "uname", "uname", "where unum=" + num.ToString());
We need to provide lastactiontime in our JSON request, to finally reach the SQLi sink here:
"where ulogmeinid=" + obj["techid"].ToString()
This calls into _CommonFunctions.getOneFieldInt where the fourth argument allows for arbitrary SQL to be executed if there is user-controllable input within it:
getOneFieldInt(ApplicationDbContext _context, string tablename, string fieldname, string wheresql = "", int ifnullvalue = -1, string orderbysql = "", bool? setresulttomax = false, DateTime? dateparam1 = null, string withSql = "")
The proof-of-concept has temporarily been removed due to a request from Halo PSA.
As with all bugs, we tried to find variants of this issue to see if this mistake had been made again elsewhere. It was surprising to see that no other locations in the code base followed the same pattern. We utilized regexes and a custom Semgrep rule to ensure that similar vulnerabilities did not exist.
Below, you can find some of the regex searches we did across the codebase and their corresponding results:

Halo ITSM has a really large attack surface, with many code patterns that can lead to serious vulnerabilities. Many potential improvements can be made to this codebase, especially with how SQL queries are constructed. This research showed how code hygiene and strict typing could have prevented a pre-authentication entry point.
From our analysis of this code base, even though the vendor has patched the issue we highlighted, there are deeper issues with the code base, especially in its post-authentication attack surface.
As always, customers of our Attack Surface Management platform were the first to know when this vulnerability affected them. We continue to perform original security research to inform our customers about zero-day vulnerabilities in their attack surface.
Searchlight Cyber's ASM solution, Assetnote, provides industry-leading attack surface management and adversarial exposure validation solutions, helping organizations identify and remediate security vulnerabilities before they can be exploited. Customers receive security alerts and recommended mitigations simultaneously with any disclosures made to third-party vendors. Visit our attack surface management page to learn more about our platform and the research we do.