04. 11. 2025 William Calliari Contribution, Icinga Web 2

Reconstructing Protected or Hidden Custom Variables in Icinga DB Web

Recently Icinga DB Web had a new security release, fixing a vulnerability where protected or hidden custom variables could be inferred by any user with object visibility by abusing comparative filters on those hidden variables. This led to a 5.3/10 rated Information Disclosure Vulnerability, documented in CVE-2025-61789. In this blog post I’ll go through the discovery and the inner workings of the exploit.

Setting the Scene

Every company has their own specific needs when it comes to monitoring their environments. From specific metrics they’re interested in, to thresholds, workdays, team composition and shifts, and company structure, there’s a lot more that goes into effective monitoring than meets the eye.

Icinga’s approach to this complexity is to provide a framework to specify templates and custom data fields that lets you adapt the monitoring environment to your needs. It can be used to store thresholds, parameters for checks or notifications, or flags for searching objects. But most importantly it can be used to store passwords or API tokens needed for the check itself.

To make the monitoring results visible to those users who aren’t directly managing the infrastructure, Icinga Web 2 allows users to access only the monitoring results, or in this case the Icinga DB view and not Director with the configuration.

Icinga DB Web further allows you to hide specific custom variables, like the aforementioned passwords and API tokens. These can be hidden by giving the user a role that sets the restriction icingadb/protect/variables to give a list of patterns the user is not allowed to access, e.g. *pass*,api-token.

To execute the exploit, an attacker must have at least viewing permissions on the attacked host or service, and needs to know the name of the variable, which can’t be known in hidden variables, but can often still be inferred from the check or other properties of the object.

The Discovery

While I was investigating Icinga DB Web for integration into NetEye, I happened to be working on understanding these restrictions while at the same time helping a colleague look over a PR to support the customization of the search params.

That’s where I made the first connection. So purely out of curiosity for what might happen, I looked up one of the passwords for an object I had created to test the restriction and ran the following query against Icinga DB Web: host.name=test-host&host.vars.password=<password>.

To my surprise the object was actually returned correctly. Weird. So I tried changing the password and it disappeared. Okay, so apparently the protected variables are only protected AFTER the filters have already run, giving us access to some information about them.

If it was just an equality check that in and of itself isn’t the biggest of problems, a normal user wouldn’t know the password to run this query. And if you already know it, you don’t need the query any more.

The worst outcome of this behavior is the possibility of brute-forcing the password in Icinga DB Web instead of the service directly, in some cases circumventing the service’s brute-force protection. But we are only marginally improving our options, fundamentally working in the same time-complexity.

Improving Our Attack

So to actually be able to exploit this vulnerability, we need some way to improve the time complexity, getting more information for each request. But since we cannot see the actual password, the only state we can observe is either true or false, 1 or 0, depending on whether the object is still present. So is there no way to actually exploit this small oversight?

Well, I wouldn’t be writing this blog post if that were the case. A quick look into the UI of Icinga DB Web show that = is not the only operator we can apply to the variables. Other than equality, we can also check for greater or lesser than, i.e. the lexicographical ordering of the words.

For this example, let’s assume the password can be found in a dictionary. We all know that the words there are sorted A-Z. So if we wanted to find it as fast as possible, we could start asking: “does the password start with ‘'“. After we’ve asked that at most 26 times, we’re bound to have found the initial letter of the password, effectively reducing the amount of words we have to look for by 96%.

And it doesn’t stop there. Now we can do the same procedure again for the second letter of the password. Another 26 questions later, and we’ve removed another 96% of the remaining words, with only 0.15% of words remaining. Continuing this strategy, we can find the password in the dictionary in at most 26 times the number of characters in the password, going from exponential to linear.

We can decrease this even further if we simply ask “is the character the password starts with greater than ?”. Then we can start in the middle and with each request exclude half of the remaining letters. With that we need at most 5 queries to find a character, going from linear to log.

We can use the same strategy for Icinga DB Web. For that we take the known prefix (the part of the password that we already know) and add the character we want to test afterwards, looking for the host with the query host.name=test-host&host.vars.password=(known prefix)(character), and with that, perform a binary search for the next character.

This strategy lets us reduce the overall time complexity from O(A^n) to O(n * log2 A). As an example, if we had a strong random password with upper and lowercase letters, digits and special characters, then we have in total A=100 options per character, and the difference would look as follows: Randomly guessing a password with 16 characters would take you at most 100^16 = 10^32 tries (yes, this is not a typo).

Even with 10,000 requests per second it would take over 10^32 / 10^4 / 60 / 60 / 24 / 365 ≈ 3.17e20 years (or 24 billion times the age of the universe) to crack. On the other hand, with our improved strategy, we could reduce the amount of queries to at most 16 * ceil(log2(100)) = 16 * 7 = 112 (since log2(100) ≈ 6.64).

So with the same 10,000 requests per second it would take just 0.01 seconds before the password is broken. In a realistic scenario, this will likely take much longer, since we need to perform our requests strictly sequentially, reducing the number of queries we can perform per second, but it’ll probably still take less than a minute. Afterwards, to know if you’ve actually found the whole password, you then simply make the query from the beginning using the equality operator.

Of course, passwords are just one example, but this attack can be used on any variable protected in this way, including API tokens, internal IDs or other sensitive information. The credentials could then also be used to pivot to other services to use the access to trigger exploits there.

The Aftermath

Once I had found the vulnerability and verified it actually worked, I immediately reported it to security@icinga.com. They investigated it and came to the same conclusion as me that this was a severe problem.

Their investigations also uncovered a similar problem in Icinga 2, where the fix was released with versions 2.15.1, 2.14.7 and 2.13.13, while the fix for Icinga DB Web was released in versions 1.1.4 and 1.2.3. All versions were released on October 16th 2025. I encourage all users who run versions lower than that to update their systems as soon as possible.

I’ve collaborated with the people from Icinga 2 on the fix for the vulnerability, and hope I can do that in the future as well to keep all Icinga users safe. This has been a fun experience and a valuable lesson in how seemingly small bugs can be abused to obtain additional data.

These Solutions are Engineered by Humans

Did you learn from this article? Perhaps you’re already familiar with some of the techniques above? If you find cybersecurity issues interesting, maybe you could start in a cybersecurity or similar position here at Würth Phoenix.

William Calliari

William Calliari

Author

William Calliari

Leave a Reply

Your email address will not be published. Required fields are marked *

Archive