查看原文
其他

BROP Attack之Nginx远程代码执行漏洞分析及利用

k0pwn_ko 计算机与网络安全 2022-06-01

信息安全公益宣传,信息安全知识启蒙。

加微信群回复公众号:微信群;QQ群:16004488

加微信群或QQ群可免费索取:学习教程


Blind ROP是一种很有意思的攻击方式,其实很多国外文章,以及原来乌云知识库中的一篇文章都有介绍,我把这些参考文章都放在结尾位置,感兴趣的小伙伴可以一起学习交流一下。作为Flappy pig战队的脑残粉,我也会时时关注CTF的动态,听说Blind ROP也出现在了今年的HCTF的pwn题中,文后我会附上z神的github,里面有HCTF的这道BROP pwn题。


最近也跟着joker师傅和muhe师傅一起看了看关于Blind ROP的东西,这是个非常有意思的利用方式,虽然比较复杂,但同时也很佩服这个利用的脑洞,攻防对抗就是不断在这种精彩的利用和缓解中提升的。


对于CTF我不是特别了解,但是在学习的过程中,通过一个Nginx的老洞,总算“认识”了Blind ROP,受益匪浅,同时也感谢joker师傅,muhe师傅,swing师傅在学习过程中的讨论和指导。


下面我将就Nginx漏洞原理,以及这个Nginx漏洞的Exploit来全方位浅析BROP这种利用方式,关于这个漏洞的原理,网上有详细说明,这里我将结合Exploit的利用来讲解整个过程。文中有失误的地方还请师傅们多多包含,多多批评指正(毕竟通读2000+行ruby太痛苦T.T)。


Nginx漏洞分析(CVE-2013-2028)


这是一个Nginx的栈溢出漏洞,我的分析环境是在x86下,而利用是在x86_64下,我本来是不想这样的,但是之前在x86下用msf复现了Nginx这个漏洞,顺道进行了分析,然后拿到Exploit的时候又在x86_64下进行了BROP的利用研究,不过这个系统版本不影响我们对漏洞的理解,利用的分析。


关于漏洞环境以及Nginx安装搭建这里我就不多说了,文后的参考文章中,我会提供一个搭建环境,按那个搭没错,这里漏洞分析我的环境是Ubuntu 13.04 x86,利用分析环境是Kali 2.0。


搭建好环境之后用“#/usr/local/nginx/nginx”运行nginx服务,有的环境下是“#/usr/local/nginx/sbin/nginx”,运行服务后,用gdb attach方法附加,之后通过msf方法发送Exploit,这里我碰到过一种情况,就是msf发送Exploit的时候,会提示ERRCONNECT,这时候可以通过set target 0的方法设置好目标对象,而不用auto target,应该就可以了。发送Exploit之后,首先我们来看一下发送的数据包。



一共发送了两个GET包,在后面的畸形字符串前,包含了一个Encoding字段,值为chunked,而第一个数据包,同样也包含了一个值为chunked,但不带畸形数据,后面会解释为什么要发送两个,这时Nginx捕获到了崩溃。



崩溃状况下,通过bt的方法回溯一下堆栈调用。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

gdb-peda$ bt

#0  0xb77d5424 in __kernel_vsyscall ()

#1  0xb7596b1f in raise () from /lib/i386-linux-gnu/libc.so.6

#2  0xb759a0b3 in abort () from /lib/i386-linux-gnu/libc.so.6

#3  0xb75d3ab5 in ?? () from /lib/i386-linux-gnu/libc.so.6

#4  0xb766ebc3 in __fortify_fail () from /lib/i386-linux-gnu/libc.so.6

#5  0xb766eb5a in __stack_chk_fail () from /lib/i386-linux-gnu/libc.so.6

#6  0x0807b4c3 in ngx_http_read_discarded_request_body (r=r@entry=0x83f7838)

    at src/http/ngx_http_request_body.c:676

#7  0x0807bdf7 in ngx_http_discard_request_body (r=r@entry=0x83f7838)

    at src/http/ngx_http_request_body.c:526

#8  0x08087a98 in ngx_http_static_handler (r=0x83f7838)

    at src/http/modules/ngx_http_static_module.c:211

#9  0x0806fb2b in ngx_http_core_content_phase (r=0x83f7838, ph=0x84022b8)

    at src/http/ngx_http_core_module.c:1415


这里问题出在stack_chk_fail,也就是canary的检查失败了,导致了程序异常中断,这个bt回溯内容很长,这里我就不讲述回溯过程了,我们直接来正向动静结合分析一下整个漏洞的成因。首先我们发送的数据包包含chunked字段。这样会进行一次if语句比较,然后给一个r在结构体的headers_in的chunked成员变量赋值。


src/http/ngx_http_request.c:1707:


1

2

3

4

5

6

7

8

9

10

11

12

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;

        }


如果看之前的bt回溯可以看到,这个r结构体经常会作为参数被引用到,这个r结构体是一个Nginx HTTP请求的结构体,其定义如下:


1

typedef struct ngx_http_request_s     ngx_http_request_t;


通过这种定义,我们能直接用p命令来打印整个结构体的内容。


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

gdb-peda$ p *(struct ngx_http_request_s*)0x83f7838

$1 = {

  headers_in = {

    headers = {

      last = 0x83f7870, 

      part = {

      }, 

      size = 0x18, 

      nalloc = 0x14, 

      pool = 0x83f7810

    }, 

    host = 0x83f7d9c, 

    connection = 0x0, 

    if_modified_since = 0x0, 

    if_unmodified_since = 0x0, 

    if_match = 0x0, 

    if_none_match = 0x0, 

    user_agent = 0x0, 

    referer = 0x0, 

    content_length = 0x0, 

    content_type = 0x0, 

    range = 0x0, 

    if_range = 0x0, 

    transfer_encoding = 0x83f7db4, 

    expect = 0x0, 

    upgrade = 0x0, 

    accept_encoding = 0x0, 

    via = 0x0, 

    authorization = 0x0, 

    keep_alive = 0x0, 

    x_forwarded_for = {

    }, 

    user = {

    }, 

    passwd = {

    }, 

    cookies = {

    }, 

    server = {

      l

  }, 

  headers_out = 


这里我列举了成员变量headers_in的内容,除了这个成员变量还有很多,感兴趣的小伙伴可以直接在源码中找到,在这个漏洞中,我们只关心headers_in中的部分成员,因此后续分析中,我只列举关键的成员变量值,结构体名字太长,后面都称之为r结构体。。


回到刚才if语句位置,在ngx_http_process_request_header函数下断点进行单步跟踪。


1

2

3

4

5

6

7

8

9

10

gdb-peda$ b *ngx_http_process_request_header

Breakpoint 2 at 0x80741ad: file src/http/ngx_http_request.c, line 1707.

[-------------------------------------code-------------------------------------]

   0x80741a4 <ngx_http_request_finalizer+18>:call   0x80737ef <ngx_http_finalize_request>

   0x80741a9 <ngx_http_request_finalizer+23>:add    esp,0x1c

   0x80741ac <ngx_http_request_finalizer+26>:ret    

=> 0x80741ad <ngx_http_process_request_header>:push   ebx

[------------------------------------stack-------------------------------------]

[------------------------------------------------------------------------------]

Legend: code, data, rodata, value


命中函数入口位置,这个时候查看r结构体中的chunked对象。


1

2

3

4

gdb-peda$ p *(struct ngx_http_request_s*)0x83f7838

$1 = {

  headers_in = {

    chunked = 0x0,


这个值还是0x0,接下来开始单步跟踪。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

gdb-peda$ ni

[----------------------------------registers-----------------------------------]

EAX: 0x83f7443 ("Chunked")

EBX: 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bT\326?\b\306,\a\b")

ECX: 0xa ('\n')

EDX: 0x0 

ESI: 0x83fd6ec --> 0x83f7950 --> 0x0 

EDI: 0x1 

EBP: 0x8403ac8 --> 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bT\326?\b\306,\a\b")

ESP: 0xbfc6b650 --> 0x83f7443 ("Chunked")

EIP: 0x8074314 (<ngx_http_process_request_header+359>:call   0x804f8cf <ngx_strncasecmp>)

EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)

[-------------------------------------code-------------------------------------]

   0x8074306 <ngx_http_process_request_header+345>:mov    DWORD PTR [esp+0x4],0x80b0586

   0x807430e <ngx_http_process_request_header+353>:mov    eax,DWORD PTR [eax+0x10]

   0x8074311 <ngx_http_process_request_header+356>:mov    DWORD PTR [esp],eax

=> 0x8074314 <ngx_http_process_request_header+359>:call   0x804f8cf <ngx_strncasecmp>

Guessed arguments:

arg[0]: 0x83f7443 ("Chunked")

arg[1]: 0x80b0586 ("chunked")

arg[2]: 0x7


单步跟踪到0x8074314地址位置,调用了strncasecmp做比较,比较的两个值就是chunked,这时候数据包包含chunked,所以会进入刚才源码中的if语句处理,处理后再看r结构体的chunked变量已经被赋值。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

gdb-peda$ ni

[----------------------------------registers-----------------------------------]

[-------------------------------------code-------------------------------------]

   0x807431d <ngx_http_process_request_header+368>:mov    DWORD PTR [ebx+0x70],0x0

   0x8074324 <ngx_http_process_request_header+375>:mov    DWORD PTR [ebx+0xdc],0xffffffff

   0x807432e <ngx_http_process_request_header+385>:mov    DWORD PTR [ebx+0xe0],0xffffffff

=> 0x8074338 <ngx_http_process_request_header+395>:or     BYTE PTR [ebx+0xe8],0x4

[------------------------------------stack-------------------------------------]

0000| 0xbfc6b650 --> 0x83f7443 ("Chunked")

0004| 0xbfc6b654 --> 0x80b0586 ("chunked")

[------------------------------------------------------------------------------]

Legend: code, data, rodata, value

1749            r->headers_in.chunked = 1;

gdb-peda$ p *(struct ngx_http_request_s*)0x83f7838

$2 = {

  headers_in = {

    chunked = 0x1,


随后,程序会进入漏洞触发的关键函数ngx_http_discard_request_body,这个函数中会有一处对chunked的值进行判断,如果chunked的值为1,则会进入到if语句内部处理逻辑,会调用另一个函数ngx_http_discard_request_body_filter。


1

2

3

4

5

6

7

8

9

10

11

12

ngx_http_discard_request_body(ngx_http_request_t *r)

{

    if (size || r->headers_in.chunked) {

        rc = ngx_http_discard_request_body_filter(r, r->header_in);

        if (rc != NGX_OK) {

            return rc;

        }

        if (r->headers_in.content_length_n == 0) {

            return NGX_OK;

        }

    }

    ngx_http_read_discarded_request_body


在执行完ngx_http_discard_request_body离开if语句的逻辑之后,会执行ngx_http_read_discarded_request_body,也就是刚才在回溯过程中stack_chk所在的最内层漏洞函数,首先我们来看一下ngx_http_discard_request_body_filter函数。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

src/http/ngx_http_request_body.c:679

static ngx_int_t

ngx_http_discard_request_body_filter(ngx_http_request_t *r, ngx_buf_t *b)

{

     for ( ;; ) {

            rc = ngx_http_parse_chunked(r, b, rb->chunked);

            if (rc == NGX_OK) {

                /* a chunk has been parsed successfully */

            }

            if (rc == NGX_DONE) {

                /* a whole response has been parsed successfully */

            }

            if (rc == NGX_AGAIN) {

                /* set amount of data we want to see next time */

                r->headers_in.content_length_n = rb->chunked->length;

                break;

            }


注意在for循环中会进行三个if语句逻辑处理,分别对应三种NGX状态,在rc==NGX_AGAIN的时候,会对content_length_n成员变量进行赋值。NGX_AGAIN就是要第二次接收时才会触发,因此这就解释了在之前我们提到的为什么要发两个数据包的问题。


在这个函数下断点进行单步跟踪。首先会判断if语句中NGX状态。


1

2

3

4

5

gdb-peda$ ni

   0x807aada <ngx_http_discard_request_body_filter+282>:cmp    eax,0xfffffffe

=> 0x807aadd <ngx_http_discard_request_body_filter+285>:jne    0x807aafe <ngx_http_discard_request_body_filter+318>

Legend: code, data, rodata, value

0x0807aadd735            if (rc == NGX_AGAIN) {


这里判断通过,会进入到这个if语句中进行处理。


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

gdb-peda$ ni

[----------------------------------registers-----------------------------------]

EAX: 0xfffffffe 

EBX: 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bL\337?\b\306,\a\b\360\256\006\b")

ECX: 0xfdffffff 

EDX: 0xfdffbbff 

ESI: 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bL\337?\b\306,\a\b\360\256\006\b")

EDI: 0x83f7ffc --> 0x0 

EBP: 0x83f10e4 --> 0x83f7808 --> 0x0 

ESP: 0xbfc6b2e0 --> 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bL\337?\b\306,\a\b\360\256\006\b")

EIP: 0x807aadf (<ngx_http_discard_request_body_filter+287>:mov    eax,DWORD PTR [edi+0x1c])

EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)

[-------------------------------------code-------------------------------------]

   0x807aad5 <ngx_http_discard_request_body_filter+277>:jmp    0x807aba2 <ngx_http_discard_request_body_filter+482>

   0x807aada <ngx_http_discard_request_body_filter+282>:cmp    eax,0xfffffffe

   0x807aadd <ngx_http_discard_request_body_filter+285>:jne    0x807aafe <ngx_http_discard_request_body_filter+318>

=> 0x807aadf <ngx_http_discard_request_body_filter+287>:mov    eax,DWORD PTR [edi+0x1c]

   0x807aae2 <ngx_http_discard_request_body_filter+290>:mov    edx,DWORD PTR [eax+0x10]

   0x807aae5 <ngx_http_discard_request_body_filter+293>:mov    eax,DWORD PTR [eax+0xc]

   0x807aae8 <ngx_http_discard_request_body_filter+296>:mov    DWORD PTR [esi+0xdc],eax

   0x807aaee <ngx_http_discard_request_body_filter+302>:mov    DWORD PTR [esi+0xe0],edx

[------------------------------------stack-------------------------------------]

0000| 0xbfc6b2e0 --> 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bL\337?\b\306,\a\b\360\256\006\b")

0004| 0xbfc6b2e4 --> 0x83f10e4 --> 0x83f7808 --> 0x0 

0008| 0xbfc6b2e8 --> 0x83f8020 --> 0x1 

0012| 0xbfc6b2ec --> 0xb77df9b2 (cmp    eax,0x0)

0016| 0xbfc6b2f0 --> 0xbfc6b370 --> 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bL\337?\b\306,\a\b\360\256\006\b")

0020| 0xbfc6b2f4 --> 0x83f7838 ("HTTP\310:@\b\304|?\b\324%?\b\274\325?\bL\337?\b\306,\a\b\360\256\006\b")

0024| 0xbfc6b2f8 --> 0x83f7fb6 ("/index.html")

0028| 0xbfc6b2fc --> 0x806fa75 (<ngx_http_map_uri_to_path+402>:jmp    0x806fad9 <ngx_http_map_uri_to_path+502>)

[------------------------------------------------------------------------------]

Legend: code, data, rodata, value

739                r->headers_in.content_length_n = rb->chunked->length;

gdb-peda$ ni

=> 0x807aaf4 <ngx_http_discard_request_body_filter+308>:mov    eax,0x0

Legend: code, data, rodata, value

764    return NGX_OK;


▼ 点击阅读原文,查看更多精彩文章。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存