1# 2#Copyright (C) 2003, 2004, 2005, 2006, 2007, 2009, 2010, 2011, 2018 Olivier Sessink 3#All rights reserved. 4# 5#Redistribution and use in source and binary forms, with or without 6#modification, are permitted provided that the following conditions 7#are met: 8# * Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# * Redistributions in binary form must reproduce the above 11# copyright notice, this list of conditions and the following 12# disclaimer in the documentation and/or other materials provided 13# with the distribution. 14# * The names of its contributors may not be used to endorse or 15# promote products derived from this software without specific 16# prior written permission. 17# 18#THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19#"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20#LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21#FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22#COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23#INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24#BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25#LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26#CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27#LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28#ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29#POSSIBILITY OF SUCH DAMAGE. 30# 31 32from __future__ import print_function 33 34import os.path 35import string 36#import os 37import sys 38import stat 39import shutil 40import glob 41import subprocess 42 43 44dir_mode = 493 #octal 755, "rwxr-xr-x" 45 46statcache = {} 47 48def cachedlstat(path): 49 ret = statcache.get(path, None) 50 if ret is None: 51 statcache[path] = ret = os.lstat(path) 52 return ret 53 54#def nextpathup(path): 55# #if (path[-1:] == '/'): 56# # path = path[:-1] 57# try: 58# #print('path='+path) 59# indx = string.rindex(path,'/') 60# if (indx > 0): 61# return path[:indx] 62# except ValueError: 63# pass 64# return None 65 66def path_is_safe(path, failquiet=0): 67 try: 68 statbuf = cachedlstat(path) 69 except OSError: 70 if (failquiet == 0): 71 sys.stderr.write('ERROR: cannot lstat() '+path+'\n') 72 return -1 73 if (sys.platform[-3:] == 'bsd'): 74 # on freebsd root is in group wheel 75 if (statbuf[stat.ST_UID] != 0 or statbuf[stat.ST_GID] != grp.getgrnam('wheel').gr_gid): 76 sys.stderr.write('ERROR: '+path+' is not owned by root:wheel!\n') 77 return -3 78 else: 79 if (statbuf[stat.ST_UID] != 0 or statbuf[stat.ST_GID] != 0): 80 sys.stderr.write('ERROR: '+path+' is not owned by root:root!\n') 81 return -3 82 if ((statbuf[stat.ST_MODE] & stat.S_IWOTH or statbuf[stat.ST_MODE] & stat.S_IWGRP)and not stat.S_ISLNK(statbuf[stat.ST_MODE])): 83 sys.stderr.write('ERROR: '+path+' is writable by group or others!') 84 return -4 85 if (not stat.S_ISDIR(statbuf[stat.ST_MODE])): 86 if (stat.S_ISLNK(statbuf[stat.ST_MODE])): 87 # Fedora has moved /sbin /lib and /bin into /usr 88 target = os.readlink(path) 89 print(str(target) + str(path)) 90 if (target[:4] != 'usr/'): 91 sys.stderr.write('ERROR: '+path+' is a symlink, please point to the real directory\n') 92 return -2 93 else: 94 sys.stderr.write('ERROR: '+path+' is not a directory!\n') 95 return -2 96 return 1 97 98def chroot_is_safe(path, failquiet=0): 99 """tests if path is a safe jail, not writable, no writable /etc/ and /lib, return 1 if all is OK""" 100 path = os.path.abspath(path) 101 retval = path_is_safe(path,failquiet) 102 if (retval < -1): 103 return retval 104 for subd in 'etc','usr','var','bin','dev','proc','sbin','sys': 105 retval = path_is_safe(path+'/'+subd,1) 106 if (retval < -1): 107 return retval 108 npath = os.path.dirname(path) 109 while (npath != '/'): 110 #print(npath) 111 retval = path_is_safe(npath,0) 112 if (retval != 1): 113# print('testing path='+npath+'returned '+str(retval)) 114 return retval 115 npath = os.path.dirname(npath) 116 return 1 117 118def test_suid_sgid(path): 119 """returns 1 if the file is setuid or setgid, returns 0 if it is not""" 120 statbuf = cachedlstat(path) 121 if (statbuf[stat.ST_MODE] & (stat.S_ISUID | stat.S_ISGID)): 122 return 1 123 return 0 124 125def gen_library_cache(jail): 126 if (sys.platform[:5] == 'linux'): 127 create_parent_path(jail,'/etc', 0, copy_permissions=0, allow_suid=0, copy_ownership=0) 128 os.system('ldconfig -r '+jail) 129 130 131def lddlist_libraries_linux(executable): 132 """returns a list of libraries that the executable depends on """ 133 retval = [] 134 p = subprocess.Popen('ldd '+executable, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) 135 line = p.stdout.readline() 136 #pd = os.popen3('ldd '+executable) 137 #line = pd[1].readline() 138 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 139 line = line.decode('utf-8', 'replace') 140 while (len(line)>0): 141 subl = line.split() 142 #print('parse line',subl,'with len',len(subl)) 143 if (len(subl)>0): 144 if (subl[0] == 'statically' and subl[1] == 'linked'): 145 return retval 146 elif (subl[0] == 'not' and subl[2] == 'dynamic' and subl[3] == 'executable'): 147 return retval 148 elif (subl[0] == 'linux-gate.so.1' or subl[0] == 'linux-vdso.so.1'): 149 pass 150 elif (len(subl)==4 and subl[2] == 'not' and subl[3] == 'found'): 151 pass 152 elif (len(subl)>=3): 153 if (os.path.exists(subl[2])): 154 retval += [subl[2]] 155 else: 156 print('ldd returns non existing library '+subl[2]+' for '+executable) 157 # on gentoo amd64 the last entry of ldd looks like '/lib64/ld-linux-x86-64.so.2 (0x0000002a95556000)' 158 elif (len(subl)>=1 and subl[0][0] == '/'): 159 if (os.path.exists(subl[0])): 160 retval += [subl[0]] 161 else: 162 print('ldd returns non existing library '+subl[0]) 163 else: 164 print('WARNING: failed to parse ldd output '+line[:-1]) 165 else: 166 print('WARNING: failed to parse ldd output '+line[:-1]) 167 #line = pd[1].readline() 168 line = p.stdout.readline() 169 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 170 line = line.decode('utf-8', 'replace') 171 return retval 172 173def lddlist_libraries_openbsd(executable): 174 """returns a list of libraries that the executable depends on """ 175 retval = [] 176 mode = 3 # openbsd 4 has new ldd output 177 p = subprocess.Popen('ldd '+executable, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) 178 line = p.stdout.readline() 179# pd = os.popen3('ldd '+executable) 180# line = pd[1].readline() 181 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 182 line = line.decode('utf-8', 'replace') 183 while (len(line)>0): 184 subl = line.split() 185 if (len(subl)>0): 186 if (subl[0] == executable+':'): 187 pass 188 elif (subl[0] == 'Start'): 189 if (len(subl)==7 and subl[6] == 'Name'): 190 mode = 4 191 pass 192 elif (len(subl)>=5): 193 if (mode == 3): 194 if (os.path.exists(subl[4])): 195 retval += [subl[4]] 196 else: 197 print('ldd returns non existing library '+subl[4]) 198 elif (mode == 4): 199 if (os.path.exists(subl[6])): 200 retval += [subl[6]] 201 else: 202 print('ldd returns non existing library '+subl[6]) 203 else: 204 print('unknown mode, please report this bug in jk_lib.py') 205 else: 206 print('WARNING: failed to parse ldd output '+line[:-1]) 207 else: 208 print('WARNING: failed to parse ldd output '+line[:-1]) 209 line = p.stdout.readline() 210 #line = pd[1].readline() 211 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 212 line = line.decode('utf-8', 'replace') 213 return retval 214 215def lddlist_libraries_freebsd(executable): 216 """returns a list of libraries that the executable depends on """ 217 retval = [] 218 p = subprocess.Popen('ldd '+executable, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) 219 line = p.stdout.readline() 220 #pd = os.popen3('ldd '+executable) 221 #line = pd[1].readline() 222 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 223 line = line.decode('utf-8', 'replace') 224 while (len(line)>0): 225 subl = line.split() 226 if (len(subl)>0): 227 if (len(subl)==1 and subl[0][:len(executable)+1] == executable+':'): 228 pass 229 elif (len(subl)>=6 and subl[2] == 'not' and subl[4] == 'dynamic'): 230 return retval 231 elif (len(subl)>=4): 232 if (os.path.exists(subl[2])): 233 retval += [subl[2]] 234 else: 235 print('ldd returns non existing library '+subl[2]) 236 else: 237 print('WARNING: failed to parse ldd output "'+line[:-1]+'"') 238 elif (line[:len(executable)+1] == executable+':'): 239 pass 240 else: 241 print('WARNING: failed to parse ldd output "'+line[:-1]+'"') 242 line = p.stdout.readline() 243 #line = pd[1].readline() 244 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 245 line = line.decode('utf-8', 'replace') 246 return retval 247 248def lddlist_libraries(executable): 249 if (sys.platform[:5] == 'linux'): 250 return lddlist_libraries_linux(executable) 251 elif (sys.platform[:7] == 'openbsd'): 252 return lddlist_libraries_openbsd(executable) 253 elif (sys.platform[:7] == 'freebsd'): 254 return lddlist_libraries_freebsd(executable) 255 elif (sys.platform[:5] == 'sunos'): 256 retval = lddlist_libraries_linux(executable) 257 retval += ['/lib/ld.so.1'] 258 return retval 259 else: 260 retval = lddlist_libraries_linux(executable) 261 retval += ['/usr/libexec/ld.so','/usr/libexec/ld-elf.so.1','/libexec/ld-elf.so.1'] 262 return retval 263 264def resolve_realpath(path, chroot='', include_file=0): 265 if (path=='/'): 266 return '/' 267 spath = split_path(path) 268 basename = '' 269 if (not include_file): 270 basename = spath[-1] 271 spath = spath[:-1] 272 ret = '/' 273 doscounter=0# a symlink loop may otherwise hang this script 274 #print('path' + path + 'spath' + spath) 275 for entry in spath: 276 ret = os.path.join(ret,entry) 277 #print('lstat' + ret) 278 sb = cachedlstat(ret) 279 if (stat.S_ISLNK(sb.st_mode)): 280 doscounter+=1 281 realpath = os.readlink(ret) 282 if (realpath[0]=='/'): 283 ret = os.path.normpath(chroot+realpath) 284 else: 285 tmp = os.path.normpath(os.path.join(os.path.dirname(ret),realpath)) 286 if (len(chroot)>0 and tmp[:len(chroot)]!=chroot): 287 sys.stderr.write('ERROR: symlink '+tmp+' points outside jail, ABORT\n') 288 raise Exception("Symlink points outside jail") 289 ret = tmp 290 return os.path.join(ret,basename) 291 292# os.path.realpath() seems to do the same: 293# NO: it cannot handle symlink resolving WITH a chroot 294def OLDresolve_realpath(path, chroot='', include_file=0): 295 """will return the same path that contains not a single symlink directory element""" 296 chrootlen=len(chroot) 297 if (include_file): 298 donepath = '' 299 todopath = path 300 else: 301 donepath = os.path.basename(path) 302 todopath = os.path.dirname(path) 303 doscounter=0 # a symlink loop may otherwise hang this script 304 while (todopath != '/' and doscounter != 100): 305 #print('todopath=' + todopath + 'donepath=' + donepath) 306 sb = cachedlstat(todopath) 307 if (stat.S_ISLNK(sb.st_mode)): 308 doscounter += 1 309 realpath = os.readlink(todopath) 310 if (realpath[0]=='/'): 311 todopath = chroot+realpath 312 if (todopath[-1:]=='/'): 313 todopath = todopath[:-1] 314 else: 315 tmp = os.path.normpath(os.path.join(os.path.dirname(todopath),realpath)) 316 if (chrootlen>0 and tmp[:chrootlen]!=chroot): 317 sys.stderr.write('ERROR: symlink '+tmp+' points outside jail, ABORT\n') 318 raise Exception("Symlink points outside jail") 319 todopath=tmp 320 else: 321 donepath = os.path.basename(todopath)+'/'+donepath 322 todopath = os.path.dirname(todopath) 323 sb=None 324 return '/'+donepath 325 326def copy_time_and_permissions(src, dst, be_verbose=0, allow_suid=0, copy_ownership=0): 327 # the caller should catch any exceptions! 328# similar to shutil.copymode(src, dst) but we do not copy any SUID bits 329 sbuf = os.stat(src) 330# in python 2.1 the return value is a tuple, not an object, st_mode is field 0 331# mode = stat.S_IMODE(sbuf.st_mode) 332 mode = stat.S_IMODE(sbuf[stat.ST_MODE]) 333 if (not allow_suid): 334 if (mode & (stat.S_ISUID | stat.S_ISGID)): 335 print('removing setuid and setgid permissions from '+dst) 336# print('setuid!!! mode='+str(mode)) 337 mode = (mode & ~stat.S_ISUID) & ~stat.S_ISGID 338# print('setuid!!! after mode='+str(mode)) 339# print('mode='+str(mode)) 340 os.utime(dst, (sbuf[stat.ST_ATIME], sbuf[stat.ST_MTIME])) 341 if (copy_ownership): 342 os.chown(dst, sbuf[stat.ST_UID], sbuf[stat.ST_GID]) 343 #WARNING: chmod must be AFTER chown to preserve setuid/setgid bits 344 os.chmod(dst, mode) 345 346 347def OLDreturn_existing_base_directory(path): 348 """This function tests if a directory exists, if not tries the parent etc. etc. until it finds a directory that exists""" 349 tmp = path 350 while (not os.path.exists(tmp) and not (tmp == '/' or tmp=='')): 351 tmp = os.path.dirname(tmp) 352 return tmp 353 354def OLD_create_parent_path(chroot, path, be_verbose=0, copy_permissions=1, allow_suid=0, copy_ownership=0): 355 """creates the directory and all its parents id needed. copy_ownership can only be used if copy permissions is also used""" 356 directory = path 357 if (directory[-1:] == '/'): 358 directory = directory[:-1] 359 # TODO, this function cannot yet handle if /lib in the jail is a symlink to something else 360 try: 361 chrootdirectory = resolve_realpath(chroot+directory,chroot) 362 if (os.path.exists(chrootdirectory)): 363 return 364 except OSError: 365 pass 366 tmp = return_existing_base_directory(chroot+directory) 367 oldindx = len(tmp)-len(chroot) 368 # find the first slash after the existing directories 369 indx = directory.find('/',oldindx+1) 370 if (indx == -1 ): 371 indx=len(directory) 372 while (indx != -1): 373 # avoid the /bla//bla pitfall 374 if (oldindx +1 == indx): 375 oldindx = indx 376 else: 377 try: 378 sb = cachedlstat(directory[:indx]) 379 except OSError as e: 380 _, strerror = e.args 381 sys.stderr.write('ERROR: failed to lstat('+directory[:indx]+'):'+strerror+'\n') 382 break 383 if (stat.S_ISLNK(sb.st_mode)): 384 # create the link, create the target, and then continue 385 realfile = os.readlink(directory[:indx]) 386 chrootname = resolve_realpath(chroot+directory[:indx],chroot) 387 if (be_verbose): 388 print('Creating symlink '+chrootname+' to '+realfile) 389 try: 390 os.symlink(realfile, chrootname) 391 except OSError as e: 392 errno, _ = e.args 393 if (errno == 17): # file exists 394 pass 395 else: 396 sys.stderr.write('ERROR: failed to create symlink '+chroot+directory[:indx]+'\n'); 397 if (realfile[0]=='/'): 398 create_parent_path(chroot, realfile, be_verbose, copy_permissions, allow_suid, copy_ownership) 399 else: 400 indx2 = string.rfind(directory[:indx],'/') 401# print('try' + directory[:indx2+1]+realfile) 402 create_parent_path(chroot, directory[:indx2+1]+realfile, be_verbose, copy_permissions, allow_suid, copy_ownership) 403 elif (stat.S_ISDIR(sb.st_mode)): 404 chrootname = resolve_realpath(chroot+directory[:indx],chroot) 405 if (be_verbose): 406 print('Creating directory '+chrootname) 407 os.mkdir(chrootname, dir_mode) 408 if (copy_permissions): 409 try: 410 copy_time_and_permissions(directory[:indx], chrootname, be_verbose, allow_suid, copy_ownership) 411 except OSError as e: 412 _, strerror = e.args 413 sys.stderr.write('ERROR: failed to copy time/permissions/owner from '+directory[:indx]+' to '+chrootname+': '+strerror+'\n') 414 oldindx = indx 415 if (indx==len(directory)): 416 indx=-1 417 else: 418 indx = directory.find('/',oldindx+1) 419 if (indx==-1): 420 indx=len(directory) 421 422def fix_double_slashes(instring): 423 outstring='' 424 slash=0 425 for i in instring: 426 if (slash==0 or i!= '/'): 427 outstring += i 428 if (i == '/'): 429 slash=1 430 else: 431 slash=0 432 return outstring 433 434def split_path(path): 435 ret = [] 436 next=fix_double_slashes(os.path.normpath(path)) 437 while (next != '/'): 438 ret.insert(0,os.path.basename(next)) 439 next=os.path.dirname(next) 440 return ret 441 442def join_path(spath): 443 if (len(spath)==0): 444 return '/' 445 ret = '' 446 for entry in spath: 447 ret += '/'+entry 448 return ret 449 450def create_parent_path(chroot,path,be_verbose=0, copy_permissions=1, allow_suid=0, copy_ownership=0): 451 #print('create_parent_path, path' + path + 'in chroot' + chroot) 452 # the first part of the function checks the already existing paths in the jail 453 # and follows any symlinks relative to the jail 454 spath = split_path(path) 455 existpath = chroot 456 i=0 457 while (i<len(spath)): 458 tmp1 = os.path.join(existpath,spath[i]) 459 if not os.path.exists(tmp1): 460 #print('tmp1 does not exist' + tmp1) 461 break 462 tmp = resolve_realpath(tmp1,chroot,1) 463 if not os.path.exists(tmp): 464 #print('tmp does not exist' + tmp) 465 break 466 #print('tmp exists:' + tmp) 467 existpath = tmp 468 i+=1 469 #print('existpath result:' + existpath + 'i=' + i) 470 # the second part of the function creates the missing parts in the jail 471 # according to the original directory names, including any symlinks 472 while (i<len(spath)): 473 origpath = join_path(spath[0:i+1]) 474 jailpath = os.path.join(existpath,spath[i]) 475 #print('origpath' + origpath + 'jailpath' + jailpath) 476 try: 477 sb = cachedlstat(origpath) 478 except OSError as e: 479 _, strerror = e.args 480 sys.stderr.write('ERROR: failed to lstat('+origpath+'):'+strerror+'\n') 481 return None 482 if (stat.S_ISDIR(sb.st_mode)): 483 if (be_verbose): 484 print('Create directory '+jailpath) 485 os.mkdir(jailpath, dir_mode) 486 if (copy_permissions): 487 try: 488 copy_time_and_permissions(origpath, jailpath, be_verbose, allow_suid, copy_ownership) 489 except OSError as e: 490 _, strerror = e.args 491 sys.stderr.write('ERROR: failed to copy time/permissions/owner from '+directory[:indx]+' to '+chrootname+': '+strerror+'\n') 492 elif (stat.S_ISLNK(sb.st_mode)): 493 realfile = os.readlink(origpath) 494 if (be_verbose): 495 print('Creating symlink '+jailpath+' to '+realfile) 496 os.symlink(realfile,jailpath) 497 if (realfile[0]=='/'): 498 jailpath = create_parent_path(chroot, realfile,be_verbose, copy_permissions, allow_suid, copy_ownership) 499 else: 500 tmp = os.path.normpath(os.path.join(os.path.dirname(jailpath),realfile)) 501 #print('symlink resolves to ' + tmp) 502 if (len(chroot)>0 and tmp[:len(chroot)]!=chroot): 503 sys.stderr.write('ERROR: symlink '+tmp+' points outside jail, ABORT\n') 504 raise Exception("Symlink points outside jail") 505 realfile = tmp[len(chroot):] 506 jailpath = create_parent_path(chroot, realfile,be_verbose, copy_permissions, allow_suid, copy_ownership) 507 existpath = jailpath 508 #print('new value for existpath is' + existpath) 509 i+=1 510 return existpath 511 512def copy_dir_with_permissions_and_owner(srcdir,dstdir,be_verbose=0): 513 # used to **move** home directories into the jail 514 #create directory dstdir 515 try: 516 if (be_verbose): 517 print('Creating directory'+dstdir) 518 os.mkdir(dstdir) 519 copy_time_and_permissions(srcdir, dstdir, be_verbose, allow_suid=0, copy_ownership=1) 520 except (IOError, OSError) as e: 521 _, strerror = e.args 522 sys.stderr.write('ERROR: copying directory and permissions '+srcdir+' to '+dstdir+': '+strerror+'\n') 523 return 0 524 for root, dirs, files in os.walk(srcdir): 525 for name in files: 526 if (be_verbose): 527 print('Copying '+root+'/'+name+' to '+dstdir+'/'+name) 528 try: 529 shutil.copyfile(root+'/'+name,dstdir+'/'+name) 530 copy_time_and_permissions(root+'/'+name, dstdir+'/'+name, be_verbose, allow_suid=0, copy_ownership=1) 531 except (IOError,OSError) as e: 532 _, strerror = e.args 533 sys.stderr.write('ERROR: copying file and permissions '+root+'/'+name+' to '+dstdir+'/'+name+': '+strerror+'\n') 534 return 0 535 for name in dirs: 536 move_dir_with_permissions_and_owner(root+'/'+name,dstdir+'/'+name,be_verbose) 537 return 1 538 539def move_dir_with_permissions_and_owner(srcdir,dstdir,be_verbose=0): 540 retval = copy_dir_with_permissions_and_owner(srcdir,dstdir,be_verbose) 541 if (retval == 1): 542 # remove the source directory 543 if (be_verbose==1): 544 print('Removing original home directory '+srcdir) 545 try: 546 shutil.rmtree(srcdir) 547 except (OSError, IOError) as e: 548 _, strerror = e.args 549 sys.stderr.write('ERROR: failed to remove '+srcdir+': '+strerror+'\n') 550 else: 551 print('Not everything was copied to '+dstdir+', keeping the old directory '+srcdir) 552 553def copy_with_permissions(src, dst, be_verbose=0, try_hardlink=1, allow_suid=0, retain_owner=0): 554 """copies/links the file and the permissions (and possibly ownership and setuid/setgid bits""" 555 do_normal_copy = 1 556 if (try_hardlink==1): 557 try: 558 os.link(src,dst) 559 do_normal_copy = 0 560 except: 561 print('Linking '+src+' to '+dst+' failed, will revert to copying') 562 pass 563 if (do_normal_copy == 1): 564 try: 565 shutil.copyfile(src,dst) 566 copy_time_and_permissions(src, dst, be_verbose, allow_suid=allow_suid, copy_ownership=retain_owner) 567 except (IOError, OSError) as e: 568 _, strerror = e.args 569 sys.stderr.write('ERROR: copying file and permissions '+src+' to '+dst+': '+strerror+'\n') 570 571def copy_device(chroot, path, be_verbose=1, retain_owner=0): 572 # perhaps the calling function should make sure the basedir exists 573 create_parent_path(chroot,os.path.dirname(path), be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=0) 574 chrootpath = resolve_realpath(chroot+path,chroot) 575 if (os.path.exists(chrootpath)): 576 print('Device '+chrootpath+' does exist already') 577 return 578 sb = os.stat(path) 579 try: 580 if (sys.platform[:5] == 'linux'): 581 major = sb.st_rdev / 256 #major = st_rdev divided by 256 (8bit reserved for the minor number) 582 minor = sb.st_rdev % 256 #minor = remainder of st_rdev divided by 256 583 elif (sys.platform == 'sunos5'): 584 if (sys.maxint == 2147483647): 585 major = sb.st_rdev / 262144 #major = st_rdev divided by 256 (18 bits reserved for the minor number) 586 minor = sb.st_rdev % 262144 #minor = remainder of st_rdev divided by 256 587 else: 588 #64 bit solaris has 32 bit minor/32bit major 589 major = sb.st_rdev / 2147483647 590 minor = sb.st_rdev % 2147483647 591 else: 592 major = sb.st_rdev / 256 #major = st_rdev divided by 256 593 minor = sb.st_rdev % 256 #minor = remainder of st_rdev divided by 256 594 if (stat.S_ISCHR(sb.st_mode)): 595 mode = 'c' 596 elif (stat.S_ISBLK(sb.st_mode)): 597 mode = 'b' 598 else: 599 print('WARNING, '+path+' is not a character or block device') 600 return 1 601 if (be_verbose==1): 602 print('Creating device '+chroot+path) 603 ret = os.spawnlp(os.P_WAIT, 'mknod','mknod', chrootpath, str(mode), str(major), str(minor)) 604 copy_time_and_permissions(path, chrootpath, allow_suid=0, copy_ownership=retain_owner) 605 except: 606 print('Failed to create device '+chrootpath+', this is a know problem with python 2.1') 607 print('use "ls -l '+path+'" to find out the mode, major and minor for the device') 608 print('use "mknod '+chrootpath+' mode major minor" to create the device') 609 print('use chmod and chown to set the permissions as found by ls -l') 610 611def copy_dir_recursive(chroot,dir,force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, allow_suid=0, retain_owner=0, handledfiles=[]): 612 """copies a directory and the permissions recursively, possibly with ownership and setuid/setgid bits""" 613 files2 = () 614 for entry in os.listdir(dir): 615 tmp = os.path.join(dir, entry) 616 try: 617 sbuf = cachedlstat(tmp) 618 if (stat.S_ISDIR(sbuf.st_mode)): 619 create_parent_path(chroot, tmp, be_verbose=be_verbose, copy_permissions=1, allow_suid=allow_suid, copy_ownership=retain_owner) 620 handledfiles = copy_dir_recursive(chroot,tmp,force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles) 621 else: 622 files2 += os.path.join(dir, entry), 623 except OSError as e: 624 sys.stderr.write('ERROR: failed to investigate source file '+tmp+': '+e.strerror+'\n') 625 handledfiles = copy_binaries_and_libs(chroot,files2,force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles) 626 return handledfiles 627# for root, dirs, files in os.walk(dir): 628# files2 = () 629# for name in files: 630# files2 += os.path.join(root, name), 631# handledfiles = copy_binaries_and_libs(chroot,files2,force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles) 632# for name in dirs: 633# tmp = os.path.join(root, name) 634# create_parent_path(chroot, tmp, be_verbose=be_verbose, copy_permissions=1, allow_suid=0, copy_ownership=retain_owner) 635# handledfiles = copy_dir_recursive(chroot,os.path.join(root, name),force_overwrite, be_verbose, check_libs, try_hardlink, retain_owner, handledfiles) 636# return handledfiles 637 638 639# there is a very tricky situation for this function: 640# suppose /srv/jail/opt/bin is a symlink to /usr/bin 641# try to lstat(/srv/jail/opt/bin/foo) and you get the result for /usr/bin/foo 642# so use resolve_realpath to find you want lstat(/srv/jail/usr/bin/foo) 643# 644def copy_binaries_and_libs(chroot, binarieslist, force_overwrite=0, be_verbose=0, check_libs=1, try_hardlink=1, allow_suid=0, retain_owner=0, try_glob_matching=0, handledfiles=[]): 645 """copies a list of executables and their libraries to the chroot""" 646 if (chroot[-1] == '/'): 647 chroot = chroot[:-1] 648 for file in binarieslist: 649 if (file in handledfiles): 650 continue 651 652 try: 653 sb = cachedlstat(file) 654 except OSError as e: 655 if (e.errno == 2): 656 if (try_glob_matching == 1): 657 ret = glob.glob(file) 658 if (len(ret)>0): 659 handledfiles = copy_binaries_and_libs(chroot, ret, force_overwrite, be_verbose, check_libs, try_hardlink=try_hardlink, retain_owner=retain_owner, try_glob_matching=0, handledfiles=handledfiles) 660 elif (be_verbose): 661 print('Source file(s) '+file+' do not exist') 662 elif (be_verbose): 663 print('Source file '+file+' does not exist') 664 else: 665 sys.stderr.write('ERROR: failed to investigate source file '+file+': '+e.strerror+'\n') 666 continue 667 # source file exists, resolve the chroot realfile 668 create_parent_path(chroot,os.path.dirname(file), be_verbose, copy_permissions=1, allow_suid=allow_suid, copy_ownership=retain_owner) 669 chrootrfile = resolve_realpath(os.path.normpath(chroot+'/'+file),chroot) 670 671 try: 672 chrootsb = cachedlstat(chrootrfile) 673 chrootfile_exists = 1 674 except OSError as e: 675 if (e.errno == 2): 676 chrootfile_exists = 0 677 else: 678 sys.stderr.write('ERROR: failed to investigate destination file '+chroot+file+': '+e.strerror+'\n') 679 680 if ((force_overwrite == 0) and chrootfile_exists and not stat.S_ISDIR(chrootsb.st_mode)): 681 if (be_verbose): 682 print(''+chrootrfile+' already exists, will not touch it') 683 else: 684 if (chrootfile_exists): 685 if (force_overwrite): 686 if (stat.S_ISREG(chrootsb.st_mode)): 687 if (be_verbose): 688 print('Destination file '+chrootrfile+' exists, will delete to force update') 689 try: 690 os.unlink(chrootrfile) 691 except OSError as e: 692 sys.stderr.write('ERROR: failed to delete '+chrootrfile+': '+e.strerror+'\ncannot force update '+chrootrfile+'\n') 693 # BUG: perhaps we can fix the permissions so we can really delete the file? 694 # but what permissions cause this error? 695 elif (stat.S_ISDIR(chrootsb.st_mode)): 696 print('Destination dir '+chrootrfile+' exists') 697 else: 698 if (stat.S_ISDIR(chrootsb.st_mode)): 699 pass 700 # for a directory we also should inspect all the contents, so we do not 701 # skip to the next item of the loop 702 else: 703 if (be_verbose): 704 print('Destination file '+chrootrfile+' exists') 705 continue 706 create_parent_path(chroot,os.path.dirname(file), be_verbose, copy_permissions=1, allow_suid=allow_suid, copy_ownership=retain_owner) 707 if (stat.S_ISLNK(sb.st_mode)): 708 realfile = os.readlink(file) 709 if (chrootfile_exists): 710 if (be_verbose): 711 print('Destination symlink '+chrootrfile+' exists, will delete to force update') 712 try: 713 os.unlink(chrootrfile) 714 except OSError as e: 715 sys.stderr.write('ERROR: failed to delete '+chrootrfile+': '+e.strerror+'\ncannot force update '+chrootrfile+'\n') 716 try: 717 print('Creating symlink '+chrootrfile+' to '+realfile) 718 os.symlink(realfile, chrootrfile) 719 except OSError: 720 # if the file exists already, check if it is correct 721 722 pass 723 handledfiles.append(file) 724 if (realfile[0] != '/'): 725 realfile = os.path.normpath(os.path.join(os.path.dirname(file),realfile)) 726 handledfiles = copy_binaries_and_libs(chroot, [realfile], force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles) 727 elif (stat.S_ISDIR(sb.st_mode)): 728 handledfiles = copy_dir_recursive(chroot,file,force_overwrite, be_verbose, check_libs, try_hardlink, allow_suid, retain_owner, handledfiles) 729 elif (stat.S_ISREG(sb.st_mode)): 730 if (try_hardlink): 731 print('Trying to link '+file+' to '+chrootrfile) 732 else: 733 print('Copying '+file+' to '+chrootrfile) 734 copy_with_permissions(file,chrootrfile,be_verbose, try_hardlink, allow_suid, retain_owner) 735 handledfiles.append(file) 736 elif (stat.S_ISCHR(sb.st_mode) or stat.S_ISBLK(sb.st_mode)): 737 copy_device(chroot, file, be_verbose, retain_owner) 738 else: 739 sys.stderr.write('Failed to find how to copy '+file+' into a chroot jail, please report to the Jailkit developers\n') 740# in python 2.1 the return value is a tuple, not an object, st_mode is field 0 741# mode = stat.S_IMODE(sbuf.st_mode) 742 mode = stat.S_IMODE(sb[stat.ST_MODE]) 743 if (check_libs and (file.find('lib') != -1 or file.find('.so') != -1 or (mode & (stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)))): 744 libs = lddlist_libraries(file) 745 handledfiles = copy_binaries_and_libs(chroot, libs, force_overwrite, be_verbose, 0, try_hardlink, handledfiles=handledfiles) 746 return handledfiles 747 748def config_get_option_as_list(cfgparser, sectionname, optionname): 749 """retrieves a comma separated option from the configparser and splits it into a list, returning an empty list if it does not exist""" 750 retval = [] 751 if (cfgparser.has_option(sectionname,optionname)): 752 inputstr = cfgparser.get(sectionname,optionname) 753 for tmp in inputstr.split(','): 754 retval += [tmp.strip()] 755 return retval 756 757def clean_exit(exitno,message,usagefunc,type='ERROR'): 758 print('') 759 print(type+': '+message) 760 usagefunc() 761 sys.exit(exitno) 762 763def test_numitem_exist(item,num,filename): 764 try: 765 fd = open(filename,'r') 766 except: 767 #print(''+filename+' does not exist') 768 return 0 769 line = fd.readline() 770 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 771 line = line.decode('utf-8', 'replace') 772 while (len(line)>0): 773 pwstruct = line.split(':') 774 #print('len pwstruct='+str(len(pwstruct))+' while looking for '+item) 775 if (len(pwstruct) > num and pwstruct[num] == item): 776 fd.close() 777 return 1 778 line = fd.readline() 779 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 780 line = line.decode('utf-8', 'replace') 781 return 0 782 783def test_user_exist(user, passwdfile): 784 return test_numitem_exist(user,0,passwdfile) 785 786def test_group_exist(group, groupfile): 787 return test_numitem_exist(group,0,groupfile) 788 789def init_passwd_and_group(chroot,users,groups,be_verbose=0): 790 if (chroot[-1] == '/'): 791 chroot = chroot[:-1] 792 create_parent_path(chroot,'/etc/', be_verbose, copy_permissions=0, allow_suid=0, copy_ownership=0) 793 if (sys.platform[4:7] == 'bsd'): 794 open(chroot+'/etc/passwd','a').close() 795 open(chroot+'/etc/spwd.db','a').close() 796 open(chroot+'/etc/pwd.db','a').close() 797 open(chroot+'/etc/master.passwd','a').close() 798 else: 799 if (not os.path.isfile(chroot+'/etc/passwd')): 800 fd2 = open(chroot+'/etc/passwd','w') 801 else: 802 # the chroot passwds file exists, check if any of the users exist already 803 fd2 = open(chroot+'/etc/passwd','r+') 804 line = fd2.readline() 805 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 806 line = line.decode('utf-8', 'replace') 807 while (len(line)>0): 808 pwstruct = line.split(':') 809 if (len(pwstruct) >=3): 810 if ((pwstruct[0] in users) or (pwstruct[2] in users)): 811 if (be_verbose): 812 print('user '+pwstruct[0]+' exists in '+chroot+'/etc/passwd') 813 try: 814 users.remove(pwstruct[0]) 815 except ValueError: 816 pass 817 try: 818 users.remove(pwstruct[2]) 819 except ValueError: 820 pass 821 line = fd2.readline() 822 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 823 line = line.decode('utf-8', 'replace') 824 fd2.seek(0,2) 825 if (len(users) > 0): 826 fd = open('/etc/passwd','r') 827 line = fd.readline() 828 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 829 line = line.decode('utf-8', 'replace') 830 while (len(line)>0): 831 pwstruct = line.split(':') 832 if (len(pwstruct) >=3): 833 if ((pwstruct[0] in users) or (pwstruct[2] in users)): 834 fd2.write(line) 835 if (be_verbose): 836 print('writing user '+pwstruct[0]+' to '+chroot+'/etc/passwd') 837 if (not pwstruct[3] in groups): 838 groups += [pwstruct[3]] 839 line = fd.readline() 840 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 841 line = line.decode('utf-8', 'replace') 842 fd.close() 843 fd2.close() 844 # do the same sequence for the group files 845 if (not os.path.isfile(chroot+'/etc/group')): 846 fd2 = open(chroot+'/etc/group','w') 847 else: 848 fd2 = open(chroot+'/etc/group','r+') 849 line = fd2.readline() 850 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 851 line = line.decode('utf-8', 'replace') 852 while (len(line)>0): 853 groupstruct = line.split(':') 854 if (len(groupstruct) >=2): 855 if ((groupstruct[0] in groups) or (groupstruct[2] in groups)): 856 if (be_verbose): 857 print('group '+groupstruct[0]+' exists in '+chroot+'/etc/group') 858 try: 859 groups.remove(groupstruct[0]) 860 except ValueError: 861 pass 862 try: 863 groups.remove(groupstruct[2]) 864 except ValueError: 865 pass 866 line = fd2.readline() 867 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 868 line = line.decode('utf-8', 'replace') 869 fd2.seek(0,2) 870 if (len(groups) > 0): 871 fd = open('/etc/group','r') 872 line = fd.readline() 873 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 874 line = line.decode('utf-8', 'replace') 875 while (len(line)>0): 876 groupstruct = line.split(':') 877 if (len(groupstruct) >=2): 878 if ((groupstruct[0] in groups) or (groupstruct[2] in groups)): 879 fd2.write(line) 880 if (be_verbose): 881 print('writing group '+groupstruct[0]+' to '+chroot+'/etc/group') 882 line = fd.readline() 883 if sys.version_info > (3, 0) and isinstance(line, bytes): #Python 3 884 line = line.decode('utf-8', 'replace') 885 fd.close() 886 fd2.close() 887 888def find_file_in_path(filename): 889 search_path = os.getenv('PATH') 890 paths = search_path.split(':') 891 for path in paths: 892 joined = os.path.join(path, filename) 893 if os.path.exists(joined): 894 return os.path.abspath(joined) 895 return None 896 897def find_files_in_path(paths): 898 paths2 = [] 899 for tmp in paths: 900 if (tmp[0] == '/'): 901 paths2.append(tmp) 902 else: 903 tmp2 = find_file_in_path(tmp) 904 if (tmp2): 905 paths2.append(tmp2) 906 return paths2 907