CVE-2015-6240: Improper Link Resolution Before File Access ('Link Following') (CWE-59)
value: The chroot, jail, and zone connection plugins in ansible before 1.9.2 allow local users to escape a restricted environment via a symlink attack.
CWE description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
CVSS v3 base score: 7.8
Published: 2017-06-07T20:29Z
Repository: https://github.com/ansible/ansible
Language: Python
Files changed: lib/ansible/plugins/connections/chroot.py
Commit date: 2015-06-22 20:08:08 -0700
Commit message: Fix problem with chroot connection plugins and symlinks from within the chroot.

Diff: diff --git a/lib/ansible/plugins/connections/chroot.py b/lib/ansible/plugins/connections/chroot.py
index 3ecc0f703..7e3cbe335 100644
--- a/lib/ansible/plugins/connections/chroot.py
+++ b/lib/ansible/plugins/connections/chroot.py
@@ -1,7 +1,8 @@
 # Based on local.py (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
 # (c) 2013, Maykel Moya <mmoya@speedyrails.com>
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
 #
 # This file is part of Ansible
 #
 # Ansible is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -19,17 +20,18 @@ from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
 import distutils.spawn
 import traceback
 import os
-import shutil
 import subprocess
 from ansible import errors
 from ansible import utils
 from ansible.callbacks import vvv
 import ansible.constants as C
 
+BUFSIZE = 65536
+
 class Connection(object):
     ''' Local chroot based connections '''
 
     def __init__(self, runner, host, port, *args, **kwargs):
         self.chroot = host
@@ -62,73 +64,91 @@ class Connection(object):
 
         vvv("THIS IS A LOCAL CHROOT DIR", host=self.chroot)
 
         return self
 
-    def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
-        ''' run a command on the chroot '''
+    def _generate_cmd(self, executable, cmd):
+        if executable:
+            local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
+        else:
+            local_cmd = '%s "%s" %s' % (self.chroot_cmd, self.chroot, cmd)
+        return local_cmd
+
+    def _buffered_exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None, stdin=subprocess.PIPE):
+        ''' run a command on the chroot.  This is only needed for implementing
+        put_file() get_file() so that we don't have to read the whole file
+        into memory.
+
+        compared to exec_command() it looses some niceties like being able to
+        return the process's exit code immediately.
+        '''
 
         if sudoable and self.runner.become and self.runner.become_method not in self.become_methods_supported:
             raise errors.AnsibleError("Internal Error: this module does not support running commands via %s" % self.runner.become_method)
 
         if in_data:
             raise errors.AnsibleError("Internal Error: this module does not support optimized module pipelining")
 
         # We enter chroot as root so we ignore privlege escalation?
-
-        if executable:
-            local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
-        else:
-            local_cmd = '%s "%s" %s' % (self.chroot_cmd, self.chroot, cmd)
+        local_cmd = self._generate_cmd(executable, cmd)
 
         vvv("EXEC %s" % (local_cmd), host=self.chroot)
         p = subprocess.Popen(local_cmd, shell=isinstance(local_cmd, basestring),
                              cwd=self.runner.basedir,
-                             stdin=subprocess.PIPE,
+                             stdin=stdin,
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
 
+        return p
+
+    def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
+        ''' run a command on the chroot '''
+
+        p = self._buffered_exec_command(cmd, tmp_path, become_user, sudoable, executable, in_data)
+
         stdout, stderr = p.communicate()
         return (p.returncode, '', stdout, stderr)
 
     def put_file(self, in_path, out_path):
         ''' transfer a file from local to chroot '''
 
-        if not out_path.startswith(os.path.sep):
-            out_path = os.path.join(os.path.sep, out_path)
-        normpath = os.path.normpath(out_path)
-        out_path = os.path.join(self.chroot, normpath[1:])
-
         vvv("PUT %s TO %s" % (in_path, out_path), host=self.chroot)
-        if not os.path.exists(in_path):
-            raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
+
         try:
-            shutil.copyfile(in_path, out_path)
-        except shutil.Error:
-            traceback.print_exc()
-            raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
+            with open(in_path, 'rb') as in_file:
+                try:
+                    p = self._buffered_exec_command('dd of=%s' % out_path, None, stdin=in_file)
+                except OSError:
+                    raise errors.AnsibleError("chroot connection requires dd command in the chroot")
+                try:
+                    stdout, stderr = p.communicate()
+                except:
+                    traceback.print_exc()
+                    raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
+                if p.returncode != 0:
+                    raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
         except IOError:
-            traceback.print_exc()
-            raise errors.AnsibleError("failed to transfer file to %s" % out_path)
+            raise errors.AnsibleError("file or module does not exist at: %s" % in_path)
 
     def fetch_file(self, in_path, out_path):
         ''' fetch a file from chroot to local '''
 
-        if not in_path.startswith(os.path.sep):
-            in_path = os.path.join(os.path.sep, in_path)
-        normpath = os.path.normpath(in_path)
-        in_path = os.path.join(self.chroot, normpath[1:])
-
         vvv("FETCH %s TO %s" % (in_path, out_path), host=self.chroot)
-        if not os.path.exists(in_path):
-            raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
+
         try:
-            shutil.copyfile(in_path, out_path)
-        except shutil.Error:
-            traceback.print_exc()
-            raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
-        except IOError:
-            traceback.print_exc()
-            raise errors.AnsibleError("failed to transfer file to %s" % out_path)
+            p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, BUFSIZE), None)
+        except OSError:
+            raise errors.AnsibleError("chroot connection requires dd command in the jail")
+
+        with open(out_path, 'wb+') as out_file:
+            try:
+                for chunk in p.stdout.read(BUFSIZE):
+                    out_file.write(chunk)
+            except:
+                traceback.print_exc()
+                raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
+            stdout, stderr = p.communicate()
+            if p.returncode != 0:
+                raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
 
     def close(self):
         ''' terminate the connection; nothing to do here '''
         pass

Vulnerable code: import shutil
    def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
        ''' run a command on the chroot '''

        if executable:
            local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
        else:
            local_cmd = '%s "%s" %s' % (self.chroot_cmd, self.chroot, cmd)
                             stdin=subprocess.PIPE,
        if not out_path.startswith(os.path.sep):
            out_path = os.path.join(os.path.sep, out_path)
        normpath = os.path.normpath(out_path)
        out_path = os.path.join(self.chroot, normpath[1:])

        if not os.path.exists(in_path):
            raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
            shutil.copyfile(in_path, out_path)
        except shutil.Error:
            traceback.print_exc()
            raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
            traceback.print_exc()
            raise errors.AnsibleError("failed to transfer file to %s" % out_path)
        if not in_path.startswith(os.path.sep):
            in_path = os.path.join(os.path.sep, in_path)
        normpath = os.path.normpath(in_path)
        in_path = os.path.join(self.chroot, normpath[1:])

        if not os.path.exists(in_path):
            raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
            shutil.copyfile(in_path, out_path)
        except shutil.Error:
            traceback.print_exc()
            raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
        except IOError:
            traceback.print_exc()
            raise errors.AnsibleError("failed to transfer file to %s" % out_path)
Fixed code: # (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
BUFSIZE = 65536

    def _generate_cmd(self, executable, cmd):
        if executable:
            local_cmd = [self.chroot_cmd, self.chroot, executable, '-c', cmd]
        else:
            local_cmd = '%s "%s" %s' % (self.chroot_cmd, self.chroot, cmd)
        return local_cmd

    def _buffered_exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None, stdin=subprocess.PIPE):
        ''' run a command on the chroot.  This is only needed for implementing
        put_file() get_file() so that we don't have to read the whole file
        into memory.

        compared to exec_command() it looses some niceties like being able to
        return the process's exit code immediately.
        '''
        local_cmd = self._generate_cmd(executable, cmd)
                             stdin=stdin,
        return p

    def exec_command(self, cmd, tmp_path, become_user=None, sudoable=False, executable='/bin/sh', in_data=None):
        ''' run a command on the chroot '''

        p = self._buffered_exec_command(cmd, tmp_path, become_user, sudoable, executable, in_data)


            with open(in_path, 'rb') as in_file:
                try:
                    p = self._buffered_exec_command('dd of=%s' % out_path, None, stdin=in_file)
                except OSError:
                    raise errors.AnsibleError("chroot connection requires dd command in the chroot")
                try:
                    stdout, stderr = p.communicate()
                except:
                    traceback.print_exc()
                    raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
                if p.returncode != 0:
                    raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))
            raise errors.AnsibleError("file or module does not exist at: %s" % in_path)

            p = self._buffered_exec_command('dd if=%s bs=%s' % (in_path, BUFSIZE), None)
        except OSError:
            raise errors.AnsibleError("chroot connection requires dd command in the jail")

        with open(out_path, 'wb+') as out_file:
            try:
                for chunk in p.stdout.read(BUFSIZE):
                    out_file.write(chunk)
            except:
                traceback.print_exc()
                raise errors.AnsibleError("failed to transfer file %s to %s" % (in_path, out_path))
            stdout, stderr = p.communicate()
            if p.returncode != 0:
                raise errors.AnsibleError("failed to transfer file %s to %s:\n%s\n%s" % (in_path, out_path, stdout, stderr))