补丁分析

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

官方的补丁打在了_ngx_httpparse.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--- 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

1
2
3
4
5
6
7
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_httprequest.h

1
typedef struct ngx_http_request_s ngx_http_request_t;

ngx_http_request_s的结构成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
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类的结构成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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的内容需要被写到磁盘上的临时文件中去,那么这时,就设置此标志 。
1
2
3
4
5
line:109
enum {
sw_start = 0,
...
}

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

1
line:139 state = r->state;

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

文件:_src/core/ngxconfig.h

1
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_

中被声明

1
typedef unsigned long uintptr_t;

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

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

进行验证:

1
2
3
4
5
6
7
#include <stdio.h>
int main(){
unsigned long a;
int *b;
printf("%d %d", sizeof(a), sizeof(b));
}

输出结果:

1
2
8 8
Process finished with exit code 0

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

1
2
​ line:84 typedef unsigned char u_char;


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

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


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

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

1
2
3
4
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,

1
2
3
4
5
6
7
switch (ch) {
case ' ':
break;
default:
return NGX_HTTP_PARSE_INVALID_REQUEST;
​ }


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

1
2
3
4
5
6
7
​ 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

1
2
3
4
5
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解码实现

Comments

⬆︎TOP