前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CAPTAIN HOOK - 如何(不)寻找 JAVA 应用程序中的漏洞

CAPTAIN HOOK - 如何(不)寻找 JAVA 应用程序中的漏洞

作者头像
Khan安全团队
发布2022-01-21 15:27:53
7680
发布2022-01-21 15:27:53
举报
文章被收录于专栏:Khan安全团队Khan安全团队

寻找 Java 应用程序漏洞的好时机!在过去的几个月里,我一直在尝试构建一个名为Captain Hook的工具,它使用动态方法来查找大型闭源 Java 应用程序的一些有趣(安全方面)特性。在此过程中,我尝试了许多仪器工具和技术,但很难找到满足我所有需求的工具和技术。在本文中,我将总结通过我的许多(一些失败,一些成功)尝试所学到的东西。

要求

由于 Synacktiv 专家在寻找大型 Java 项目中的漏洞时将使用 Captain Hook,它应该:

  • 易于在目标应用程序上设置。
  • 易于使用,直观。
  • 不要为我们列出漏洞列表,而是将分析师指向应用程序的隐蔽功能,以便他可以专注于它。

因此,我和我的同事将工具的目标设定为能够跟踪任意方法调用,将有趣的与堆栈跟踪和输入一起记录给专家,并区分方法调用的输入是否是用户-控制与否。记录或不记录的内容应该是可定制的,并且默认为一组通常危险的本机 Java 方法。

我要分析的 Java 应用程序有时需要繁重而复杂的设置;有些只在 Windows 上运行,有些需要特定版本的 Java,等等。从这一点来看,我认为在虚拟机、容器或主机以外的任何地方设置 Java 应用程序会更容易。此外,为了使该工具尽可能通用,该工具必须独立于目标软件的执行环境。通过在自己的组件中运行该工具,确实应该可以使其与目标软件的要求无关,例如所需的操作系统。因此,我选择在 Docker 容器中开发我的工具,远程连接到运行正在调查的 Java 应用程序的 Java 虚拟机。

JAVA 代理

Java 提供了一种用于检测 Java 虚拟机的本机机制。根据官方Java文档

package java.lang.instrument 提供允许 Java 编程语言代理检测在 JVM 上运行的程序的服务。检测机制是修改方法的字节码。

我以为这将是我的主要工具,但我很快意识到许多库都是基于这种机制编写的,以便在更高级别上进行编程并获得更有意义的错误。这将在本文后面进行开发。

第一次接触项目

当我第一次得到这个主题时,我对仪器的概念一点也不熟悉。我在学校练习过 Java,并且对 Java 虚拟机的内部结构有基本的了解,但仅此而已。因此,我开始学习 Java 中的不同检测机制,并很快将注意力转向了几个项目:

  • Frida可能是最著名的检测框架,它支持 Dalvik 虚拟机(用于 android 应用程序)已有几年时间,最近还支持 Hotspot 虚拟机,允许检测在标准台式计算机上运行的 Java 应用程序。在 Java 进程中注入了一个 frida-agent,它允许我们通过 Javascript 绑定在 JVM 中执行代码;
  • ByteMan,一个直观的检测框架,基于 Java 提供的原生检测机制。它使用自定义脚本语言来描述加载代理后要运行的操作;
  • ByteBuddy,一个先进的、强大的、更可定制的原生检测框架。该代理使用 ByteBuddy 的类和方法用 Java 编写。

使用 Frida,我的设置是在应用程序 VM 上安装 frida-server,从 Captain Hook 的 docker 连接并注入 Frida 脚本,如下所示:

弗里达设置
弗里达设置

使用本机 Java 代理,应将编译后的代理复制到应用程序 VM,并从此处注入正在运行的 JVM 中。然后它可以由 CLI 控制,例如使用 TCP 套接字:

本机代理设置
本机代理设置

我认为这些将是我可能需要的所有工具,以便在 Java 应用程序中采用这种动态方法进行漏洞研究。

但是等等……你如何缓解漏洞的发现?

目标 0 - 选择一个典型的目标

为了创建一个工具来帮助审计人员发现大型闭源 Java 应用程序中的漏洞,其中很大一部分是识别典型的“大型闭源 Java 应用程序”并尝试使用我的工具重新发现公共漏洞。我在 Docker 容器中设置了多个应用程序,包括 Atlassian Jira & Confluence、ManageEngine OPManager、Oracle WebLogic 和 Jenkins。我最终选择专注于重新发现Orange Tsai 在 Jenkins 上使用的漏洞利用链 ,因为这是我遇到的记录最多的漏洞并且很容易重现。此漏洞利用链导致 Jenkins 版本低于 2.138 的预身份验证远程代码执行 (RCE)。在我的工具开发的不同阶段,我确保 Jenkins 的性能正常,并且可以使用我的工具发现 RCE(而不是完整的链)。

目标 1 -完整的堆栈跟踪

假设您想在 Java Web 应用程序中查找 RCE。要检测潜在的,您应该监视对类方法的调用。这可以使用我之前提到的三个工具很容易地完成,如图所示: exec java.lang.Runtime

  • 与弗里达:
代码语言:javascript
复制
Java.perform(function () {
    var runtimeClass = Java.use("java.lang.Runtime");
    runtimeClass.exec.overload("java.lang.String").implementation = function () {
        send("java.lang.Runtime exec called!");
        this.execute();
    };
});
  • 使用 ByteMan: 
代码语言:javascript
复制
RULE trace exec entry
CLASS java.lang.Runtime
METHOD exec(java.lang.String)
AT ENTRY
IF true
DO traceln("java.lang.Runtime exec called!")
ENDRULE
  • 与 ByteBuddy:
代码语言:javascript
复制
public class Agent {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        AgentBuilder mybuilder = new AgentBuilder.Default()
        .disableClassFormatChanges()
        .with(RedefinitionStrategy.RETRANSFORMATION)
        .with(InitializationStrategy.NoOp.INSTANCE)
        .with(TypeStrategy.Default.REDEFINE);
        mybuilder.type(nameMatches("java.lang.Runtime"))
        .transform((builder, type, classLoader, module) -> {
            try {
                return builder
                .visit(Advice.to(TraceAdvice.class)
                    .on(isMethod()
                        .and(nameMatches("exec"))
                    )
                );
            } catch (SecurityException e) {
                e.printStackTrace();
                return null;
            }
        }).installOn(inst);
    }
}


public class TraceAdvice {
    @Advice.OnMethodEnter
    static void onEnter(
        @Origin Method method,
    ) {
        System.out.println(method.getDeclaringClass().getName() + " " + method.getName() + " called!");
    }
}

请注意,在实际场景中,应该涵盖exec方法的所有重载,这仅适用于此处的 ByteBuddy 示例。

但随后,用户可能会想:“ 的论点从何而来?”。这就是事情开始变得奇怪的地方,因为很容易获得从线程开始到调用的堆栈跟踪,但是这个堆栈跟踪将不包括父调用的参数。为了澄清这个想法,让我向您介绍我的测试程序。这是一个简单的回声应用程序,我在整个工具的开发过程中都大量使用了它。与我之前提到的典型目标相比,它的启动速度非常快,这可以挽救生命,因为我无法计算导致 JVM 崩溃的次数...... exec exec

代码语言:javascript
复制
import java.io.*;
import java.util.*;

public class Main {

    public static void a(String s) {
        System.out.println(s);
    }

    public static void a(String s, String t) {
        if ("hi".equals(s)) {
            System.out.println(t);
        }
        else {
            b(s);
        }
    }

    public static void b(String s) {
        a(s);
    }

    public static void main(String[] args) {
        Scanner myObj = new Scanner(System.in);
        while (true) {
            System.out.println("Type something");
            a(myObj.nextLine(), "test");
        }
    }
}

使用上面提到的三个框架并检查对 的调用,很容易得到以下列表: java.io.PrintStream println(java.lang.String)

  • Main main(java.lang.String[])
  • Main a(java.lang.String, java.lang.String)
  • Main b(java.lang.String)
  • Main a(java.lang.String)
  • java.io.PrintStream println(java.lang.String)

但是在对 javadoc 以及三个框架的文档进行了一些挖掘之后,我想不出一个简单的方法来获得以下列表:

  • Main main([])
  • Main a("Hello", "test")
  • Main b("Hello")
  • Main a("Hello")
  • java.io.PrintStream println("Hello")

所以我最终编写了一个肮脏的解决方案,它基本上包括挂钩每个加载的方法,以跟踪传递给每个方法调用的每个参数。很酷的是,我知道 JVM 中发生的一切。坏事是,你猜对了,它在我的 echo 程序上运行良好,但是,当需要在真实目标上测试它时,它完全无法使用。

我在这个过程的早期就放弃了 ByteMan,因为当时我没有看到调用任意代码和修改方法参数的可能性。此外,尝试使用三个不同的框架将我的工具的每个功能开发 3 次有点繁重,我更喜欢当时只保留更有前途的两个(我也很快放弃了 ByteBuddy)。回想起来,我认为我应该花更多的时间来摆弄它,因为如果我掌握了它,它可能会满足我的需求。

回到主要问题:拥有完整的堆栈跟踪。我记得在这个话题上卡住了很长一段时间,直到一位同事告诉我从 Java IDE 的工作中获取灵感。实际上,其中一些能够打印这样的堆栈跟踪。所以我开始研究这些调试器是如何发挥这种魔力的。在这里,我发现了将成为该项目其余部分的主要工具:Java 调试接口。严格来说,它不是仪器,但它能够完全按照我的意愿去做。

根据官方Java文档:

Java 调试接口 (JDI) 是一种高级 Java API,它为需要访问(通常是远程)虚拟机运行状态的调试器和类似系统提供有用的信息。JDI 提供对正在运行的虚拟机的状态、类、数组、接口和原始类型以及这些类型的实例的内省访问。JDI 还提供对虚拟机执行的显式控制。暂停和恢复线程、设置断点、[...] 以及检查暂停线程状态、局部变量、堆栈回溯等的能力。

唯一的缺点是运行应用程序进行分析的 JVM 需要使用几个命令行参数启动。这略微增加了设置的复杂性,但大多数主流 Java 应用程序都提供了一个配置文件,可以在其中指定额外的 JVM 启动选项。

所以我写了一个 Java 程序,就像一个调试器,它通过 UNIX 套接字与我的主 CLI(用 Python 编写)进行通信,这个过程很简单:

  1. 在所需方法上设置断点;
  2. 当断点命中时,调用一组 Java 调试接口方法来检索父调用和这些调用的参数;
  3. 恢复 JVM。

这种方法的性能比上面提到的两种方法要好得多,并且允许我通过 CLI 显示我想要的信息。

在这一点上,是我放弃 ByteBuddy 的时候了。事实上,我没有看到保留它的意义,因为 Frida 也能够重新实现我选择的方法。我几乎不知道这个功能会给我带来这么多麻烦......

目标 2 - 对象检查

拥有完整的堆栈跟踪很酷,但是如果传递给您感兴趣的方法(或其任何父方法)的参数是? 您不能只是打印出来并展示给审核员。它由许多实例变量组成,每个变量要么是“简单”类型(我的意思是,您可以直接打印)或复杂对象本身。 org.eclipse.jetty.server.Request

📷

再一次,Java 调试接口在那里救援。当断点命中时,每个参数都以在我的调试器中实现接口的对象的形式检索,这是对虚拟机中实际对象的引用。只要对象没有在主 JVM 中被垃圾收集,该引用就有效。Java 调试接口为对象提供了一组方法和属性,这使我能够递归地获取对复杂对象属性的引用,并使用Jackson以 JSON 格式输出每个对象,Jackson是一个流行的用于 JSON 格式化和对象检查的 Java 库。 com.sun.jdi.Value Value

完成后,我的工具使审核员能够在通过可疑方法时彻底检查调用堆栈,从而了解调用的来源以及对他通过应用程序提供的数据进行的操作。

目标 3 - 重新实现方法

如果堆栈跟踪看起来像这样:

  • input()
  • executeSafe(userData)
  • execute(userDataSanitized)

我希望我的工具对审计员尽可能方便,因此,我认为能够更改任何方法的实现会很酷。在前面的示例中,重写该方法可能会很有趣,以便它直接调用用户输入,而不需要清理部分。Frida 是完美的工具,所以我决定将它与 Java 调试接口结合使用。该工具的架构如下所示: executeSafe execute

📷

在这里,我发现自己遇到了另一面墙:Java 调试接口在字节码级别(在 JVM 中)起作用,而 frida-agent 在本机代码级别(修改 JVM 进程的程序流和内存)起作用。因此,两者结合起来很容易导致 JVM 崩溃。由于 Java 的 Frida 绑定的内部机制目前还没有文档,所以我花了很长时间调试这个问题,最后发现在使用 Frida 重新实现设置断点的方法时发生冲突(无论顺序如何两者中)。这是一个根深蒂固的问题,当时我没有找到解决办法,所以我把这个目标放在一边,继续前进。

目标 4 - 在主 JVM 上执行任意代码

尽管如此,我还是被 Frida 提供的可能性大肆宣传,并希望将其保留在我的项目中。这将有助于例如通过注入这样的脚本来模糊过滤器方法:

代码语言:javascript
复制
Java.perform(function () {
    var myList = ["fuz1z", "fuz2z", "fuz3z"];
    var myClass = Java.use("my.Class");
    var myMethod = myClass.filter.overload("java.lang.String")
    for (var i=0; i<myList.length; i++) {
        myResult = myMethod.invoke(myList[i]);
        if (myResult.indexOf("fuzz") > -1) {
            send(myList[i] + " bypasses the filter!");
        }
    }
    myClass.myMethod.overload("java.lang.String").invoke()
});

因此,我添加了一个在 JVM 中注入任意 Frida 脚本的功能,并在此功能的文档中添加了一个关于重新实现方法的重大警告。这很容易实现和测试。

因为我想让设置过程尽可能简单,所以这个功能是可选的,如果没有安装 Frida 并在主机上监听,该工具的其余功能运行完全正常。

目标 3,返回 - 设置方法调用的参数,模拟方法

在实习结束前几周,我有了重新引入 ByteBuddy 的想法,以恢复我的第三个目标,即重新实现方法。我想看看它是否与 Java 调试接口兼容。

ByteBuddy 是一个 Java 库,旨在简化本地 Java 代理的创建。本机 Java 代理是一个 Java 程序,其工作是在 JVM 中在运行时转换给定类或方法的字节码。它可以在启动时或之后附加到 JVM。ByteBuddy 提供类和方法,它们是库(例如 ASM)的包装器,它们本身就是原生 Java 字节码转换器方法的包装器。

为了重新实现方法,我使用 ByteBuddy 创建了一个简单的代理,并通过Maven插件将 ByteBuddy 依赖项捆绑在代理 JAR 文件中。这个插件是为经典的 JAR 文件而不是代理制作的,所以我必须在构建之后手动修改以添加代理运行所需的条目。然后,我在目标机器上手动安装了代理,并将其加载到 JVM 中。这让我可以试验 ByteBuddy 和 Java 调试接口之间的兼容性,这看起来很棒。 maven-assembly MANIFEST.MF

然后,我记得我想保持设置简单。这并不容易,因为代理 JAR 文件必须在主机上才能注入 JVM。我知道,当我们在安全评估期间遇到侦听开放端口的 Java Debug Wire Protocol(Java 调试接口使用的端口)服务时,我们可以轻松地从中获取 shell。因此,我将调试器编程为在可能的情况下获取 shell,并将 ByteBuddy 代理和启动器 JAR 文件发送到主机。完成后,调试器启动启动JAR,它将代理注入主 JVM。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 要求
  • 纯 JAVA 代理
  • 第一次接触项目
  • 目标 0 - 选择一个典型的目标
  • 目标 1 -完整的堆栈跟踪
  • 目标 2 - 对象检查
  • 目标 3 - 重新实现方法
  • 目标 4 - 在主 JVM 上执行任意代码
  • 目标 3,返回 - 设置方法调用的参数,模拟方法
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档