IT Security Research by Pierre

HomeAboutFeed

4 vulnerabilities in ibmsecurity

Product description

This repository contains Python code to manage IBM Security Appliances using their respective REST APIs. ISAM appliance has the most mature code.

From https://github.com/IBM-Security/ibmsecurity

Vulnerability Summary

Vulnerable versions: ibmsecurity < v2024.4.5.

The summary of the vulnerabilities is as follows:

  1. CVE-2024-31871 - Insecure communications 1/2
  2. CVE-2024-31872 - Insecure communications 2/2
  3. CVE-2024-31873 - Hardcoded passwords
  4. CVE-2024-31874 - Uninitialized variables

TL;DR: An attacker located on the network can MITM TLS connections to IBM Security Verify Access (ISVA) appliances, recover credentials and compromise the entire IBM Security Verify Access infrastructure. IBM Security Verify Access is a SSO solution mainly used by banks, Fortune 500 companies and governmental entities.

Miscellaneous notes:

The vulnerabilities were found in February 2023 and were communicated to IBM in March 2023. They ultimately were patched in April 2024 (after 13 months).

Communication with IBM was difficult. At first, IBM support had confirmed that they would patch the vulnerabilities found in the ibmsecurity library. Then, in April 2024, IBM advised that the only way to get security patches is to go Full-disclosure and open a github issue containing all technical details. Although this was unusual, I created a github issue, which was subsequently redacted by IBM.

Impacts

An attacker can compromise the entire authentication infrastructure based on IBM Security Verify Access by intercepting admin credentials on the network.

Recommendations

Details - Insecure communications 1/2

The ibmsecurity package has been partially audited as it provides the underlying Python APIs used to communicate with IBM Security Verify Access (ISVA).

Unfortunately, the security of the ibmsecurity package is very poor and, by default, all the SSL/TLS connections to the remote ISVA server are configured in an insecure way.

The latest version of the ibmsecurity library (ibmsecurity-2022.8.22.0) has been downloaded using pip in order for the source code to be reviewed:

kali% pip download ibmsecurity
Collecting ibmsecurity
Using cached ibmsecurity-2022.8.22.0-py3-none-any.whl (391 kB)
Collecting requests
Using cached requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting urllib3<1.27,>=1.21.1
Using cached urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Collecting certifi>=2017.4.17
Using cached certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting idna<4,>=2.5
Using cached idna-3.4-py3-none-any.whl (61 kB)
Saved ./ibmsecurity-2022.8.22.0-py3-none-any.whl
Saved ./requests-2.28.1-py3-none-any.whl
Saved ./certifi-2022.9.24-py3-none-any.whl
Saved ./charset_normalizer-2.1.1-py3-none-any.whl
Saved ./idna-3.4-py3-none-any.whl
Saved ./urllib3-1.26.12-py2.py3-none-any.whl
Successfully downloaded ibmsecurity requests certifi charset-normalizer idna urllib3
kali%

The invoke_* functions in the ibmsecurity library will use by default the _suppress_ssl_warning() method that will remove any security related to SSL/TLS.

For example, the method invoke_put() is defined in the file ibmsecurity/appliance/isamappliance.py, as shown below on line 402:

Content of ibmsecurity/appliance/isamappliance.py:

...
  3 from requests.packages.urllib3.exceptions import InsecureRequestWarning
...
 17 class ISAMAppliance(IBMAppliance):
...
 45     def _suppress_ssl_warning(self):
 46         # Disable https warning because of non-standard certs on appliance
 47         try:
 48             self.logger.debug("Suppressing SSL Warnings.")
 49             requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # [1] <- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
 50         except AttributeError:
 51             self.logger.warning("load requests.packages.urllib3.disable_warnings() failed")
...
402     def invoke_put(self, description, uri, data, ignore_error=False, requires_modules=None, requires_version=None,
403                    warnings=[], requires_model=None):
404         """ 
405         Send a PUT request to the LMI.
406         """ 
407 
408         self._log_request("PUT", uri, description)
409         response = self._invoke_request(self.session.put, description, uri,
410                                         ignore_error, data,
411                                         requires_modules=requires_modules, requires_version=requires_version,
412                                         requires_model=requires_model, warnings=warnings)
413         return response
...

The method _invoke_request() called on line 409 inside the invoke_put() method will disable any SSL/TLS security on line 334 by calling the method _suppress_ssl_warning() previously defined on line 45:

Content of ibmsecurity/appliance/isamappliance.py:

...
305     def _invoke_request(self, func, description, uri, ignore_error, data={}, requires_modules=None,
306                         requires_version=None, warnings=[], requires_model=None):
307         """
308         Send a request to the LMI.  This function is private and should not be
309         used directly.  The invoke_get/invoke_put/etc functions should be used instead.
310         """
...
334         self._suppress_ssl_warning() # <- insecure SSL/TLS connection to the remote ISVA instance
...
336         try:
337             if func == self.session.get or func == self.session.delete:
338             
339                 if data != {}:
340                     r = func(url=self._url(uri), data=json_data, verify=False, headers=headers)
341                 else:
342                     r = func(url=self._url(uri), verify=False, headers=headers)
343             else:
344                 r = func(url=self._url(uri), data=json_data,
345                          verify=False, headers=headers)
346 
347             if func != self.session.get:
348                 return_obj['changed'] = True  # Anything but GET should result in change
349 
350             self._process_response(return_obj=return_obj, http_response=r, ignore_error=ignore_error)
...

Execution flow: The invoke_put() method is defined on the line 402 of the ibmsecurity/appliance/isamappliance.py file. This method will then use the _invoke_request() method, defined on line 305. This _invoke_request() method will call the suppress_ssl_warning() method on line 334. The suppress_ssl_warning() method is defined on line 45: any security related to SSL/TLS will be then removed.

These methods defined in ibmsecurity/appliance/isamappliance.py are insecure:

These methods defined in ibmsecurity/appliance/isdsappliance.py are also insecure:

In the ibmsecurity/appliance/isdsappliance.py Python file, we can find again the same suppress_ssl_warning() method which is also used by other methods (e.g. invoke_post_files() as shown below):

  3 from requests.packages.urllib3.exceptions import InsecureRequestWarning
...
 17 class ISDSAppliance(IBMAppliance):
...
 39     def _suppress_ssl_warning(self):
 40         # Disable https warning because of non-standard certs on appliance
 41         try:
 42             self.logger.debug("Suppressing SSL Warnings.")
 43             requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
 44         except AttributeError:
 45             self.logger.warning("load requests.packages.urllib3.disable_warnings() failed")
...
135     def invoke_post_files(self, description, uri, fileinfo, data, ignore_error=False, requires_modules=None,
136                           requires_version=None, warnings=[], json_response=True):
...
166         self._suppress_ssl_warning()
167 
168         try:
169             r = requests.post(url=self._url(uri=uri), data=data, auth=(self.user.username, self.user.password),
170                               files=files, verify=False, headers=headers)
171             return_obj['changed'] = True  # POST of file would be a change
172             self._process_response(return_obj=return_obj, http_response=r, ignore_error=ignore_error)
...

These methods are used everywhere in the ibmsecurity library to communicate with the remote ISVA infrastructure. 1162 calls to insecure methods have been identified:

kali% rgrep invoke_ ibmsecurity
ibmsecurity/isds/server.py: return isdsAppliance.invoke_get("Retrieving Server Status", "/widgets/server")
ibmsecurity/isds/server.py: return isdsAppliance.invoke_post("Restarting the service " + serverID,
ibmsecurity/isds/server.py: return isdsAppliance.invoke_post("Restarting the service " + serverID,
ibmsecurity/isds/server.py: return isdsAppliance.invoke_post("Restarting the service " + serverID,
ibmsecurity/isds/server.py: return isdsAppliance.invoke_post("Restarting the service " + serverID,
ibmsecurity/isds/available_updates.py: return isdsAppliance.invoke_get("Retrieving available updates",
ibmsecurity/isds/available_updates.py: return isdsAppliance.invoke_get("Discover available updates",
ibmsecurity/isds/available_updates.py: return isdsAppliance.invoke_post_files(
ibmsecurity/isds/available_updates.py: ret_obj = isdsAppliance.invoke_post("Install Available Update",
ibmsecurity/isds/fixpack.py: return isdsAppliance.invoke_get("Retrieving fixpacks",
ibmsecurity/isds/fixpack.py: return isdsAppliance.invoke_post_files(
[...]
kali% rgrep invoke_ ibmsecurity | wc -l
1162
kali%

The ibmsecurity Python library massively uses insecure methods to communicate with the remote ISVA infrastructure, with 1162 calls to insecure functions.

Details - Insecure communications 2/2

The ibmsecurity package has been partially audited as it provides the underlying Python APIs used to communicate with IBM Security Verify Access (ISVA).

Unfortunately, the security of the ibmsecurity package is very poor, and, by default, all the SSL/TLS connections to the remote ISVA server are insecure due to the insecure option (Verify=false) used with the methods provided by the requests module (to send HTTPS requests to the remote ISVA infrastructure).

For example, the method invoke_post_snapshot_id() will use Verify=false in the HTTPS request on line 455 to disable any verification of the remote SSL certificate (in addition to the previous insecure _suppress_ssl_warning() method found in Insecure communications 1/2).

Content of ibmsecurity/appliance/isamappliance.py:

429     def invoke_post_snapshot_id(self, description, uri, data, ignore_error=False, requires_modules=None,
430                                 requires_version=None, warnings=[], requires_model=None):
431         """ 
432         Send a POST request to the LMI.  Snapshot id is part of the uri.
433         Requires different headers to normal post.
434         """ 
...
452         self._suppress_ssl_warning()
453
454         try:                                                       # VVVVVVVVVVVV
455             r = self.session.post(url=self._url(uri=uri), data=data, verify=False, headers=headers)
456             return_obj['changed'] = False  # POST of snapshot id would not be a change
457             self._process_response(return_obj=return_obj, http_response=r, ignore_error=ignore_error)
...

Similar vulnerabilities can be found everywhere in the Python sources.

For example, in the method invoke_request() inside ibmsecurity/appliance/isamappliance.py and invoke_get_file() inside ibmsecurity/appliance/isdsappliance.py:

Content of ibmsecurity/appliance/isamappliance.py (line 555):

...
518     def invoke_request(self, description, method, uri, filename=None, ignore_error=False, requires_modules=None,
519                        requires_version=None,
520                        warnings=[], requires_model=None, **kwargs):
...
551         self._suppress_ssl_warning()
...
555             r = self.session.request(method, url=self._url(uri), verify=False, **args)

Content of ibmsecurity/appliance/isdsappliance.py (line 254):

...
228     def invoke_get_file(self, description, uri, filename, no_headers=False, ignore_error=False, requires_modules=None,
229                         requires_version=None, warnings=[]):
230         """ 
231         Invoke a GET request and download the response data to a file
232         """ 
...
251         self._suppress_ssl_warning()
252 
253         try:
254             r = requests.get(url=self._url(uri=uri), auth=(self.user.username, self.user.password), verify=False,
255                              stream=True, headers=headers)

Vulnerable functions identified in ibmsecurity/appliance/isdsappliance.py:

Vulnerable functions identified in ibmsecurity/appliance/isamappliance.py:

The vulnerable library can also be found in Github (https://github.com/IBM-Security/ibmsecurity/blob/master/ibmsecurity/appliance/isamappliance.py#L187):

        try:
            if data_as_files is False:
                r = self.session.post(url=self._url(uri=uri), data=data, files=files, verify=False, headers=headers)
            else:
                r = self.session.post(url=self._url(uri=uri), files=files, verify=False, headers=headers)

The ibmsecurity Python library massively uses insecure methods to communicate with the remote ISVA infrastructure, with 1162 calls to insecure functions.

Details - Hardcoded passwords

It was observed that the ibmsecurity library contains hardcoded users and passwords:

Content of ibmsecurity/isam/web/reverse_proxy/federation_configuration.py:

...
7 def config(isamAppliance, instance_id, federation_id=None, federation_name=None, hostname='127.0.0.1',
  port='443', username='easuser',
8 password='passw0rd', reuse_certs=False, reuse_acls=False, check_mode=False, force=False):
...

Similar vulnerabilities can be found in the source codes.

For example, in the method invoke_request() inside ibmsecurity/appliance/isamappliance.py and invoke_get_file() inside ibmsecurity/appliance/isdsappliance.py, we can find hardcoded credentials:

Content of ibmsecurity/isam/web/reverse_proxy/oauth_configuration.py:

...
10 def config(isamAppliance, instance_id, hostname='127.0.0.1', port=443, username='easuser', password='passw0rd',
11 junction="/mga", reuse_certs=False, reuse_acls=False, api=False, browser=False, auth_register=None, fapi_compliant=None,
12 check_mode=False, force=False):
...

Content of ibmsecurity/isam/web/reverse_proxy/aac_configuration.py:

...
9 def config(isamAppliance, instance_id, hostname='127.0.0.1', port=443, username='easuser', password='passw0rd',
10 junction="/mga", reuse_certs=False, reuse_acls=False, check_mode=False, force=False):
...

Attacker can use hardcoded passwords to compromise installations.

Details - Uninitialized variables

It was observed that the ibmsecurity library uses variables before they are initialized in:

The json_data variable is used on line 106 but is only defined on line 108:

Content of ibmsecurity/isam/aac/attribute_matchers.py:

...
 98 def _check(isamAppliance, description, properties):
 99     """
100     Check and return True if update needed
101     """
102     update_required = False
103     ret_obj = get(isamAppliance, description)
104     if ret_obj['data'] == {}:
105         logger.warning("Attribute Matcher not found, returning no update required.")
106         return None, update_required, json_data
107     else:
108         json_data = {
109             "properties": properties,
110             "predefined": ret_obj['data']['predefined'],
111             "supportedDatatype": ret_obj['data']['supportedDatatype'],
112             "uri": ret_obj['data']['uri']
113         }
...

Content of ibmsecurity/isam/aac/risk_profiles.py:

...
152 def _check(isamAppliance, name, active, description, attributes, predefined):
153     """
154     Check and return True if update needed
155     """
156     update_required = False
157     ret_obj = get(isamAppliance, name)
158     if ret_obj['data'] == {}:
159         logger.warning("Risk Profile not found, returning no update required.")
160         return None, update_required, json_data
161     else:   
162         if ret_obj['data']['predefined'] is True:
163             logger.warning("Predefined Risk Profiles can NOT be updated, returning no update required.")
164             return ret_obj['data']['id'], update_required, {}
165         else:
166             json_data = {
167                 "name": name,
168                 "active": active,
169                 "predefined": predefined
170             }
171             if attributes is not None:
172                 json_data['attributes'] = attributes
...

The Python code can crash.

Vendor Response

IBM provided a security bulletin:

Security Bulletin: Multiple Security Vulnerabilities were found in Open Source libraries used to deploy IBM Security Verify Access Appliances (CVE-2024-31871, CVE-2024-31872, CVE-2024-31873, CVE-2024-31874):

Report Timeline

This security assessment was reported to IBM along with the security assessment of IBM Security Verify Access. Please find the complete timeline below:

Credits

These vulnerabilities were found by Pierre Barre aka Pierre Kim (@PierreKimSec).

References

https://pierrekim.github.io/blog/2024-11-01-ibmsecurity-4-vulnerabilities.html

https://pierrekim.github.io/advisories/2024-ibmsecurity.txt

https://pierrekim.github.io/blog/2024-11-01-ibm-security-verify-access-32-vulnerabilities.html

https://pierrekim.github.io/advisories/2024-ibm-security-verify-access.txt

https://www.ibm.com/support/pages/node/7147932

https://github.com/IBM-Security/ibmsecurity/issues/416

Disclaimer

This advisory is licensed under a Creative Commons Attribution Non-Commercial Share-Alike 3.0 License: http://creativecommons.org/licenses/by-nc-sa/3.0/

published on 2024-11-01 00:00:00 by Pierre Kim <pierre.kim.sec@gmail.com>