# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # # Based on tests from: # http://bazaar.launchpad.net/~ubuntu-bugcontrol/qa-regression-testing/master/view/head:/scripts/test-kernel-security.py # Copyright (C) 2008-2011 Canonical Ltd. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import pwd import tempfile import shutil import logging, os from autotest_lib.client.bin import test, utils from autotest_lib.client.common_lib import error class security_SymlinkRestrictions(test.test): version = 1 def _passed(self, msg): logging.info('ok: %s' % (msg)) def _failed(self, msg): logging.error('FAIL: %s' % (msg)) self._failures.append(msg) def _fatal(self, msg): logging.error('FATAL: %s' % (msg)) raise error.TestError(msg) def check(self, boolean, msg, fatal=False): if boolean == True: self._passed(msg) else: msg = "could not satisfy '%s'" % (msg) if fatal: self._fatal(msg) else: self._failed(msg) def _read_contents_as(self, path, content, user, fail=False): cat = utils.run("su -c 'cat %s' %s" % (path, user), ignore_status=True) if fail: self.check(cat.exit_status != 0, "%s not readable by %s (exit status %d)" % (path, user, cat.exit_status)) self.check(cat.stdout != content, "%s not readable by %s (content '%s')" % (path, user, cat.stdout)) else: self.check(cat.exit_status == 0, "%s readable by %s (exit status %d)" % (path, user, cat.exit_status)) self.check(cat.stdout == content, "%s readable by %s (content '%s')" % (path, user, cat.stdout)) def _write_path_as(self, path, user, fail=False): rc = utils.system("su -c 'dd if=/etc/passwd of=%s' %s" % (path, user), ignore_status=True) if fail: self.check(rc != 0, "%s unwritable by %s (exit status %d)" % (path, user, rc)) else: self.check(rc == 0, "%s writable by %s (exit status %d)" % (path, user, rc)) def _write_as(self, op_path, chk_path, user, create=False, fail=False): if create: if os.path.exists(chk_path): os.unlink(chk_path) self.check(os.path.exists(chk_path) == False, "%s does not exist starting _write_as()" % (chk_path)) else: open(chk_path, 'w').write('blah blah\n') self.check(os.path.exists(chk_path), "%s exists" % (chk_path)) os.chown(chk_path, pwd.getpwnam(user)[2], 0) self._write_path_as(op_path, user, fail=fail) if fail: if create: self.check(not os.path.exists(chk_path), "%s does not exist at end of _write_as()" % (chk_path)) else: self.check(os.path.exists(chk_path), "%s exists at end of _write_as()" % (chk_path)) self.check(os.stat(chk_path).st_uid == pwd.getpwnam(user)[2], "%s owned by %s at end of _write_as()" % (chk_path, user)) def _check_symlinks(self, sticky, userone, usertwo): uidone = pwd.getpwnam(userone)[2] uidtwo = pwd.getpwnam(usertwo)[2] # Verify we have distinct users. if userone == usertwo: self._failed("The '%s' and '%s' user have the same name!" % userone, usertwo) return if uidone == uidtwo: self._failed("The '%s' and '%s' user have the same uid!" % userone, usertwo) return # Build a world-writable directory, owned by userone. prefix = 'symlinks-' if not sticky: prefix += 'not' prefix += 'sticky-' tmpdir = tempfile.mkdtemp(prefix=prefix) self._rmdir.append(tmpdir) mode = 0777 if sticky: mode |= 01000 os.chmod(tmpdir, mode) os.chown(tmpdir, uidone, 0) # Verify stickiness behavior, taking uid0's DAC_OVERRIDE into account. drop = os.path.join(tmpdir, "remove.me") open(drop, 'w').write("I can be deleted in a non-sticky directory") os.chown(drop, uidone, 0) expected = 0 if sticky and (uidtwo != 0): expected = 1 rc = utils.system("su -c 'rm -f %s' %s" % (drop, usertwo), ignore_status=True) if rc != expected: if sticky: self._failed("'%s' was able to delete files owned by '%s' " "in a sticky world-writable directory" % (usertwo, userone)) else: self._failed("'%s' wasn't able to delete files owned by '%s' " "in a regular world-writable directory" % (usertwo, userone)) return # File should still exist in a sticky directory. self.check(os.path.exists(drop) == (sticky and uidtwo != 0), "'%s' should only exist in a sticky directory" % (drop)) # Create target files. message = 'not very sekrit' target = os.path.join(tmpdir, 'target') open(target, 'w').write(message) os.chmod(target, 0644) sekrit_userone = 'sekrit %s' % (userone) target_userone = os.path.join(tmpdir, 'target-%s' % (userone)) open(target_userone, 'w').write(sekrit_userone) os.chmod(target_userone, 0400) os.chown(target_userone, uidone, 0) sekrit_usertwo = 'sekrit %s' % (usertwo) target_usertwo = os.path.join(tmpdir, 'target-%s' % (usertwo)) open(target_usertwo, 'w').write(sekrit_usertwo) os.chmod(target_usertwo, 0400) os.chown(target_usertwo, uidtwo, 0) # Create symlinks to target as different users. userone_symlink = os.path.join(tmpdir, '%s.symlink' % (userone)) usertwo_symlink = os.path.join(tmpdir, '%s.symlink' % (usertwo)) utils.system("su -c 'ln -s %s %s' %s" % (target, userone_symlink, userone)) utils.system("su -c 'ln -s %s %s' %s" % (target, usertwo_symlink, usertwo)) self.check(os.lstat(userone_symlink).st_uid == uidone, "%s owned by %s" % (userone_symlink, userone)) self.check(os.lstat(usertwo_symlink).st_uid == uidtwo, "%s owned by %s" % (usertwo_symlink, usertwo)) # Verify userone symlink and directory are owned by the same uid. self.check(os.lstat(userone_symlink).st_uid == os.lstat(tmpdir).st_uid, "%s and %s have same owner" % (tmpdir, userone_symlink)) ## Perform read verifications. # Global target should be directly readable by both users. self._read_contents_as(target, message, userone) self._read_contents_as(target, message, usertwo) # Individual targets should only be readable by owner, verifying # DAC sanity, before we check symlink restriction tweaks, though # we have to account for uid0's DAC_OVERRIDE. self._read_contents_as(target_userone, sekrit_userone, userone) self._read_contents_as(target_usertwo, sekrit_usertwo, usertwo) self._read_contents_as(target_userone, sekrit_userone, usertwo, fail=(uidtwo != 0)) self._read_contents_as(target_usertwo, sekrit_usertwo, userone, fail=(uidone != 0)) # Global target should be readable through symlink by symlink owner, self._read_contents_as(userone_symlink, message, userone) self._read_contents_as(usertwo_symlink, message, usertwo) # Global target should be readable through symlink of directory owner. self._read_contents_as(userone_symlink, message, usertwo) # Global target should not be readable through symlink when directory # is sticky and the symlink and directory owner are different. self._read_contents_as(usertwo_symlink, message, userone, fail=sticky) ## Perform write verifications. # Global target should be directly writable by both users. self._write_as(target, target, userone) self._write_as(target, target, usertwo) # Global target should be writable through owner's symlink. self._write_as(userone_symlink, target, userone) self._write_as(usertwo_symlink, target, usertwo) # Global target should be writable through symlink of directory owner. self._write_as(userone_symlink, target, usertwo) # Global target should be unwritable through symlink when directory # is sticky and the symlink and directory owner are different. self._write_as(usertwo_symlink, target, userone, fail=sticky) ## Perform write-with-create verifications. # Global target should be directly creatable by both users. self._write_as(target, target, userone, create=True) self._write_as(target, target, usertwo, create=True) # Global target should be creatable through owner's symlink. self._write_as(userone_symlink, target, userone, create=True) self._write_as(usertwo_symlink, target, usertwo, create=True) # Global target should be creatable through symlink of directory owner. self._write_as(userone_symlink, target, usertwo, create=True) # Global target should be uncreatable through symlink when directory # is sticky and the symlink and directory owner are different. self._write_as(usertwo_symlink, target, userone, create=True, fail=sticky) def run_once(self): # Empty failure list means test passes. self._failures = [] # Prepare list of directories to clean up. self._rmdir = [] # Verify symlink restrictions sysctl exists and is enabled. sysctl = "/proc/sys/fs/protected_symlinks" if (not os.path.exists(sysctl)): # Fall back to looking for Yama link restriction sysctl. sysctl = "/proc/sys/kernel/yama/protected_sticky_symlinks" self.check(os.path.exists(sysctl), "%s exists" % (sysctl), fatal=True) self.check(open(sysctl).read() == '1\n', "%s enabled" % (sysctl), fatal=True) # Test the basic "root follows evil symlink" situation first, in # a more auditable way than the extensive behavior tests that follow. if os.path.exists("/tmp/evil-symlink"): os.unlink("/tmp/evil-symlink") utils.system("su -c 'ln -s /etc/shadow /tmp/evil-symlink' chronos") rc = utils.system("cat /tmp/evil-symlink", ignore_status=True) if rc != 1: self._failed("root user was able to follow malicious symlink") os.unlink("/tmp/evil-symlink") # Test symlink restrictions, making sure there is no special # behavior for the root user (DAC_OVERRIDE is ignored). self._check_symlinks(sticky=False, userone='root', usertwo='chronos') self._check_symlinks(sticky=False, userone='chronos', usertwo='root') self._check_symlinks(sticky=True, userone='root', usertwo='chronos') self._check_symlinks(sticky=True, userone='chronos', usertwo='root') # Clean up from the tests. for path in self._rmdir: if os.path.exists(path): shutil.rmtree(path, ignore_errors=True) # Raise a failure if anything unexpected was seen. if len(self._failures): raise error.TestFail((", ".join(self._failures)))