Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12:Update
cobbler
v2-6-6-fix-rce.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File v2-6-6-fix-rce.patch of Package cobbler
From 02cdda598214d9e770fce1fcc8174e2fd15efd64 Mon Sep 17 00:00:00 2001 From: SchoolGuy <egotthold@suse.de> Date: Tue, 24 Aug 2021 11:32:37 +0200 Subject: [PATCH] Backport of the log poisoning and arbitrary read & write patch This should prevent injecting Cheetah or Jinja template language into a logmessage and reading or writing arbitrary files thorugh relative paths. --- cobbler/item.py | 10 ++----- cobbler/pxegen.py | 9 +++++- cobbler/remote.py | 76 ++++++++++++++++++++++++++++++++++------------- cobbler/utils.py | 34 +++++++++++++++++++++ 4 files changed, 101 insertions(+), 28 deletions(-) diff --git a/cobbler/item.py b/cobbler/item.py index 39693654..e056d595 100644 --- a/cobbler/item.py +++ b/cobbler/item.py @@ -19,14 +19,12 @@ from cexceptions import * from utils import _ import pprint import fnmatch -import re + class Item: TYPE_NAME = "generic" - _re_name = re.compile(r'[a-zA-Z0-9_\-.:+]*$') - def __init__(self,config,is_subobject=False): """ Constructor. Requires a back reference to the Config management object. @@ -144,10 +142,8 @@ class Item: def validate_name(self, name): """Validate name. Raises CX if the name if invalid""" - if not isinstance(name, basestring): - raise CX(_("name must be a string")) - if not self._re_name.match(name): - raise CX(_("invalid characters in name: '%s'" % name)) + if not utils.validate_obj_name(name): + raise CX(_("invalid characters in name or wrong type: '%s'" % name)) def set_name(self,name): """ diff --git a/cobbler/pxegen.py b/cobbler/pxegen.py index b241305d..81c8e6f9 100644 --- a/cobbler/pxegen.py +++ b/cobbler/pxegen.py @@ -1230,8 +1230,13 @@ class PXEGen: def generate_script(self,what,objname,script_name): if what == "profile": obj = self.api.find_profile(name=objname) - else: + elif what == "system": obj = self.api.find_system(name=objname) + else: + raise ValueError("\"what\" needs to be either \"profile\" or \"system\"!") + + if not utils.validate_autoinstall_script_name(script_name): + raise ValueError("\"script_name\" handed to generate_script was not valid!") if not obj: return "# %s named %s not found" % (what,objname) @@ -1259,6 +1264,8 @@ class PXEGen: scripts_path = "/var/lib/cobbler/scripts" template = os.path.normpath(os.path.join(scripts_path,script_name)) + if not template.startswith(scripts_path): + return "# script template \"%s\" could not be found in the script root" % script_name available_scripts = [] for root, folders, files in os.walk(scripts_path): diff --git a/cobbler/remote.py b/cobbler/remote.py index b5f46981..d255d33b 100644 --- a/cobbler/remote.py +++ b/cobbler/remote.py @@ -19,10 +19,11 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """ - +import keyword import sys, socket, time, os, errno, re, random, stat, string import base64 import SimpleXMLRPCServer +import tokenize from SocketServer import ThreadingMixIn import xmlrpclib import base64 @@ -46,6 +47,7 @@ import clogger import pxegen import utils #from utils import * # BAD! +from cobbler import item from utils import _ import configgen @@ -404,7 +406,7 @@ class CobblerXMLRPCInterface: else: return self.token_cache[token][1] - def _log(self,msg,user=None,token=None,name=None,object_id=None,attribute=None,debug=False,error=False): + def _log(self,msg,token=None,name=None,object_id=None,attribute=None,debug=False,error=False): """ Helper function to write data to the log file from the XMLRPC remote implementation. Takes various optional parameters that should be supplied when known. @@ -412,8 +414,6 @@ class CobblerXMLRPCInterface: # add the user editing the object, if supplied m_user = "?" - if user is not None: - m_user = user if token is not None: try: m_user = self.get_user_from_token(token) @@ -423,14 +423,22 @@ class CobblerXMLRPCInterface: msg = "REMOTE %s; user(%s)" % (msg, m_user) if name is not None: + if not isinstance(name, str): + return + if not utils.validate_obj_name(name): + return msg = "%s; name(%s)" % (msg, name) if object_id is not None: + if not utils.validate_obj_id(object_id): + return msg = "%s; object_id(%s)" % (msg, object_id) # add any attributes being modified, if any if attribute: - msg = "%s; attribute(%s)" % (msg, attribute) + if not (isinstance(attribute, str) and re.match(tokenize.Name + '$', attribute)) or keyword.iskeyword(attribute): + return + msg = "%s; attribute(%s)" % (msg, attribute) # log to the correct logger if error: @@ -863,6 +871,9 @@ class CobblerXMLRPCInterface: def modify_file(self,object_id,attribute,arg,token): return self.modify_item("file",object_id,attribute,arg,token) def modify_setting(self,setting_name,value,token): + if not self.api.settings().allow_dynamic_settings: + self._log("modify_setting - feature turned off but was tried to be accessed", token=token) + return 1 self._log("modify_setting(%s)" % setting_name, token=token) self.check_access(token, "modify_setting") try: @@ -1072,7 +1083,10 @@ class CobblerXMLRPCInterface: return self.api.generate_bootcfg(profile,system) def generate_script(self,profile=None,system=None,name=None,**rest): - self._log("generate_script, name is %s" % str(name)) + # This is duplicated from tftpgen.py to prevent log poisoning via a template engine (Cheetah, Jinja2). + if not utils.validate_autoinstall_script_name(name): + raise ValueError("\"name\" handed to generate_script was not valid!") + self._log("generate_script, name is \"%s\"" % name) return self.api.generate_script(profile,system,name) def get_blended_data(self,profile=None,system=None): @@ -1287,6 +1301,8 @@ class CobblerXMLRPCInterface: As it's a bit of a potential log-flooder, it's off by default and needs to be enabled in /etc/cobbler/settings. """ + if not self.__validate_log_data_params(sys_name, file, size, offset, data, token): + return False self._log("upload_log_data (file: '%s', size: %s, offset: %s)" % (file, size, offset), token=token, name=sys_name) @@ -1301,9 +1317,29 @@ class CobblerXMLRPCInterface: if obj == None: # system not found! self._log("upload_log_data - WARNING - system '%s' not found in cobbler" % sys_name, token=token, name=sys_name) + return False return self.__upload_file(sys_name, file, size, offset, data) + def __validate_log_data_params(self, sys_name, logfile_name, size, offset, data, token=None): + # Validate all types + if not (isinstance(sys_name, str) and isinstance(logfile_name, str) and isinstance(size, int) + and isinstance(offset, int) and isinstance(data, bytes)): + self.logger.warning("upload_log_data - One of the parameters handed over had an invalid type!") + return False + if token is not None and not isinstance(token, str): + self.logger.warning("upload_log_data - token was given but had an invalid type.") + return False + # Validate sys_name with item regex + if not re.match(item.Item._re_name, sys_name): + self.logger.warning("upload_log_data - The provided sys_name contained invalid characters!") + return False + # Validate logfile_name - this uses the script name validation, possibly we need our own for this one later + if not utils.validate_autoinstall_script_name(logfile_name): + self.logger.warning("upload_log_data - The provided file contained invalid characters!") + return False + return True + def __upload_file(self, sys_name, file, size, offset, data): ''' system: the name of the system @@ -1322,21 +1358,21 @@ class CobblerXMLRPCInterface: if size != len(contents): return False - #XXX - have an incoming dir and move after upload complete - # SECURITY - ensure path remains under uploadpath - tt = string.maketrans("/","+") - fn = string.translate(file, tt) - if fn.startswith('..'): - raise CX("invalid filename used: %s" % fn) + anamon_base_directory = "/var/log/cobbler/anamon" + anamon_sys_directory = os.path.join(anamon_base_directory, sys_name) + + file_name = os.path.join(anamon_sys_directory, file) + normalized_path = os.path.normpath(file_name) + if not normalized_path.startswith(anamon_sys_directory): + self.logger.warning("upload_log_data: built path for the logfile was outside of the Cobbler-Anamon log " + "directory!") + return False - # 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, 0755) + if not os.path.isdir(anamon_sys_directory): + os.mkdir(anamon_sys_directory, 0o755) - fn = "%s/%s" % (udir, fn) try: - st = os.lstat(fn) + st = os.lstat(file_name) except OSError, e: if e.errno == errno.ENOENT: pass @@ -1344,9 +1380,9 @@ class CobblerXMLRPCInterface: raise else: if not stat.S_ISREG(st.st_mode): - raise CX("destination not a file: %s" % fn) + raise CX("destination not a file: %s" % file_name) - fd = os.open(fn, os.O_RDWR | os.O_CREAT, 0644) + fd = os.open(file_name, os.O_RDWR | os.O_CREAT, 0644) # log_error("fd=%r" %fd) try: if offset == 0 or (offset == -1 and size == len(contents)): diff --git a/cobbler/utils.py b/cobbler/utils.py index cb431964..615616ba 100644 --- a/cobbler/utils.py +++ b/cobbler/utils.py @@ -110,6 +110,7 @@ _re_kernel = re.compile(r'(vmlinu[xz]|kernel.img)') _re_initrd = re.compile(r'(initrd(.*).img|ramdisk.image.gz)') _re_is_mac = re.compile(':'.join(('[0-9A-Fa-f][0-9A-Fa-f]',)*6) + '$') _re_is_ibmac = re.compile(':'.join(('[0-9A-Fa-f][0-9A-Fa-f]',)*20) + '$') +_re_name = re.compile(r'[a-zA-Z0-9_\-.:+]*$') # all logging from utils.die goes to the main log even if there # is another log. @@ -2331,6 +2332,39 @@ def suse_kopts_textmode_overwrite(distro, kopts): kopts.pop('text', None) kopts['textmode'] = ['1'] + +RE_SCRIPT_NAME = re.compile(r"(?:[a-zA-Z0-9_.]+)\Z") + + +def validate_autoinstall_script_name(name): + if not isinstance(name, str): + return False + if re.match(RE_SCRIPT_NAME, name): + return True + return False + + +def validate_obj_name(object_name): + if not isinstance(object_name, basestring): + return False + return bool(re.match(_re_name, object_name)) + + +def validate_obj_type(object_type): + if not isinstance(object_type, str): + return False + return object_type in ["distro", "profile", "system", "repo", "image", "mgmtclass", "package", "file"] + + +def validate_obj_id(object_id): + if not isinstance(object_id, str): + return False + if object_id.startswith("___NEW___"): + object_id = object_id[9:] + (otype, oname) = object_id.split("::", 1) + return validate_obj_type(otype) and validate_obj_name(oname) + + if __name__ == "__main__": print os_release() # returns 2, not 3 -- 2.32.0
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor