Wednesday, April 29, 2020

Re: Salt Vulnerability Discovered [Was: Re: salt 2019.2.3 & python3]

On 24/04/20 13:35 +0200, Robert Nagy wrote:
> On 24/04/20 12:15 +0100, Raf Czlonka wrote:
> > Hi all,
> >
> > Really bad timing but it looks like Salt master in OpenBSD will be
> > vulnerable for at least half a year more, unless someone backports
> > the fix, which is to be released next week, to version 2018.3.
> >
> > https://groups.google.com/forum/#!topic/salt-users/zjwt44a919U
> >
> > Regards,
> >
> > Raf

Please give this a throughout testing.

Index: Makefile
===================================================================
RCS file: /cvs/ports/sysutils/salt/Makefile,v
retrieving revision 1.137
diff -u -p -u -r1.137 Makefile
--- Makefile 18 Apr 2020 09:22:32 -0000 1.137
+++ Makefile 30 Apr 2020 05:17:01 -0000
@@ -19,7 +19,7 @@ COMMENT = remote execution and configur

MODPY_EGG_VERSION = 2018.3.3
DISTNAME = salt-${MODPY_EGG_VERSION}
-REVISION = 1
+REVISION = 2

CATEGORIES = sysutils net devel

Index: patches/patch-salt_master_py
===================================================================
RCS file: patches/patch-salt_master_py
diff -N patches/patch-salt_master_py
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ patches/patch-salt_master_py 30 Apr 2020 05:17:01 -0000
@@ -0,0 +1,110 @@
+$OpenBSD$
+
+commit f47e4856497231eb672da2ce0df3e641581d47e6
+Author: Daniel A. Wozniak <dwozniak@saltstack.com>
+Date: Mon Apr 13 06:41:04 2020 +0000
+
+ Fix CVE-2020-11651
+
+ Resolve issue which allows access to un-intended methods in the
+ ClearFuncs class of the salt-master process
+
+Index: salt/master.py
+--- salt/master.py.orig
++++ salt/master.py
+@@ -1045,12 +1045,13 @@ class MWorker(salt.utils.process.SignalHandlingMultipr
+ '''
+ log.trace('Clear payload received with command %s', load['cmd'])
+ cmd = load['cmd']
+- if cmd.startswith('__'):
+- return False
++ method = self.clear_funcs.get_method(cmd)
++ if not method:
++ return {}, {'fun': 'send_clear'}
+ if self.opts['master_stats']:
+ start = time.time()
+ self.stats[cmd]['runs'] += 1
+- ret = getattr(self.clear_funcs, cmd)(load), {'fun': 'send_clear'}
++ ret = method(load), {'fun': 'send_clear'}
+ if self.opts['master_stats']:
+ self._post_stats(start, cmd)
+ return ret
+@@ -1068,8 +1069,9 @@ class MWorker(salt.utils.process.SignalHandlingMultipr
+ return {}
+ cmd = data['cmd']
+ log.trace('AES payload received with command %s', data['cmd'])
+- if cmd.startswith('__'):
+- return False
++ method = self.aes_funcs.get_method(cmd)
++ if not method:
++ return {}, {'fun': 'send'}
+ if self.opts['master_stats']:
+ start = time.time()
+ self.stats[cmd]['runs'] += 1
+@@ -1092,13 +1094,43 @@ class MWorker(salt.utils.process.SignalHandlingMultipr
+ self.__bind()
+
+
++class TransportMethods(object):
++ '''
++ Expose methods to the transport layer, methods with their names found in
++ the class attribute 'expose_methods' will be exposed to the transport layer
++ via 'get_method'.
++ '''
++
++ expose_methods = ()
++
++ def get_method(self, name):
++ '''
++ Get a method which should be exposed to the transport layer
++ '''
++ if name in self.expose_methods:
++ try:
++ return getattr(self, name)
++ except AttributeError:
++ log.error("Expose method not found: %s", name)
++ else:
++ log.error("Requested method not exposed: %s", name)
++
+ # TODO: rename? No longer tied to "AES", just "encrypted" or "private" requests
+-class AESFuncs(object):
++class AESFuncs(TransportMethods):
+ '''
+ Set up functions that are available when the load is encrypted with AES
+ '''
+- # The AES Functions:
+- #
++
++ expose_methods = (
++ 'verify_minion', '_master_tops', '_ext_nodes', '_master_opts',
++ '_mine_get', '_mine', '_mine_delete', '_mine_flush', '_file_recv',
++ '_pillar', '_minion_event', '_handle_minion_event', '_return',
++ '_syndic_return', '_minion_runner', 'pub_ret', 'minion_pub',
++ 'minion_publish', 'revoke_auth', 'run_func', '_serve_file',
++ '_file_find', '_file_hash', '_file_find_and_stat', '_file_list',
++ '_file_list_emptydirs', '_dir_list', '_symlink_list', '_file_envs',
++ )
++
+ def __init__(self, opts):
+ '''
+ Create a new AESFuncs
+@@ -1812,11 +1844,18 @@ class AESFuncs(object):
+ return ret, {'fun': 'send'}
+
+
+-class ClearFuncs(object):
++class ClearFuncs(TransportMethods):
+ '''
+ Set up functions that are safe to execute when commands sent to the master
+ without encryption and authentication
+ '''
++
++ # These methods will be exposed to the transport layer by
++ # MWorker._handle_clear
++ expose_methods = (
++ 'ping', 'publish', 'get_token', 'mk_token', 'wheel', 'runner',
++ )
++
+ # The ClearFuncs object encapsulates the functions that can be executed in
+ # the clear:
+ # publish (The publish from the LocalClient)
Index: patches/patch-salt_tokens_localfs_py
===================================================================
RCS file: patches/patch-salt_tokens_localfs_py
diff -N patches/patch-salt_tokens_localfs_py
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ patches/patch-salt_tokens_localfs_py 30 Apr 2020 05:17:01 -0000
@@ -0,0 +1,31 @@
+$OpenBSD$
+
+commit 7bd0ab195fbec4f34523dad11149f741c154e2b7
+Author: Daniel A. Wozniak <dwozniak@saltstack.com>
+Date: Mon Apr 13 06:44:58 2020 +0000
+
+ Fix CVE-2020-11652
+
+ Sanitize paths in ClearFuncs methods provided by salt-master. This
+ ensures we do not allow access to un-intended files and directories.
+
+Index: salt/tokens/localfs.py
+--- salt/tokens/localfs.py.orig
++++ salt/tokens/localfs.py
+@@ -12,6 +12,7 @@ import logging
+
+ import salt.utils.files
+ import salt.utils.path
++import salt.utils.verify
+ import salt.payload
+
+ from salt.ext import six
+@@ -34,6 +35,8 @@ def mk_token(opts, tdata):
+ hash_type = getattr(hashlib, opts.get('hash_type', 'md5'))
+ tok = six.text_type(hash_type(os.urandom(512)).hexdigest())
+ t_path = os.path.join(opts['token_dir'], tok)
++ if not salt.utils.verify.clean_path(opts['token_dir'], t_path):
++ return {}
+ while os.path.isfile(t_path):
+ tok = six.text_type(hash_type(os.urandom(512)).hexdigest())
+ t_path = os.path.join(opts['token_dir'], tok)
Index: patches/patch-salt_utils_verify_py
===================================================================
RCS file: patches/patch-salt_utils_verify_py
diff -N patches/patch-salt_utils_verify_py
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ patches/patch-salt_utils_verify_py 30 Apr 2020 05:17:01 -0000
@@ -0,0 +1,97 @@
+$OpenBSD$
+
+commit 7bd0ab195fbec4f34523dad11149f741c154e2b7
+Author: Daniel A. Wozniak <dwozniak@saltstack.com>
+Date: Mon Apr 13 06:44:58 2020 +0000
+
+ Fix CVE-2020-11652
+
+ Sanitize paths in ClearFuncs methods provided by salt-master. This
+ ensures we do not allow access to un-intended files and directories.
+
+Index: salt/utils/verify.py
+--- salt/utils/verify.py.orig
++++ salt/utils/verify.py
+@@ -31,6 +31,7 @@ import salt.utils.files
+ import salt.utils.path
+ import salt.utils.platform
+ import salt.utils.user
++import salt.ext.six
+
+ log = logging.getLogger(__name__)
+
+@@ -472,23 +473,69 @@ def check_max_open_files(opts):
+ log.log(level=level, msg=msg)
+
+
++def _realpath_darwin(path):
++ base = ''
++ for part in path.split(os.path.sep)[1:]:
++ if base != '':
++ if os.path.islink(os.path.sep.join([base, part])):
++ base = os.readlink(os.path.sep.join([base, part]))
++ else:
++ base = os.path.abspath(os.path.sep.join([base, part]))
++ else:
++ base = os.path.abspath(os.path.sep.join([base, part]))
++ return base
++
++
++def _realpath_windows(path):
++ base = ''
++ for part in path.split(os.path.sep):
++ if base != '':
++ try:
++ part = os.readlink(os.path.sep.join([base, part]))
++ base = os.path.abspath(part)
++ except OSError:
++ base = os.path.abspath(os.path.sep.join([base, part]))
++ else:
++ base = part
++ return base
++
++
++def _realpath(path):
++ '''
++ Cross platform realpath method. On Windows when python 3, this method
++ uses the os.readlink method to resolve any filesystem links. On Windows
++ when python 2, this method is a no-op. All other platforms and version use
++ os.realpath
++ '''
++ if salt.utils.platform.is_darwin():
++ return _realpath_darwin(path)
++ elif salt.utils.platform.is_windows():
++ if salt.ext.six.PY3:
++ return _realpath_windows(path)
++ else:
++ return path
++ return os.path.realpath(path)
++
++
+ def clean_path(root, path, subdir=False):
+ '''
+ Accepts the root the path needs to be under and verifies that the path is
+ under said root. Pass in subdir=True if the path can result in a
+ subdirectory of the root instead of having to reside directly in the root
+ '''
+- if not os.path.isabs(root):
++ real_root = _realpath(root)
++ if not os.path.isabs(real_root):
+ return ''
+ if not os.path.isabs(path):
+ path = os.path.join(root, path)
+ path = os.path.normpath(path)
++ real_path = _realpath(path)
+ if subdir:
+- if path.startswith(root):
+- return path
++ if real_path.startswith(real_root):
++ return real_path
+ else:
+- if os.path.dirname(path) == os.path.normpath(root):
+- return path
++ if os.path.dirname(real_path) == os.path.normpath(real_root):
++ return real_path
+ return ''
+
+
Index: patches/patch-salt_wheel_config_py
===================================================================
RCS file: patches/patch-salt_wheel_config_py
diff -N patches/patch-salt_wheel_config_py
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ patches/patch-salt_wheel_config_py 30 Apr 2020 05:17:01 -0000
@@ -0,0 +1,35 @@
+$OpenBSD$
+
+commit 7bd0ab195fbec4f34523dad11149f741c154e2b7
+Author: Daniel A. Wozniak <dwozniak@saltstack.com>
+Date: Mon Apr 13 06:44:58 2020 +0000
+
+ Fix CVE-2020-11652
+
+ Sanitize paths in ClearFuncs methods provided by salt-master. This
+ ensures we do not allow access to un-intended files and directories.
+
+Index: salt/wheel/config.py
+--- salt/wheel/config.py.orig
++++ salt/wheel/config.py
+@@ -75,13 +75,19 @@ def update_config(file_name, yaml_contents):
+ dir_path = os.path.join(__opts__['config_dir'],
+ os.path.dirname(__opts__['default_include']))
+ try:
+- yaml_out = salt.utils.yaml.safe_dump(yaml_contents, default_flow_style=False)
++ yaml_out = salt.utils.yaml.safe_dump(
++ yaml_contents,
++ default_flow_style=False,
++ )
+
+ if not os.path.exists(dir_path):
+ log.debug('Creating directory %s', dir_path)
+ os.makedirs(dir_path, 0o755)
+
+ file_path = os.path.join(dir_path, file_name)
++ if not salt.utils.verify.clean_path(dir_path, file_path):
++ return 'Invalid path'
++
+ with salt.utils.files.fopen(file_path, 'w') as fp_:
+ fp_.write(yaml_out)
+
Index: patches/patch-salt_wheel_file_roots_py
===================================================================
RCS file: patches/patch-salt_wheel_file_roots_py
diff -N patches/patch-salt_wheel_file_roots_py
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ patches/patch-salt_wheel_file_roots_py 30 Apr 2020 05:17:01 -0000
@@ -0,0 +1,35 @@
+$OpenBSD$
+
+commit 7bd0ab195fbec4f34523dad11149f741c154e2b7
+Author: Daniel A. Wozniak <dwozniak@saltstack.com>
+Date: Mon Apr 13 06:44:58 2020 +0000
+
+ Fix CVE-2020-11652
+
+ Sanitize paths in ClearFuncs methods provided by salt-master. This
+ ensures we do not allow access to un-intended files and directories.
+
+Index: salt/wheel/file_roots.py
+--- salt/wheel/file_roots.py.orig
++++ salt/wheel/file_roots.py
+@@ -25,6 +25,8 @@ def find(path, saltenv='base'):
+ return ret
+ for root in __opts__['file_roots'][saltenv]:
+ full = os.path.join(root, path)
++ if not salt.utils.verify.clean_path(root, full):
++ continue
+ if os.path.isfile(full):
+ # Add it to the dict
+ with salt.utils.files.fopen(full, 'rb') as fp_:
+@@ -107,7 +109,10 @@ def write(data, path, saltenv='base', index=0):
+ if os.path.isabs(path):
+ return ('The path passed in {0} is not relative to the environment '
+ '{1}').format(path, saltenv)
+- dest = os.path.join(__opts__['file_roots'][saltenv][index], path)
++ root = __opts__['file_roots'][saltenv][index]
++ dest = os.path.join(root, path)
++ if not salt.utils.verify.clean_path(root, dest, subdir=True):
++ return 'Invalid path: {}'.format(path)
+ dest_dir = os.path.dirname(dest)
+ if not os.path.isdir(dest_dir):
+ os.makedirs(dest_dir)

No comments:

Post a Comment