TUCoPS :: SunOS/Solaris :: patchadd.htm

SunOS patchadd bug
Vulnerability

    patchadd

Affected

    SunOS

Description

    Jonathan  Fortin  found  following.   He  was  playing around with
    patchadd and the bug was found when he issued a

        truss -f -o patch.log patchadd patch

    where  patch  was  a  tarball  and  then patchadd omitted an error
    because of it being a tarball, so then when Jonathan went  through
    the  debug  output,  he  found  out  that there was a serious race
    condition vulnerability.

        Line    Pid	exec call
        
         105:   12869:  open64("/tmp/sh12869.1", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
         136:  12869:  open64("/tmp/sh12869.2", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
         481:  12869:  open64("/tmp/sh12869.3", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
        
         file "/tmp/sh12869.1":
        
         105: 12869:  open64("/tmp/sh12869.1", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
         106: 12869:  write(3, "\n U s a g e :   p a t c".., 482)     = 482
         107: 12869:  close(3)
        
         file "/tmp/sh12869.2":
        
         136: 12869:  open64("/tmp/sh12869.2", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
         137: 12869:  write(3, " m a i l =\n i n s t a n".., 145)     = 145
         138: 12869:  close(3)
        
         file "/tmp/sh12869.3:
        
        481: 12869:  open64("/tmp/sh12869.3", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
        482: 12869:  close(61)                                       Err#9 EBADF
        483: 12869:  fcntl(3, F_DUPFD, 0x0000003D)                   = 61
        484: 12869:  close(3)

    When  patchadd  is  executed,  It  creates a temporary file called
    "/tmp/sh<pidofpatchadd>.1"    ,    "/tmp/sh<pidofpatchadd>.2     ,
    "/tmp/sh<pidofpatchadd>.3   and assigns  them mode  666 then  gets
    unlink'd  upon  exit.A  vulnerability  exist  in patchadd, a patch
    utility shipped  with Solaris,  where as  if an  attacker predicts
    the correct pid of the  next process before execution of  patchadd
    by another user or If he creates a fiew hundred symlinks to  brute
    force the pid before execution of patchadd, he can with a symbolic
    link pointing to a specific key system file, overwrite contents of
    the file, he can do up to 3 file simultaneously, and user will  be
    able to  do his  own modifications  to this  file since  this file
    would  have  world-write  permissions  resulting  in a increase of
    privilege and host compromise.

    Exploit:
    1. Email admin telling him theirs a new patch out there that needs
       to be installed.
    2.  Create  a  perl/C  script  that  will  copy  /etc/passwd   and
       /etc/shadow to a hidden file that you will want to be  appended
       to /etc/shadow/passwd later on, get the next current  available
       process, create 2 symlinks and  when the current process id  is
       taken, then stat for /etc/passwd and /etc/shadow to be 666,  if
       not avail, do it again, when avail, append a user with id 0  no
       password to those hidden files, then those files will  truncate
       /etc/passwd and /etc/shadow then  will be appended to  them and
       send ya an email to login and take advantage!
    3. su trojand_user
    4. #

Solution

    Sun  Microsystems  does  recommend  to  only  install  patches  at
    single-user mode  (runlevel S).   So no  other possibly  malicious
    user can exploit this ksh behaviour.

    Since patchadd is a script the bug it pretty easy to fix, Sun does
    intend to release patches.  So here is a set of diffs to  patchadd
    for those that really can't wait.  Please note these are  personal
    diffs and  they do  not reflect  the official  opinion of those in
    Sun responsible for the patchadd  command and may not reflect  the
    fix that is eventually released.

    *** patchadd.orig	Wed Dec 20 11:21:38 2000
    --- patchadd	Wed Dec 20 11:21:42 2000
    ***************
    *** 161,182 ****
      function set_globals
      {
    
    ! 	EXISTFILES=/tmp/existfiles.$$
    ! 	PATCHFILES=/tmp/patchfiles.$$
    ! 	PKGCOFILE=/tmp/pkgchk.out.$$
    ! 	VALERRFILE=/tmp/valerr.$$
    ! 	VALWARNFILE=/tmp/valwarn.$$
    ! 	ADMINTFILE=/tmp/admin.tmp.$$
    ! 	ADMINFILE=/tmp/admin.$$
    ! 	LOGFILE=/tmp/pkgaddlog.$$
    ! 	TMP_ARCHIVE=/tmp/TmpArchive.$$
    ! 	TMP_FILELIST=/tmp/FileList.$$
    ! 	TMP_LIB_DIR=/tmp/TmpLibDir.$$
    ! 	INSTPATCHES_FILE=/tmp/MyShowrevFile.$$
    ! 	PARAMS_FILE=/tmp/ParamsFile.$$
    ! 	RESPONSE_FILE=/tmp/response.$$
    ! 	TEMP_REMOTE=/tmp/temp_remote.$$
    
  	    Obsoletes=
  	    Incompat=
  	    Requires=
    --- 161,192 ----
      function set_globals
      {
    
    ! 	if [ -z "$WORKDIR" ]; then
    ! 		safedir=${TMPDIR:-/tmp}/patchadd${RANDOM}$$
    ! 		mkdir -m 700 $safedir
    ! 		if [ $? != 0 ]; then
    ! 			exit 1;
    ! 		fi
    ! 		WORKDIR=$safedir
    ! 		unset safedir
    ! 	fi
    
    + 	EXISTFILES=${WORKDIR}/existfiles.$$
    + 	PATCHFILES=${WORKDIR}/patchfiles.$$
    + 	PKGCOFILE=${WORKDIR}/pkgchk.out.$$
    + 	VALERRFILE=${WORKDIR}/valerr.$$
    + 	VALWARNFILE=${WORKDIR}/valwarn.$$
    + 	ADMINTFILE=${WORKDIR}/admin.tmp.$$
    + 	ADMINFILE=${WORKDIR}/admin.$$
    + 	LOGFILE=${WORKDIR}/pkgaddlog.$$
    + 	TMP_ARCHIVE=${WORKDIR}/TmpArchive.$$
    + 	TMP_FILELIST=${WORKDIR}/FileList.$$
    + 	TMP_LIB_DIR=${WORKDIR}/TmpLibDir.$$
    + 	INSTPATCHES_FILE=${WORKDIR}/MyShowrevFile.$$
    + 	PARAMS_FILE=${WORKDIR}/ParamsFile.$$
    + 	RESPONSE_FILE=${WORKDIR}/response.$$
    + 	TEMP_REMOTE=${WORKDIR}/temp_remote.$$
    +
  	    Obsoletes=
  	    Incompat=
  	    Requires=
    ***************
    *** 305,314 ****
  	    fi
    
  	    $RM -f $INSTPATCHES_FILE
    - 	$RM -f /tmp/*.$$.1
    - 	$RM -f /tmp/archive.cpio*
    - 	$RM -fr /tmp/*.$$
  	    $RM -f $patchFileStripped
      }
    
      #
    --- 315,322 ----
  	    fi
    
  	    $RM -f $INSTPATCHES_FILE
  	    $RM -f $patchFileStripped
    + 	$RM -rf ${WORKDIR}
      }
    
      #
    ***************
    *** 435,441 ****
  			    /usr/bin/gettext "The postpatch script exited with return code $retcode.\n"
  			    if [[ "$isapplied" = "no" ]]
  			    then
    ! 				$CP $1/$2/log /tmp/log.$2
  				    /usr/bin/gettext "Backing out patch:\n"
  				    cd $3
  				    if [[ "$ROOTDIR" != "/" ]]
    --- 443,449 ----
  			    /usr/bin/gettext "The postpatch script exited with return code $retcode.\n"
  			    if [[ "$isapplied" = "no" ]]
  			    then
    ! 				$CP $1/$2/log ${WORKDIR}/log.$2
  				    /usr/bin/gettext "Backing out patch:\n"
  				    cd $3
  				    if [[ "$ROOTDIR" != "/" ]]
    ***************
    *** 444,450 ****
  				    else
  					    /usr/sbin/patchrm $2
  				    fi
    ! 				/usr/bin/gettext "See /tmp/log.$2 for more details.\n"
  			    else
  				    /usr/bin/gettext "Not backing out patch because this is a re-installation.\nThe system may be in an unstable state!\nSee $1/$2/log for more details.\n"  |tee -a $1/$2/log
  			    fi
    --- 452,458 ----
  				    else
  					    /usr/sbin/patchrm $2
  				    fi
    ! 				/usr/bin/gettext "See ${WORKDIR}/log.$2 for more details.\n"
  			    else
  				    /usr/bin/gettext "Not backing out patch because this is a re-installation.\nThe system may be in an unstable state!\nSee $1/$2/log for more details.\n"  |tee -a $1/$2/log
  			    fi
    ***************
    *** 1527,1533 ****
      #
      function check_for_symbolic_link
      {
    ! 	$RM -f /tmp/symlink.$$ > /dev/null 2>&1
  	    olddir=$(pwd)
  	    cd $patchdir
  	    for ii in * X
    --- 1535,1541 ----
      #
      function check_for_symbolic_link
      {
    ! 	$RM -f ${WORKDIR}/symlink.$$ > /dev/null 2>&1
  	    olddir=$(pwd)
  	    cd $patchdir
  	    for ii in * X
    ***************
    *** 1551,1562 ****
  		    symlinks=
  		    symlinks=$($SED -n '/^[^ 	]*[ 	]*s[ 	]/p' $1/$2/$ii/pkgmap)
  		    if [[ "$symlinks" != "" ]]; then
    ! 			/usr/bin/gettext "Symbolic link in package $ii.\n" >> /tmp/symlink.$$
  		    fi
  	    done
    ! 	if [[ -s /tmp/symlink.$$ ]]
  	    then
    ! 		cat /tmp/symlink.$$
  		    /usr/bin/gettext "Symbolic links cannot be part of a patch.\n"
  		    patch_quit 13 "no"
  		    return 0
    --- 1559,1570 ----
  		    symlinks=
  		    symlinks=$($SED -n '/^[^ 	]*[ 	]*s[ 	]/p' $1/$2/$ii/pkgmap)
  		    if [[ "$symlinks" != "" ]]; then
    ! 			/usr/bin/gettext "Symbolic link in package $ii.\n" >> ${WORKDIR}/symlink.$$
  		    fi
  	    done
    ! 	if [[ -s ${WORKDIR}/symlink.$$ ]]
  	    then
    ! 		cat ${WORKDIR}/symlink.$$
  		    /usr/bin/gettext "Symbolic links cannot be part of a patch.\n"
  		    patch_quit 13 "no"
  		    return 0
    ***************
    *** 1840,1848 ****
  		    return
  	    fi
    
    ! 	pkgfiles=/tmp/pkgfiles.$$
    ! 	resfiles=/tmp/resolvedfiles.$$
    ! 	macrofiles=/tmp/pkgmacros.$$
  	    pkginst=
  	    pkginfofile=
  	    patchpkg=
    --- 1848,1856 ----
  		    return
  	    fi
    
    ! 	pkgfiles=${WORKDIR}/pkgfiles.$$
    ! 	resfiles=${WORKDIR}/resolvedfiles.$$
    ! 	macrofiles=${WORKDIR}/pkgmacros.$$
  	    pkginst=
  	    pkginfofile=
  	    patchpkg=
    ***************
    *** 2343,2350 ****
  			    /usr/bin/gettext "Pkgadd of $i package failed with error code $pkgadderr.\n" |tee -a $1/$2/log
  			    if [ "$isapplied" = "no" ]
  			    then
    ! 				/usr/bin/gettext "See /tmp/log.$2 for reason for failure.\n"
    ! 				$CP $1/$2/log /tmp/log.$2
  				    /usr/bin/gettext "Backing out patch:\n"
  				    cd $3
  				    if [ "$ROOTDIR" != "/" ]
    --- 2351,2358 ----
  			    /usr/bin/gettext "Pkgadd of $i package failed with error code $pkgadderr.\n" |tee -a $1/$2/log
  			    if [ "$isapplied" = "no" ]
  			    then
    ! 				/usr/bin/gettext "See ${WORKDIR}/log.$2 for reason for failure.\n"
    ! 				$CP $1/$2/log ${WORKDIR}/log.$2
  				    /usr/bin/gettext "Backing out patch:\n"
  				    cd $3
  				    if [ "$ROOTDIR" != "/" ]
    ***************
    *** 2988,2998 ****
  			    $FIND . -print > $TMP_FILELIST
    
  			    cd $ROOTDIR
    ! 			cpio -oL -O /tmp/archive.cpio < $EXISTFILES >/dev/null 2>&1
  			    exit_code=$?
    
  			    cd $TMP_ARCHIVE
    ! 			cpio -oAL -O /tmp/archive.cpio < $TMP_FILELIST >/dev/null 2>&1
  			    exit_code=exit_code+$?
    
  			    cd $ROOTDIR
    --- 2996,3006 ----
  			    $FIND . -print > $TMP_FILELIST
    
  			    cd $ROOTDIR
    ! 			cpio -oL -O ${WORKDIR}/archive.cpio < $EXISTFILES >/dev/null 2>&1
  			    exit_code=$?
    
  			    cd $TMP_ARCHIVE
    ! 			cpio -oAL -O ${WORKDIR}/archive.cpio < $TMP_FILELIST >/dev/null 2>&1
  			    exit_code=exit_code+$?
    
  			    cd $ROOTDIR
    ***************
    *** 3022,3028 ****
  			    then
  				    compress $archive_path/archive.cpio
  			    else
    ! 				compress /tmp/archive.cpio
  			    fi
  			    if [ $? = 0 ]
  			    then
    --- 3030,3036 ----
  			    then
  				    compress $archive_path/archive.cpio
  			    else
    ! 				compress ${WORKDIR}/archive.cpio
  			    fi
  			    if [ $? = 0 ]
  			    then
    ***************
    *** 3035,3041 ****
  		    fi
  		    if [ "$isapplied" = "yes" ]
  		    then
    ! 			$CP /tmp/archive.cpio* $1/$2/save
  		    fi
  		    chmod 600 $archive_path/archive.cpio*
  		    $TOUCH $1/$2/.oldfilessaved
    --- 3043,3049 ----
  		    fi
  		    if [ "$isapplied" = "yes" ]
  		    then
    ! 			$CP ${WORKDIR}/archive.cpio* $1/$2/save
  		    fi
  		    chmod 600 $archive_path/archive.cpio*
  		    $TOUCH $1/$2/.oldfilessaved
    ***************
    *** 3057,3063 ****
  	    then
  		    $MD -m 750 -p $1/$2
  	    fi
    ! 	$MV -f /tmp/ACTION.$PatchNum $1/$2 >/dev/null 2>&1
  	    $CP -p README.$2 $1/$2 >/dev/null 2>&1
    
  	    # Note the following line should be removed for 2.7.
    --- 3065,3071 ----
  	    then
  		    $MD -m 750 -p $1/$2
  	    fi
    ! 	$MV -f ${WORKDIR}/ACTION.$PatchNum $1/$2 >/dev/null 2>&1
  	    $CP -p README.$2 $1/$2 >/dev/null 2>&1
    
  	    # Note the following line should be removed for 2.7.
    ***************
    *** 3092,3102 ****
  	    else
  		    if [[ "$PATCH_UNDO_ARCHIVE" != "none" ]]
  		    then
    ! 			$CP /tmp/archive.cpio* $PATCH_UNDO_ARCHIVE/$2
  		    else
    ! 			$CP /tmp/archive.cpio* $1/$2/save
  		    fi
    ! 		$RM -f /tmp/archive.cpio*
  		    /usr/bin/gettext "Patchadd Interrupted.\n" >> $1/$2/log
  	    fi
  	    patch_quit 12 "yes"
    --- 3100,3110 ----
  	    else
  		    if [[ "$PATCH_UNDO_ARCHIVE" != "none" ]]
  		    then
    ! 			$CP ${WORKDIR}/archive.cpio* $PATCH_UNDO_ARCHIVE/$2
  		    else
    ! 			$CP ${WORKDIR}/archive.cpio* $1/$2/save
  		    fi
    ! 		$RM -f ${WORKDIR}/archive.cpio*
  		    /usr/bin/gettext "Patchadd Interrupted.\n" >> $1/$2/log
  	    fi
  	    patch_quit 12 "yes"
    ***************
    *** 3119,3125 ****
  	    fi
  	    if [[ "$isapplied" = "yes" ]]
  	    then
    ! 		$RM -f /tmp/archive.cpio*
  	    fi
  	    patch_quit 12 "yes"
      }
    --- 3127,3133 ----
  	    fi
  	    if [[ "$isapplied" = "yes" ]]
  	    then
    ! 		$RM -f ${WORKDIR}/archive.cpio*
  	    fi
  	    patch_quit 12 "yes"
      }
    ***************
    *** 3132,3138 ****
      function trap_notinstalled
      {
  	    /usr/bin/gettext "Interrupt signal detected. Patch not installed.\n"
    ! 	$RM -fr /tmp/*.$$
  	    $RM -f $INSTPATCHES_FILE
  	    if [[ "$isapplied" = "no" ]]
  	    then
    --- 3140,3146 ----
      function trap_notinstalled
      {
  	    /usr/bin/gettext "Interrupt signal detected. Patch not installed.\n"
    ! 	$RM -fr ${WORKDIR}/*.$$
  	    $RM -f $INSTPATCHES_FILE
  	    if [[ "$isapplied" = "no" ]]
  	    then
    ***************
    *** 3411,3417 ****
  	    typeset -i insPs=0
    
  	    patchFile=""
    ! 	patchFileStripped=/tmp/patchDBstripped.$$
    
  	    if [[ "$validate" = "no" ]]
  	    then
    --- 3419,3425 ----
  	    typeset -i insPs=0
    
  	    patchFile=""
    ! 	patchFileStripped=${WORKDIR}/patchDBstripped.$$
    
  	    if [[ "$validate" = "no" ]]
  	    then
    ***************
    *** 3796,3802 ****
          else
  		    if [[ "$netImage" = "boot" ]]
  		    then
    !         	backout_dir=$ROOTDIR/tmp
  		    else
          	    backout_dir=$PKGDB
  		    fi
    --- 3804,3810 ----
          else
  		    if [[ "$netImage" = "boot" ]]
  		    then
    !         	backout_dir=$ROOTDIR${WORKDIR}
  		    else
          	    backout_dir=$PKGDB
  		    fi
    ***************
    *** 3850,3856 ****
  	    dryrunExit=
  	    dryrunDir=
    
    ! 	dryrunDir="/tmp/$PatchNum.$$"
    
  	    if [[ ! -d "$dryrunDir"  || "$1" != "0" ]]; then
  		    dryrunFailure="yes"
    --- 3858,3864 ----
  	    dryrunExit=
  	    dryrunDir=
    
    ! 	dryrunDir="${WORKDIR}/$PatchNum.$$"
    
  	    if [[ ! -d "$dryrunDir"  || "$1" != "0" ]]; then
  		    dryrunFailure="yes"
    ***************
    *** 3955,3967 ****
  		    restore_net_image
  	    fi
    
    ! 	# The .../Boot/.tmp_proto/root needs to be re-mapped to .../Boot/tmp in order
  	    # for the boot image to be patched successfully.
    
  	    if [[ -z "$RE_MINIROOT_PATCH" ]]; then
    ! 		$MOUNT -F lofs -O $ROOTDIR/tmp $ROOTDIR/mnt
    ! 		$MOUNT -F lofs -O $ROOTDIR/.tmp_proto $ROOTDIR/tmp
    ! 		$MOUNT -F lofs -O $ROOTDIR/mnt/root/var $ROOTDIR/tmp/root/var
  	    fi
    
  	    # At this point patchadd thinks the net install image is just like
    --- 3963,3975 ----
  		    restore_net_image
  	    fi
    
    ! 	# The .../Boot/.tmp_proto/root needs to be re-mapped to .../Boot${WORKDIR} in order
  	    # for the boot image to be patched successfully.
    
  	    if [[ -z "$RE_MINIROOT_PATCH" ]]; then
    ! 		$MOUNT -F lofs -O $ROOTDIR${WORKDIR} $ROOTDIR/mnt
    ! 		$MOUNT -F lofs -O $ROOTDIR/.tmp_proto $ROOTDIR${WORKDIR}
    ! 		$MOUNT -F lofs -O $ROOTDIR/mnt/root/var $ROOTDIR${WORKDIR}/root/var
  	    fi
    
  	    # At this point patchadd thinks the net install image is just like
    ***************
    *** 3983,3990 ****
  	    fi
    
  	    if [[ -z "$RE_MINIROOT_PATCH" ]]; then
    ! 		$UMOUNT $ROOTDIR/tmp/root/var
    ! 		$UMOUNT $ROOTDIR/tmp
  		    $UMOUNT $ROOTDIR/mnt
  	    fi
      }
    --- 3991,3998 ----
  	    fi
    
  	    if [[ -z "$RE_MINIROOT_PATCH" ]]; then
    ! 		$UMOUNT $ROOTDIR${WORKDIR}/root/var
    ! 		$UMOUNT $ROOTDIR${WORKDIR}
  		    $UMOUNT $ROOTDIR/mnt
  	    fi
      }
    ***************
    *** 4168,4176 ****
    
          if [[ $pkgadd_code == 5 ]]  # administration
          then
    ! 		mv $LOGFILE /var/tmp/$PatchNum.log.$$ > /dev/null 2>&1
  		    [ $? = 0 ] && ! 			/usr/bin/gettext "\nPkgadd failed. See /var/tmp/$PatchNum.log.$$ for details\n"
    
  		    remove_patch_meta_data "$pkg"
  		    [ -n "$pkgsAlreadyInstalled" ] && remove_patch
    --- 4176,4184 ----
    
          if [[ $pkgadd_code == 5 ]]  # administration
          then
    ! 		mv $LOGFILE /var${WORKDIR}/$PatchNum.log.$$ > /dev/null 2>&1
  		    [ $? = 0 ] && ! 			/usr/bin/gettext "\nPkgadd failed. See /var${WORKDIR}/$PatchNum.log.$$ for details\n"
    
  		    remove_patch_meta_data "$pkg"
  		    [ -n "$pkgsAlreadyInstalled" ] && remove_patch
    ***************
    *** 4188,4196 ****
  		    fi
          elif [[ $pkgadd_code != 0 ]]
          then
    ! 		mv $LOGFILE /var/tmp/$PatchNum.log.$$ > /dev/null 2>&1
  		    [ $? = 0 ] && ! 			/usr/bin/gettext "\nPkgadd failed. See /var/tmp/$PatchNum.log.$$ for details\n"
    
  		    # If there are more pkgs in the list skip them
  		    # since this patch will not be installed.
    --- 4196,4204 ----
  		    fi
          elif [[ $pkgadd_code != 0 ]]
          then
    ! 		mv $LOGFILE /var${WORKDIR}/$PatchNum.log.$$ > /dev/null 2>&1
  		    [ $? = 0 ] && ! 			/usr/bin/gettext "\nPkgadd failed. See /var${WORKDIR}/$PatchNum.log.$$ for details\n"
    
  		    # If there are more pkgs in the list skip them
  		    # since this patch will not be installed.
    ***************
    *** 4321,4331 ****
  				    if [[ "$firstTimeThru" = "yes" ]]
  				    then
  					    if [[ "$PKGADD_DEBUG" = "yes" ]]; then
    ! 						pkgadd -v -D /tmp/$PatchNum.$$ -S -n -a $ADMINTFILE   					  		$MOPTION -r $RESPONSE_FILE.1 -R $ROOTDIR   							-d . $pkglist
  					    else
    ! 						pkgadd -D /tmp/$PatchNum.$$ -S -n -a $ADMINTFILE   					  		$MOPTION -r $RESPONSE_FILE.1   			    	  		-R $ROOTDIR -d . $pkglist   							1>>$LOGFILE </dev/null 2>&1
    --- 4329,4339 ----
  				    if [[ "$firstTimeThru" = "yes" ]]
  				    then
  					    if [[ "$PKGADD_DEBUG" = "yes" ]]; then
    ! 						pkgadd -v -D ${WORKDIR}/$PatchNum.$$ -S -n -a $ADMINTFILE   					  		$MOPTION -r $RESPONSE_FILE.1 -R $ROOTDIR   							-d . $pkglist
  					    else
    ! 						pkgadd -D ${WORKDIR}/$PatchNum.$$ -S -n -a $ADMINTFILE   					  		$MOPTION -r $RESPONSE_FILE.1   			    	  		-R $ROOTDIR -d . $pkglist   							1>>$LOGFILE </dev/null 2>&1

TUCoPS is optimized to look best in Firefox® on a widescreen monitor (1440x900 or better).
Site design & layout copyright © 1986-2024 AOH