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