CVE-2013-4547是Nginx出现过的一个解析漏洞,

官方的补丁打在了_ngx_http_parse.c_上

    --- src/http/ngx_http_parse.c
    +++ src/http/ngx_http_parse.c
    @@ -617,6 +617,7 @@ ngx_http_parse_request_line(ngx_http_req
                 default:
                     r->space_in_uri = 1;
                     state = sw_check_uri;
    +                p--;
                     break;
                 }
                 break;
    @@ -670,6 +671,7 @@ ngx_http_parse_request_line(ngx_http_req
                 default:
                     r->space_in_uri = 1;
                     state = sw_uri;
    +                p--;
                     break;
                 }
                 break;

使用gdb调试nginx,将断点下在/nginx-1.5.6/src/http/ngx_http_parse.cngx_http_parse_request_line上,

发送了测试payload

GET /a.jpg /0.php HTTP/1.1
Host: 127.0.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

/a.jpg /0.php中的空格是hex 00,非真正的空格

传入的http请求只接收到了/a.jpg

ngx_http_parse_request_line传入了两个ngx_http_request_t类和ngx_buf_t类的指针,ngx_http_request_t在以下文件中定义了一个别名

文件: /src/http/ngx_http_request.h

typedef struct ngx_http_request_s     ngx_http_request_t;

ngx_http_request_s的结构成员

struct ngx_http_request_s {
    uint32_t                          signature;         /* "HTTP" */

    ngx_connection_t                 *connection;

    void                            **ctx;
    void                            **main_conf;
    void                            **srv_conf;
    void                            **loc_conf;

    ngx_http_event_handler_pt         read_event_handler;
    ngx_http_event_handler_pt         write_event_handler;

#if (NGX_HTTP_CACHE)
    ngx_http_cache_t                 *cache;
#endif

    ngx_http_upstream_t              *upstream;
    ngx_array_t                      *upstream_states;
                                         /* of ngx_http_upstream_state_t */

    ngx_pool_t                       *pool;
    ngx_buf_t                        *header_in;

    ngx_http_headers_in_t             headers_in;
    ngx_http_headers_out_t            headers_out;

    ngx_http_request_body_t          *request_body;

    time_t                            lingering_time;
    time_t                            start_sec;
    ngx_msec_t                        start_msec;

    ngx_uint_t                        method;
    ngx_uint_t                        http_version;

    ngx_str_t                         request_line;
    ngx_str_t                         uri;
    ngx_str_t                         args;
    ngx_str_t                         exten;
    ngx_str_t                         unparsed_uri;

    ngx_str_t                         method_name;
    ngx_str_t                         http_protocol;

    ngx_chain_t                      *out;
    ngx_http_request_t               *main;
    ngx_http_request_t               *parent;
    ngx_http_postponed_request_t     *postponed;
    ngx_http_post_subrequest_t       *post_subrequest;
    ngx_http_posted_request_t        *posted_requests;

    ngx_int_t                         phase_handler;
    ngx_http_handler_pt               content_handler;
    ngx_uint_t                        access_code;

    ngx_http_variable_value_t        *variables;

#if (NGX_PCRE)
    ngx_uint_t                        ncaptures;
    int                              *captures;
    u_char                           *captures_data;
#endif

    size_t                            limit_rate;
    size_t                            limit_rate_after;

    /* used to learn the Apache compatible response length without a header */
    size_t                            header_size;

    off_t                             request_length;

    ngx_uint_t                        err_status;

    ngx_http_connection_t            *http_connection;
#if (NGX_HTTP_SPDY)
    ngx_http_spdy_stream_t           *spdy_stream;
#endif

    ngx_http_log_handler_pt           log_handler;

    ngx_http_cleanup_t               *cleanup;

    unsigned                          subrequests:8;
    unsigned                          count:8;
    unsigned                          blocked:8;

    unsigned                          aio:1;

    unsigned                          http_state:4;

    /* URI with "/." and on Win32 with "//" */
    unsigned                          complex_uri:1;

    /* URI with "%" */
    unsigned                          quoted_uri:1;

    /* URI with "+" */
    unsigned                          plus_in_uri:1;

    /* URI with " " */
    unsigned                          space_in_uri:1;

    unsigned                          invalid_header:1;

    unsigned                          add_uri_to_alias:1;
    unsigned                          valid_location:1;
    unsigned                          valid_unparsed_uri:1;
    unsigned                          uri_changed:1;
    unsigned                          uri_changes:4;

    unsigned                          request_body_in_single_buf:1;
    unsigned                          request_body_in_file_only:1;
    unsigned                          request_body_in_persistent_file:1;
    unsigned                          request_body_in_clean_file:1;
    unsigned                          request_body_file_group_access:1;
    unsigned                          request_body_file_log_level:3;

    unsigned                          subrequest_in_memory:1;
    unsigned                          waited:1;

#if (NGX_HTTP_CACHE)
    unsigned                          cached:1;
#endif

#if (NGX_HTTP_GZIP)
    unsigned                          gzip_tested:1;
    unsigned                          gzip_ok:1;
    unsigned                          gzip_vary:1;
#endif

    unsigned                          proxy:1;
    unsigned                          bypass_cache:1;
    unsigned                          no_cache:1;

    /*
     * instead of using the request context data in
     * ngx_http_limit_conn_module and ngx_http_limit_req_module
     * we use the single bits in the request structure
     */
    unsigned                          limit_conn_set:1;
    unsigned                          limit_req_set:1;

#if 0
    unsigned                          cacheable:1;
#endif

    unsigned                          pipeline:1;
    unsigned                          chunked:1;
    unsigned                          header_only:1;
    unsigned                          keepalive:1;
    unsigned                          lingering_close:1;
    unsigned                          discard_body:1;
    unsigned                          internal:1;
    unsigned                          error_page:1;
    unsigned                          ignore_content_encoding:1;
    unsigned                          filter_finalize:1;
    unsigned                          post_action:1;
    unsigned                          request_complete:1;
    unsigned                          request_output:1;
    unsigned                          header_sent:1;
    unsigned                          expect_tested:1;
    unsigned                          root_tested:1;
    unsigned                          done:1;
    unsigned                          logged:1;

    unsigned                          buffered:4;

    unsigned                          main_filter_need_in_memory:1;
    unsigned                          filter_need_in_memory:1;
    unsigned                          filter_need_temporary:1;
    unsigned                          allow_ranges:1;

#if (NGX_STAT_STUB)
    unsigned                          stat_reading:1;
    unsigned                          stat_writing:1;
#endif

    /* used to parse HTTP headers */

    ngx_uint_t                        state;

    ngx_uint_t                        header_hash;
    ngx_uint_t                        lowcase_index;
    u_char                            lowcase_header[NGX_HTTP_LC_HEADER_LEN];

    u_char                           *header_name_start;
    u_char                           *header_name_end;
    u_char                           *header_start;
    u_char                           *header_end;

    /*
     * a memory that can be reused after parsing a request line
     * via ngx_http_ephemeral_t
     */

    u_char                           *uri_start;
    u_char                           *uri_end;
    u_char                           *uri_ext;
    u_char                           *args_start;
    u_char                           *request_start;
    u_char                           *request_end;
    u_char                           *method_end;
    u_char                           *schema_start;
    u_char                           *schema_end;
    u_char                           *host_start;
    u_char                           *host_end;
    u_char                           *port_start;
    u_char                           *port_end;

    unsigned                          http_minor:16;
    unsigned                          http_major:16;
};

我们接下来再看一下ngx_buf_t类的结构成员

typedef struct ngx_buf_s  ngx_buf_t;

struct ngx_buf_s {
    u_char          *pos;
    u_char          *last;
    off_t            file_pos;
    off_t            file_last;

    u_char          *start;         /* start of buffer */
    u_char          *end;           /* end of buffer */
    ngx_buf_tag_t    tag;
    ngx_file_t      *file;
    ngx_buf_t       *shadow;


    /* the buf's content could be changed */
    unsigned         temporary:1;

    /*
     * the buf's content is in a memory cache or in a read only memory
     * and must not be changed
     */
    unsigned         memory:1;

    /* the buf's content is mmap()ed and must not be changed */
    unsigned         mmap:1;

    unsigned         recycled:1;
    unsigned         in_file:1;
    unsigned         flush:1;
    unsigned         sync:1;
    unsigned         last_buf:1;
    unsigned         last_in_chain:1;

    unsigned         last_shadow:1;
    unsigned         temp_file:1;

    /* STUB */ int   num;
};

结构成员名字的含义:

pos:当buf所指向的数据在内存里的时候,pos指向的是这段数据开始的位置。
last:当buf所指向的数据在内存里的时候,last指向的是这段数据结束的位置。
file_pos:当buf所指向的数据是在文件里的时候,file_pos指向的是这段数据的开始位置在文件中的偏移量。
file_last:当buf所指向的数据是在文件里的时候,file_last指向的是这段数据的结束位置在文件中的偏移量。
start:当buf所指向的数据在内存里的时候,这一整块内存包含的内容可能被包含在多个buf中(比如在某段数据中间插入了其他的数据,这一块数据就需要被拆分开)。那么这些buf中的start和end都指向这一块内存的开始地址和结束地址。而pos和last指向本buf所实际包含的数据的开始和结尾。
end:解释参见start。
tag:实际上是一个void*类型的指针,使用者可以关联任意的对象上去,只要对使用者有意义。
file:当buf所包含的内容在文件中时,file字段指向对应的文件对象。
shadow:当这个buf完整copy了另外一个buf的所有字段的时候,那么这两个buf指向的实际上是同一块内存,或者是同一个文件的同一部分,此时这两个buf的shadow字段都是指向对方的。那么对于这样的两个buf,在释放的时候,就需要使用者特别小心,具体是由哪里释放,要提前考虑好,如果造成资源的多次释放,可能会造成程序崩溃!
temporary:为1时表示该buf所包含的内容是在一个用户创建的内存块中,并且可以被在filter处理的过程中进行变更,而不会造成问题。
memory:为1时表示该buf所包含的内容是在内存中,但是这些内容却不能被进行处理的filter进行变更。
mmap:为1时表示该buf所包含的内容是在内存中, 是通过mmap使用内存映射从文件中映射到内存中的,这些内容却不能被进行处理的filter进行变更。
recycled:可以回收的。也就是这个buf是可以被释放的。这个字段通常是配合shadow字段一起使用的,对于使用ngx_create_temp_buf 函数创建的buf,并且是另外一个buf的shadow,那么可以使用这个字段来标示这个buf是可以被释放的。
in_file:为1时表示该buf所包含的内容是在文件中。
flush:遇到有flush字段被设置为1的的buf的chain,则该chain的数据即便不是最后结束的数据(last_buf被设置,标志所有要输出的内容都完了),也会进行输出,不会受postpone_output配置的限制,但是会受到发送速率等其他条件的限制。
sync:
last_buf:数据被以多个chain传递给了过滤器,此字段为1表明这是最后一个buf。
last_in_chain:在当前的chain里面,此buf是最后一个。特别要注意的是last_in_chain的buf不一定是last_buf,但是last_buf的buf一定是last_in_chain的。这是因为数据会被以多个chain传递给某个filter模块。
last_shadow:在创建一个buf的shadow的时候,通常将新创建的一个buf的last_shadow置为1。
temp_file:由于受到内存使用的限制,有时候一些buf的内容需要被写到磁盘上的临时文件中去,那么这时,就设置此标志 。
line:109
  enum {
        sw_start = 0,
        ...
}

定义了一个状态机,通过state的值来确定处理步骤。

line:139 state = r->state;

state被赋值为r->state,r->state的类型为ngx_uint_t,ngx_uint_t类型是在以下文件中声明的

文件:_src/core/ngx_config.h_

typedef uintptr_t       ngx_uint_t;

而uintptr_t类型的话,在Mac OS X中是在

_/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/sys/_types/_uintptr_t.h_

中被声明

typedef unsigned long        uintptr_t;

可以看出uintptr_t类型实际上就是一个unsigned long类型,在《深入分析Linux内核源码》中的原因描述是这样的,

尽管在混合不同数据类型时你必须小心, 有时有很好的理由这样做. 一种情况是因为内存存取, 与内核相关时是特殊的. 概念上, 尽管地址是指针, 内存管理常常使用一个无符号的整数类型更好地完成; 内核对待物理内存如同一个大数组, 并且内存地址只是一个数组索引. 进一步地, 一个指针容易解引用; 当直接处理内存存取时, 你几乎从不想以这种方式解引用. 使用一个整数类型避免了这种解引用, 因此避免了 bug. 因此, 内核中通常的内存地址常常是 unsigned long, 利用了指针和长整型一直是相同大小的这个事实, 至少在 Linux 目前支持的所有平台上.

因为其所值的原因, C99 标准定义了 intptr_t 和 uintptr_t 类型给一个可以持有一个指针值的整型变量. 但是, 这些类型几乎没在 2.6 内核中使用

进行验证:

#include <stdio.h>

int main(){
    unsigned long a;
    int *b;
    printf("%d %d", sizeof(a), sizeof(b));
}

输出结果:

8 8
Process finished with exit code 0

​ 接下来进入了for循环里面,for循环里面的p的开始和结束分别为buff在内存中的开始(pos)和结束(last)。b->pos和b->last的类型是u_char,u_char是在MacOSX在_/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/sys/types.h_中定义的。

line:84 typedef unsigned char     u_char;

所以pos和last分别储存了buff开始和结束的unsigned类型的两个字符。

line:141 for (p = b->pos; p < b->last; p++){...}

接着使用了一个for循环来控制指针的移动,ch为当前指针指向的字符。r->state的值为0

,所以第一次进入状态机进入了sw_start,如果第一个字符是CR(回车)或者LF(换行)就break,接下去再把指针p向后移动直到碰到非LF和CR,再进入下一个判断语句

if ((ch < 'A' || ch > 'Z') && ch != '_') {
    return NGX_HTTP_PARSE_INVALID_METHOD;
}

遇到A-Z和_之外的字符都返回一个解析错误。通过了这两项检查,会向后移动直到遇到一个空格,会进入method的判断,开发人员先判断了遇到的第一个空格之前字符的数量,根据数量再进入相应的case中去判断是什么method然后再将r->method设置为相应的method。

完成了以上对HTTP method的判断之后,进入了第二个判断sw_spaces_before_uri,

switch (ch) {
case ' ':
    break;
default:
    return NGX_HTTP_PARSE_INVALID_REQUEST;
}

这一段我们可以看出,只要遇到空格(space)指针就会就会往后移动,直到遇到'/'或者遇到字符A-Z。

c = (u_char) (ch | 0x20);
if (c >= 'a' && c <= 'z') {
    r->schema_start = p;
    state = sw_schema;
    break;
}

代码就是判断是否是sw_host_start,不是sw_host_start就判别为uri的开始,如果在:后碰到两个/并且如果[就进行ipv6的判断。

文字看起来没有图像直观,以下是解析的流程图nginx_http0.png

在sw_uri之后放置空格,case会进入sw_check_uri_http_09,这时候uri_ext和uri_end之间的值为Nginx判断的后缀,即如果让nginx解析的是以下的连接

http://www.lonelyrain.me/1.jpg[空格][零零]1.php

那么nginx会判定为jpg,到了case sw_check_uri_http_09中,遇到/00并不会进行处理,会使用default条件

nginx_http_1.png

default:
    r->space_in_uri = 1;
    state = sw_check_uri;
    break;

nginx_gdb_1.png

成功的没有让nginx对/00进行处理。

而后面.php成功的将uri_ext覆盖为了php,之后 nginx 就会将请求发送给 fastcgi 去解析,fastcgi查找文件会被00阻断[这里代码找不到,留个坑],于是漏洞就形成了。

nginx_burp.png
注意点就是jpg文件上传的时候必须带一个空格,触发的时候空格后面加一个00跟上.php就能触发了[security.limit_extensions没有限制的情况下]

Reference:

taobao,(2013)._nginx开发从入门到精通

囧囧有神,(2015)._nginx源码分析之http解码实现