I am a slow walker, but I never walk backwards.

Nostromo WebServer RCE原理分析(CVE-2019-16278漏洞分析【续】)

Posted on By Curz0n

0x00 前言

在文章《利用目录遍历漏洞实现RCE (CVE-2019-16278漏洞分析,附GDB调试过程)》分析RCE的部分,可知能够远程命令执行的原因是execve函数执行了指定的sh程序,但是传入的命令是怎么被执行的,POST参数中为什么要传入两个echo\n,为什么要使用HTTP 1.0协议这些疑问都还没解决,本着刨根问底的求知精神,下面我们一起继续分析,看看一个正常使用HTTP 1.1协议的POST请求能否实现RCE。

0x01 漏洞分析

1. 了解IPC-管道

管道是一种最基本的IPC(Inter-Process Communication,进程间通信)机制,由pipe函数创建,它是半双工的,数据只能在一个方向上流动,想要双方通信,就需要建立起两个管道。管道只能用于具有共同祖先的进程之间进行通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可以应用该管道。具体应用场景见如下示例:

#include <unistd.h>

int main(){
    int pfds[2];
    char *command = "whoami";
    /*
    * pipe创建管道,创建成功返回0
    * pfds返回两个文件描述符:
    *   pfds[0]:指向管道的读端
    *   pfds[1]:指向管道的写端
    *   pfds[1]的输出是pfds[0]的输入
    */
    if(pipe(pfds) == 0){
        //fork返回0,子进程
        if(fork() == 0){
            //关闭管道的写端
            close(pfds[1]);
            //复制文件描述符,让stdin(标准输入)指向管道的读端,相当于stdin的数据是由pfds[1]写入的
            dup2(pfds[0],0);
            //定义传递给程序的参数,数组指针argv必须以程序(filename)开头,NULL结尾
            char *argv[ ]={"sh", NULL};
            //定义程序运行的环境变量,默认环境变量
            char *envp[ ]={0, NULL};
            //执行sh程序,显式传递给sh程序的argv参数为空
            execve("/bin/sh", argv, envp);  
        }
        //父进程
        else{
            //关闭管道的读端
            close(pfds[0]);
            //把需要执行的命令写入到管道的写端,然后在管道的读端获取命令
            write(pfds[1],command,6);
        }
    }
    return 0;
}

简单看看代码,先使用pipe函数新建管道并fork一个子进程,在父进程中使用write函数把需要执行的命令写入管道的写端,然后在子进程中使用dup2函数复制文件描述符,让系统的标准输入(stdin)指向管道的读端,此时stdin的数据就是whoami命令,最后execve执行sh程序后,虽然没有直接给sh程序传递参数,但是sh会自己在标准输入里面找参数。编译运行程序,结果如下:

sh执行了stdin里面的whoami命令,相当于命令与命令之间使用管道符通信,command1的标准输出会作为command2的标准输入使用:

2. RCE原理分析

接着上篇文章的分析,GDB动态调试定位到execve函数位置,如下所示

可以看见在execve函数中传递给sh程序的参数cgiarg为空,运行完execve函数后就自动执行了指定命令。sh程序是怎么执行id命令的呢,一起来看看源码./src/nhttpd/http.c,关键代码如下:

 ......SNIP......
 462                 /* create pipes to communicate with cgi */
 463                 if (pipe(fds1) == -1) {
 464                         syslog(LOG_ERR, "can't fork cgi: pipe fds1: %s",
 465                             strerror(errno));
 466                         exit(1);
 467                 }
 468                 if (pipe(fds2) == -1) {
 469                         syslog(LOG_ERR, "can't fork cgi: pipe fds2: %s",
 470                             strerror(errno));
 471                         exit(1);
 472                 }
 473 
 474                 /* fork child for cgi */
 475                 if ((cpid = fork()) == -1) {
 476                         syslog(LOG_ERR, "can't fork cgi: fork: %s",
 477                             strerror(errno));
 478                         exit(1);
 479                 }
 480 
 481                 /* cgi */
 482                 if (cpid == 0) {
 483                         /* child dont need those fds */
 484                         close(fds1[1]);
 485                         close(fds2[0]);
 486 
 487                         if (chdir(rh->rq_filep) == -1) {
 488                                 syslog(LOG_ERR, "can't fork cgi: chdir: %s",
 489                                     strerror(errno));
 490                                 exit(1);
 491                         }
 492                         dup2(fds1[0], STDIN_FILENO);
 493                         dup2(fds2[1], STDOUT_FILENO);
 494 
 495                         /* build cgi environment array */
 ......SNIP......
 580 
 581                         execve(rh->rq_filef, cgiarg, cgienv); //here
 582                         exit(0);
 583                 }
 584 
 585                 /* parent dont need those fds */
 586                 close(fds1[0]);
 587                 close(fds2[1]);
 588 
 589                 /* if post send data to cgis stdin */
 590                 if (!strcasecmp(rh->rq_method, "POST")) {
 591                         rp = 0;
 592                         rt = 0;
 593                         size = atoi(rh->rq_fv_clt);
 594 
 595                         if (size > 0) {
 596                                 if (blen > 0) {
 597                                         r = http_body_comp(body, blen, blen,
 598                                             size);
 599                                         if (r > 0)
 600                                                 sys_write(fds1[1], body, r);
 601                                         else
 602                                                 sys_write(fds1[1], body, blen);
 603                                 }
 604 
 ......SNIP......
 630                         }
 631                 }
 632                 /* close fd to cgi stdin */
 633                 close(fds1[1]);
 ......SNIP......

462-479行创建管道并fork一个子进程运行CGI,关键代码是第600行处理POST参数,把参数写入管道的写端,这里的sys_write函数是write函数的封装,其实现在./src/nhttpd/sys.c文件,然后在子进程第492行把POST参数复制到标准输入(stdin),最后581行execve运行的sh程序在标准输入找到用户传入的POST参数命令并执行。

3. 利用OOB技术回显

如果仔细分析一下EXP,会发现该漏洞的EXP非常的讲究,其POST参数传递的命令之前必须存在两个echo\n字符,并且必须使用HTTP 1.0协议,否则服务器就会响应500,执行的命令结果不能正常回显。因笔者对CGI不太熟悉,所以没有再对EXP中的特殊字符和协议进一步深究。那一个正常的HTTP 1.1协议的POST请求可以完成远程代码执行吗?如下所示,使用一个正常的POST请求并发送一条命令,结果如下

服务端返回500错误,GDB调试一下,发现execve函数是正常执行了id命令的,所以EXP中的echo\n字符和HTTP 1.0协议的目的是为了让命令执行结果能在response中直接回显。既然正常的HTTP请求也可以命令执行,那有办法获取到命令执行的结果吗?如果目标服务器有外网访问权限,带外数据(Out of Band, OOB)技术是解决无回显的有效办法。我们可以直接利用Burp Suite提供的Burp Collaborator client模块,执行命令如下

结果如下,成功获取到命令执行结果

0x02 结语

通过进一步分析,我们明白了execve执行的程序参数是如何传递的,EXP中的特殊字符和协议的作用,并且利用OOB技术解决了正常HTTP请求无回显的问题。笔者水平有限,文章如有理解错误的地方,还请不吝赐教。

版权声明:转载请注明出处,谢谢。https://github.com/curz0n