1# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4# 5# Based on tests from: 6# http://bazaar.launchpad.net/~ubuntu-bugcontrol/qa-regression-testing/master/view/head:/scripts/test-kernel-security.py 7# Copyright (C) 2008-2011 Canonical Ltd. 8# 9# This program is free software: you can redistribute it and/or modify 10# it under the terms of the GNU General Public License version 3, 11# as published by the Free Software Foundation. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program. If not, see <http://www.gnu.org/licenses/>. 20 21import pwd 22import tempfile 23import shutil 24import logging, os 25from autotest_lib.client.bin import test, utils 26from autotest_lib.client.common_lib import error 27 28class security_SymlinkRestrictions(test.test): 29 version = 1 30 31 def _passed(self, msg): 32 logging.info('ok: %s' % (msg)) 33 34 def _failed(self, msg): 35 logging.error('FAIL: %s' % (msg)) 36 self._failures.append(msg) 37 38 def _fatal(self, msg): 39 logging.error('FATAL: %s' % (msg)) 40 raise error.TestError(msg) 41 42 def check(self, boolean, msg, fatal=False): 43 if boolean == True: 44 self._passed(msg) 45 else: 46 msg = "could not satisfy '%s'" % (msg) 47 if fatal: 48 self._fatal(msg) 49 else: 50 self._failed(msg) 51 52 def _read_contents_as(self, path, content, user, fail=False): 53 cat = utils.run("su -c 'cat %s' %s" % (path, user), ignore_status=True) 54 if fail: 55 self.check(cat.exit_status != 0, 56 "%s not readable by %s (exit status %d)" % 57 (path, user, cat.exit_status)) 58 self.check(cat.stdout != content, 59 "%s not readable by %s (content '%s')" % 60 (path, user, cat.stdout)) 61 else: 62 self.check(cat.exit_status == 0, 63 "%s readable by %s (exit status %d)" % 64 (path, user, cat.exit_status)) 65 self.check(cat.stdout == content, 66 "%s readable by %s (content '%s')" % 67 (path, user, cat.stdout)) 68 69 def _write_path_as(self, path, user, fail=False): 70 rc = utils.system("su -c 'dd if=/etc/passwd of=%s' %s" % (path, user), 71 ignore_status=True) 72 if fail: 73 self.check(rc != 0, "%s unwritable by %s (exit status %d)" % 74 (path, user, rc)) 75 else: 76 self.check(rc == 0, "%s writable by %s (exit status %d)" % 77 (path, user, rc)) 78 79 def _write_as(self, op_path, chk_path, user, create=False, fail=False): 80 if create: 81 if os.path.exists(chk_path): 82 os.unlink(chk_path) 83 self.check(os.path.exists(chk_path) == False, 84 "%s does not exist starting _write_as()" % (chk_path)) 85 else: 86 open(chk_path, 'w').write('blah blah\n') 87 self.check(os.path.exists(chk_path), 88 "%s exists" % (chk_path)) 89 os.chown(chk_path, pwd.getpwnam(user)[2], 0) 90 self._write_path_as(op_path, user, fail=fail) 91 if fail: 92 if create: 93 self.check(not os.path.exists(chk_path), 94 "%s does not exist at end of _write_as()" % 95 (chk_path)) 96 else: 97 self.check(os.path.exists(chk_path), 98 "%s exists at end of _write_as()" % (chk_path)) 99 self.check(os.stat(chk_path).st_uid == pwd.getpwnam(user)[2], 100 "%s owned by %s at end of _write_as()" % 101 (chk_path, user)) 102 103 def _check_symlinks(self, sticky, userone, usertwo): 104 uidone = pwd.getpwnam(userone)[2] 105 uidtwo = pwd.getpwnam(usertwo)[2] 106 107 # Verify we have distinct users. 108 if userone == usertwo: 109 self._failed("The '%s' and '%s' user have the same name!" % 110 userone, usertwo) 111 return 112 if uidone == uidtwo: 113 self._failed("The '%s' and '%s' user have the same uid!" % 114 userone, usertwo) 115 return 116 117 # Build a world-writable directory, owned by userone. 118 prefix = 'symlinks-' 119 if not sticky: 120 prefix += 'not' 121 prefix += 'sticky-' 122 tmpdir = tempfile.mkdtemp(prefix=prefix) 123 self._rmdir.append(tmpdir) 124 mode = 0777 125 if sticky: 126 mode |= 01000 127 os.chmod(tmpdir, mode) 128 os.chown(tmpdir, uidone, 0) 129 130 # Verify stickiness behavior, taking uid0's DAC_OVERRIDE into account. 131 drop = os.path.join(tmpdir, "remove.me") 132 open(drop, 'w').write("I can be deleted in a non-sticky directory") 133 os.chown(drop, uidone, 0) 134 135 expected = 0 136 if sticky and (uidtwo != 0): 137 expected = 1 138 rc = utils.system("su -c 'rm -f %s' %s" % (drop, usertwo), 139 ignore_status=True) 140 if rc != expected: 141 if sticky: 142 self._failed("'%s' was able to delete files owned by '%s' " 143 "in a sticky world-writable directory" % 144 (usertwo, userone)) 145 else: 146 self._failed("'%s' wasn't able to delete files owned by '%s' " 147 "in a regular world-writable directory" % 148 (usertwo, userone)) 149 return 150 # File should still exist in a sticky directory. 151 self.check(os.path.exists(drop) == (sticky and uidtwo != 0), 152 "'%s' should only exist in a sticky directory" % (drop)) 153 154 # Create target files. 155 message = 'not very sekrit' 156 target = os.path.join(tmpdir, 'target') 157 open(target, 'w').write(message) 158 os.chmod(target, 0644) 159 160 sekrit_userone = 'sekrit %s' % (userone) 161 target_userone = os.path.join(tmpdir, 'target-%s' % (userone)) 162 open(target_userone, 'w').write(sekrit_userone) 163 os.chmod(target_userone, 0400) 164 os.chown(target_userone, uidone, 0) 165 166 sekrit_usertwo = 'sekrit %s' % (usertwo) 167 target_usertwo = os.path.join(tmpdir, 'target-%s' % (usertwo)) 168 open(target_usertwo, 'w').write(sekrit_usertwo) 169 os.chmod(target_usertwo, 0400) 170 os.chown(target_usertwo, uidtwo, 0) 171 172 # Create symlinks to target as different users. 173 userone_symlink = os.path.join(tmpdir, '%s.symlink' % (userone)) 174 usertwo_symlink = os.path.join(tmpdir, '%s.symlink' % (usertwo)) 175 176 utils.system("su -c 'ln -s %s %s' %s" % (target, userone_symlink, 177 userone)) 178 utils.system("su -c 'ln -s %s %s' %s" % (target, usertwo_symlink, 179 usertwo)) 180 self.check(os.lstat(userone_symlink).st_uid == uidone, 181 "%s owned by %s" % (userone_symlink, userone)) 182 self.check(os.lstat(usertwo_symlink).st_uid == uidtwo, 183 "%s owned by %s" % (usertwo_symlink, usertwo)) 184 # Verify userone symlink and directory are owned by the same uid. 185 self.check(os.lstat(userone_symlink).st_uid == os.lstat(tmpdir).st_uid, 186 "%s and %s have same owner" % 187 (tmpdir, userone_symlink)) 188 189 ## Perform read verifications. 190 # Global target should be directly readable by both users. 191 self._read_contents_as(target, message, userone) 192 self._read_contents_as(target, message, usertwo) 193 # Individual targets should only be readable by owner, verifying 194 # DAC sanity, before we check symlink restriction tweaks, though 195 # we have to account for uid0's DAC_OVERRIDE. 196 self._read_contents_as(target_userone, sekrit_userone, userone) 197 self._read_contents_as(target_usertwo, sekrit_usertwo, usertwo) 198 self._read_contents_as(target_userone, sekrit_userone, 199 usertwo, fail=(uidtwo != 0)) 200 self._read_contents_as(target_usertwo, sekrit_usertwo, 201 userone, fail=(uidone != 0)) 202 # Global target should be readable through symlink by symlink owner, 203 self._read_contents_as(userone_symlink, message, userone) 204 self._read_contents_as(usertwo_symlink, message, usertwo) 205 # Global target should be readable through symlink of directory owner. 206 self._read_contents_as(userone_symlink, message, usertwo) 207 # Global target should not be readable through symlink when directory 208 # is sticky and the symlink and directory owner are different. 209 self._read_contents_as(usertwo_symlink, message, userone, 210 fail=sticky) 211 212 ## Perform write verifications. 213 # Global target should be directly writable by both users. 214 self._write_as(target, target, userone) 215 self._write_as(target, target, usertwo) 216 # Global target should be writable through owner's symlink. 217 self._write_as(userone_symlink, target, userone) 218 self._write_as(usertwo_symlink, target, usertwo) 219 # Global target should be writable through symlink of directory owner. 220 self._write_as(userone_symlink, target, usertwo) 221 # Global target should be unwritable through symlink when directory 222 # is sticky and the symlink and directory owner are different. 223 self._write_as(usertwo_symlink, target, userone, fail=sticky) 224 225 ## Perform write-with-create verifications. 226 # Global target should be directly creatable by both users. 227 self._write_as(target, target, userone, create=True) 228 self._write_as(target, target, usertwo, create=True) 229 # Global target should be creatable through owner's symlink. 230 self._write_as(userone_symlink, target, userone, create=True) 231 self._write_as(usertwo_symlink, target, usertwo, create=True) 232 # Global target should be creatable through symlink of directory owner. 233 self._write_as(userone_symlink, target, usertwo, create=True) 234 # Global target should be uncreatable through symlink when directory 235 # is sticky and the symlink and directory owner are different. 236 self._write_as(usertwo_symlink, target, userone, create=True, 237 fail=sticky) 238 239 def run_once(self): 240 # Empty failure list means test passes. 241 self._failures = [] 242 243 # Prepare list of directories to clean up. 244 self._rmdir = [] 245 246 # Verify symlink restrictions sysctl exists and is enabled. 247 sysctl = "/proc/sys/fs/protected_symlinks" 248 if (not os.path.exists(sysctl)): 249 # Fall back to looking for Yama link restriction sysctl. 250 sysctl = "/proc/sys/kernel/yama/protected_sticky_symlinks" 251 self.check(os.path.exists(sysctl), "%s exists" % (sysctl), fatal=True) 252 self.check(open(sysctl).read() == '1\n', "%s enabled" % (sysctl), 253 fatal=True) 254 255 # Test the basic "root follows evil symlink" situation first, in 256 # a more auditable way than the extensive behavior tests that follow. 257 if os.path.exists("/tmp/evil-symlink"): 258 os.unlink("/tmp/evil-symlink") 259 utils.system("su -c 'ln -s /etc/shadow /tmp/evil-symlink' chronos") 260 rc = utils.system("cat /tmp/evil-symlink", ignore_status=True) 261 if rc != 1: 262 self._failed("root user was able to follow malicious symlink") 263 os.unlink("/tmp/evil-symlink") 264 265 # Test symlink restrictions, making sure there is no special 266 # behavior for the root user (DAC_OVERRIDE is ignored). 267 self._check_symlinks(sticky=False, userone='root', usertwo='chronos') 268 self._check_symlinks(sticky=False, userone='chronos', usertwo='root') 269 self._check_symlinks(sticky=True, userone='root', usertwo='chronos') 270 self._check_symlinks(sticky=True, userone='chronos', usertwo='root') 271 272 # Clean up from the tests. 273 for path in self._rmdir: 274 if os.path.exists(path): 275 shutil.rmtree(path, ignore_errors=True) 276 277 # Raise a failure if anything unexpected was seen. 278 if len(self._failures): 279 raise error.TestFail((", ".join(self._failures))) 280