原文链接:Revisiting an Old Bug: File Upload to Code Execution
译者:知道创宇404实验室翻译组

最近一位朋友询问我几年前发现的一个漏洞(CVE-2021-27198)。该漏洞是Visualware MyConnection Server软件版本 11.0 至 11.0b 中未经身份验证的任意文件上传问题。

MyConnection Server 软件是用Java编写的,旨在在不同平台上无缝运行。在这种情况下,任意文件写入具有特权,会给予文件服务器提升的权限(在 Windows 上具有 SYSTEM 权限,在 Linux 上具有 root 权限)。有了特权文件写入,实现代码执行通常很简单。Doyensec 的这篇文章概述了最常见的方法,这些方法可以大致分为两类:特定于 Web 应用程序的方法和特定于操作系统的方法。Web 应用程序特定的方法涉及寻找在 Web 服务器进程内启动执行的方法,示例包括上传 Web 框架配置文件或 Web 应用程序源代码。另一方面,操作系统特定的技术涉及寻找操作系统本身控制的执行触发器,例如服务、计划任务、cron 作业等。

遗憾的是,在这个特定的漏洞案例中,无法直接针对Web服务器本身进行攻击,因为它是纯 Java 实现,而不是使用 Apache 或 Nginx 等 Web 服务器框架。因此,我们的重点将主要转向探索特定于操作系统的可能性或更创新的方法。

Windows 下的代码执行

在这种情况下,开始研究仅通过文件写入实现代码执行的可能技术。在 Windows 环境中,对易受传统 DLL 劫持或幻像 DLL 劫持的易受攻击的应用程序,利用这些写入所赋予的特权。

打开Sysinternals Procmon 工具开始寻找 CreateFile 的“NAME NOT FOUND”或“PATH NOT FOUND”结果。通常寻找可执行文件或 DLL 的实例。在查看结果时,注意到 MCS java 进程每分钟都在尝试打开几个“rtaplugin”JAR 文件,其中一些文件不存在。

普雷康蒙2

根据输出,服务器看起来是从磁盘动态中加载这些JAR文件到Java进程中的。如果是这样,需将自定义 JAR 文件放置在文件系统的特定位置即可执行任意代码。我决定在JD-GUI中打开 MCS JAR进行调查。

当搜索“rtaplugin”时,发现一个名为 RTAPlugin 的类,该类具有一个函数,可以从磁盘加载文件,创建自定义 ClassLoader,从文件加载类,然后创建该类的新实例。

加载jar

为了确认执行,我创建了一个简单的 POC,在创建该类的实例时执行 calc.exe。

有效负载1

然后手动将 JAR 复制到适当的目录中以查看它是否已加载,而它确实起作用了!现在我只需要利用文件上传漏洞进行测试。

计算

启动Burp,并粘贴到JAR文件中作为文件上传请求的正文。不幸的是,当我发送请求后,在进程列表中并没有看到calc.exe。

打嗝请求

打开 MyConnection 服务器的日志文件后,尝试解压JAR时引发了以下异常。

CRC

获取了原始JAR有效负载,将其与上传到 rtaplugin 目录中的有效负载进行了比较,发现某些字节已损坏。再次打开 JD-GUI 来仔细查看执行文件写入的代码。

文件写入

不太明显的是,调用 String.valueOf 和 String.getBytes 时存在一种隐含的编码/解码过程。结果,某些范围的字节被损坏。在 Windows 上,发现 0x80 和 0x9f 之间的字节被其他值替换。这意味需要进行一些位操作才能使载荷正常工作。

经过一番搜索后,发现Yudai Fujiwara 的一篇CTF 文章也面临着类似的编码问题。文章提供了一个用于生成只包含ASCII范围内字节(0x0-0x7f)的zip文件的代码。该脚本主要关注zip协议中两个结构需要无非ASCII字节的情况,即压缩文件的CRC和任何长度字段。

该脚本通过迭代修改内部文件来暴力破解 CRC 字段的有效 ASCII 值。在 CTF 挑战中,zip 包含 ASCII 脚本,而 JAR 包含二进制类文件。我更新了算法,对 Java 源代码执行类似的修改,然后在每次迭代时重新编译 Java 类文件以进行 CRC 更新。

while True:

    cmd = ['/opt/jdk1.7.0_80/bin/javac','-cp','/opt/mcs/java.jar', '/opt/mcs/error.java']
    output = subprocess.check_output(cmd)

    class_file_contents = ''
    with open("/opt/mcs/error.class", "rb") as f:
        class_file_contents = f.read()

    class_file_len = len(class_file_contents)
    if class_file_len > 0:
        crc32 = binascii.crc32(class_file_contents)
        logger.info("CRC: " + hex(crc32))
        if all([(crc32 >> i) & 0xff < 0x80 for i in range(0, 32, 8)]):
            if all([(class_file_len >> i) & 0xff < 0x80 for i in range(0, 32, 8)]):
                break
            else:
                new_needle = needle + random.choice(string.ascii_letters)*0x2
        else:
            new_needle = needle + random.choice(string.ascii_letters)

        # Add more data to the src
        with open("/opt/mcs/error.java", "r") as f:
            y = f.read()

        new_data= y.replace(needle, new_needle)
        with open("/opt/mcs/error.java", "w") as f:
            f.write(new_data)

        # Update needle
        needle = new_needle

创建完 ascii-zip 载荷后,使用易受攻击的端点上传了它。正如期待的,它在目标服务器上作为 SYSTEM 用户加载并执行。为了避免每次执行不同命令都需要重新编译JAR文件,生成了一个可以在已知位置执行脚本的 JAR文件。然后,可以随时单独上传新命令的脚本进行命令执行。添加了“setExecutable”指令以确保它也能在 Linux 上运行。

有效载荷2

Linux远程代码执行

在处理基于 Linux 的操作系统时,利用任意文件写入漏洞的主要挑战之一是确保正确配置文件权限。即使文件被执行,如果它没有被标记为可执行文件,也不会起作用。为了克服这个障碍,针对已经设置了执行权限的文件进行攻击。

正如预料的那样,在Linux上进行漏洞利用非常简单,只需覆盖 /etc/cron.目录中的脚本即可。在 Red Hat 发行版上,可以通过覆盖 /etc/cron.hourly/0anacron 脚本来实现相对及时的执行。以 cron 作业为目标的缺点是无法保证特定脚本的存在,并且覆盖它们可能会导致系统不稳定。

0anacron

假设出站网络连接未被阻止,一个简单的反向 shell是放入 cron 作业的最简单有效的载荷。如果这个不起作用,可以修改 cron 作业(包含未修改的rtaplugin JAR文件的base64编码副本),然后将其复制到正确的目录中。接着可以使用与上述相同的技术,通过上传不同版本的脚本到/tmp/b.bat来获取重复的代码执行能力。

凭据认证

很遗憾,在开发rtaplugin漏洞利用工具时,发现只有当Web服务器具有有效许可证时,rtaplugins(以及定期加载它们的线程)才处于活动状态。由于测试实例仍处于试用阶段,我不知道这一点。我决定再次查看,看看特权文件写入漏洞(CVE-2021-27198)是否可以再次被利用,但这次是为了欺骗服务器,使其认为它已获得许可。这里希望是找到一个文件或数据库条目,可以使用我们的文件上传漏洞进行修改以绕过许可。需要注意的是,这里描述的任何内容都不能用于解或破解完全打补丁的系统上该软件的许可证。目标是利用已经具备的特权文件系统访问权限来绕过任何许可证检查。

当导航到 Web 应用程序主页时,会看到左侧有一个包含“许可证”链接的菜单。可以肯定这是查看的正确位置。

执照

遗憾的是,当单击它时,会出现登录页面。如果管理员用户的密码尚未更改,看到的页面如下所示。

登录

如果能走到这一步,或者恰好猜到一些凭据,将拥有足够的权限访问服务器许可证终端节点。关于预期密钥格式的格式没有太多线索,只有一个以MCS开头的提示。

许可

再次打开JAR文件,找到了负责处理许可证激活的代码。在该处理程序在进行一系列检查和转换之后,通过向visualware域发出网络请求来验证许可证。

http_verify2

如果失败(通过异常),服务器将尝试通过发送特制的 DNS 请求来验证许可证。

dns_验证

利用 CVE-2021-27198 的特权文件写入功能,可以使用多种方法来绕过许可证,而无需直接解密许可证密钥。当软件发起网络请求来验证输入的密钥时,可以将其重定向到我控制的服务器上,进行提供的密钥的身份验证。

实现此目的的最直接方法是修改hosts文件,该文件包含 IP 地址到主机名的映射,在解析主机名的IP地址时,操作系统通常首先检查此文件。在 Windows 系统上,可以在C:\Windows\System32\drivers\etc\hosts*中找到此文件,在 nix 系统上,它位于/etc/hosts。为了操作许可证验证过程,只需在hosts文件中插入一个新条目,将许可证服务器域名指向我控制的服务器的IP地址。

主机

另一种方法(特定于nix 系统)涉及更改 /etc/resolv.conf 文件,该文件指定用于域解析的 DNS 服务器的 IP 地址。通过将 DNS 服务器地址更改为我管理的服务器,可以确保对许可证服务器的任何 DNS 请求都解析为我伪造的许可证服务器的IP地址。

警告:覆盖/etc/hosts或/etc/resolv.conf文件可能会导致系统不稳定,如果预期正确解析自定义DNS条目或解析器,则可能出现问题。

接下来追踪activate函数到可以触发它的Web终端节点。不幸的是,输入到 Web 表单中的密钥似乎与发送到许可证服务器的格式不同。在对提供的许可证密钥执行一些基于字符串的检查之后,会调用validate函数。它似乎实现了一个手工制作版本的RSA加密。

RSA

从安全角度来看,要求将加密的许可证密钥作为 Web 应用程序的输入的理由是没有意义的。首先,由于未加密的许可证密钥随后以明文形式通过网络传输,可以很容易地被像Wireshark这样的工具捕获。其次,实际的许可证密钥是在供应商的服务器上验证的,因此推断许可证密钥的生成方式是不切实际的。然而对我来说,这给实现上一节中详细介绍的网络激活函数带来了一个小障碍。

敏锐的读者可能已经注意到了RSA参数的一个有趣细节。那个公钥看起来很小,刚刚超过 256 位。看来我们的 CTF 挑战仍在继续……

酒吧

由于公钥非常小,我应该能找到一个CTF的解决方案,指导我如何将其分解为两个质数。经过几次搜索后,我找到了Dennis Yurichev的一篇文章,介绍了使用名为CADO-NFS的工具来完成这个任务。大约5分钟后,我得到了答案。

卡多

有了这两个素数,尝试使用尤里切夫帖子中提供的脚本重建私钥。不幸的是,Python 中的 RSA 加密库与提供的素数不能很好地配合。主要原因是块大小向上取整到257位,并且会出现截断错误。我还发现服务器的自定义 RSA 实现没有实现填充。为了解决这些问题,我编写了代码来手动执行计算,而不是使用加密库。

from Crypto.Util.number import bytes_to_long
from Crypto.Util.number import inverse

def encrypt_with_private_key_raw(data):
    p = 624106295606602100995951586143562696483  
    q = 264089186086669371634156709929132346711

e = 17
n = p * q

phi = (p - 1) * (q - 1)
d = inverse(e, phi)

# Convert data to an integer and perform raw RSA encryption
m = bytes_to_long(data)

ciphertext = pow(m, d, n)
return ciphertext

使用我的脚本生成了一个带有派生RSA私钥的加密blob,并发送了一个包含完整许可证密钥的Web请求。由于我已经覆盖了hosts文件,只需要通过解密检查,因为密钥不会传输到供应商的服务器进行验证。令人欣喜的是,它起作用了!

活性

结论

我的开发CVE-2021-27198的可行利用程序的旅程终于告一段落。起初只是一个简单的练习,却变成了一项相当艰巨的任务。对于感兴趣的人,我已经在这里上传了我的漏洞利用程序。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3069/