javascript
Spring Data REST 远程代码执行漏洞(CVE-2017-8046)分析与复现
前言
2009年9月Spring 3.0 RC1發布后,Spring就引入了SpEL(Spring Expression Language)。對于開發者而言,引入新的工具顯然是令人興奮的,但是對于運維人員,也許是噩耗的開始。類比Struts 2框架,會發現絕大部分的安全漏洞都和ognl脫不了干系。尤其是遠程命令執行漏洞,占據了多少甲方乙方工程師的夜晚/周末,這導致Struts 2越來越不受待見。
因此,我們有理由相信Spring引入SpEL必然增加安全風險。事實上,過去多個Spring CVE都與其相關,如CVE-2017-8039、CVE-2017-4971、CVE-2016-5007、CVE-2016-4977等。
本文分析的CVE-2017-8046同樣也與SpEL有關。如果急于查看自己的應用是否受影響和修復建議,請查看官方公告,或者跳至0x07漏洞修復。
Spring Data REST簡介
Spring Data REST是Spring Data的一個子項目。關于Spring Data,引用官方介紹如下: > Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store.
It makes it easy to use data access technologies, relational and non-relational databases, map-reduce frameworks, and cloud-based data services. This is an umbrella project which contains many subprojects that are specific to a given database. The projects are developed by working together with many of the companies and developers that are behind these exciting technologies.
一句話概括:Spring Data是對數據訪問的更高抽象。通過它,開發者進一步從數據層解放出來,更專注于業務邏輯。不管是關系型數據還是非關系型數據,利用相應接口,開發者可以使用非常簡單的代碼構建對數據的訪問(當然,Spring Data還有很多特性和功能,感興趣的可參考官方文檔)。
回過頭看Spring Data REST,它是一個構建在Spring Data之上,為了幫助開發者更加容易地開發REST風格的Web服務,官方聲稱完成demo只需15分鐘。
官方提供的Demo
參照官方文檔,筆者使用Maven構建Spring-boot應用,數據庫為H2 Database。
1) 添加依賴,pom.xml內容來自官方示例文檔。 2) 編寫實體類Person。
//import 省略@Entity public class Person {@Id@GeneratedValue(strategy = GenerationType.AUTO)private long id; //自增主健private String firstName;private String lastName; //getter setter省略 }3) 編寫接口。
//import 省略//在/people處創建RESTful入口點 @RepositoryRestResource(collectionResourceRel = "people", path = "people") public interface PersonRepository extends PagingAndSortingRepository<Person, Long> { //接口繼承了PagingAndSortingRepository,此接口封裝了對Person實體類的CURD,并且具備分頁和排序 }4) Spring Boot執行入口。
//import 省略@SpringBootApplication public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);} }5) 編譯運行。
數據操作測試
1)測試是否成功
2)使用POST方法添加一個數據
3)查看新加入的數據
4)使用PATCH請求方法更新數據
對于JSON Patch請求方法IETF制定了標準RFC6902。JSON Patch方法提交的數據必須包含一個path成員,用于定位數據,同時還必須包含op成員,可選值如下:
| add | 添加數據 |
| remove | 刪除數據 |
| replace | 修改數據 |
| move | 移動數據 |
| copy | 拷貝數據 |
| test | 測試給定數據與指定位置數據是否相等 |
比如對于上面添加的Person數據,修改其lastName屬性,請求數據如下: > [{ “op”: “replace”, “path”: “/lastName”, “value”: “Zhang” }]
有兩點需要注意:
① 必須將Content-Type指定為application/json-patch+json。 ② 請求數據必須是json數組。
漏洞分析
漏洞分析涉及的源碼比較多,為了減少歧義和減小篇幅,約定兩點: ① 代碼以片段[a-z]標識; ② 提到某個方法不會包含完整的方法簽名,僅提供方法名,需聯系上下文識別。
1)根據官方公告,結合GitHub 的commit,猜測漏洞出在path參數值的處理上。嘗試提交非法的path參數值,查看異常堆棧信息:
at org.springframework.expression.spel.ast.MethodReference$MethodValueRef.setValue(MethodReference.java:355) ~[spring-expression-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.expression.spel.ast.CompoundExpression.setValue(CompoundExpression.java:95) ~[spring-expression-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:438) ~[spring-expression-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.data.rest.webmvc.json.patch.PatchOperation.setValueOnTarget(PatchOperation.java:167) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.json.patch.ReplaceOperation.perform(ReplaceOperation.java:41) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.json.patch.Patch.apply(Patch.java:64) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.config.JsonPatchHandler.applyPatch(JsonPatchHandler.java:91) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.config.JsonPatchHandler.apply(JsonPatchHandler.java:83) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:206) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.read(PersistentEntityResourceHandlerMethodArgumentResolver.java:184) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.resolveArgument(PersistentEntityResourceHandlerMethodArgumentResolver.java:141) ~[spring-data-rest-webmvc-2.6.6.RELEASE.jar:na]at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:158) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:128) ~[spring-web-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:97) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:827) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:738) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:967) ~[spring-webmvc-4.3.10.RELEASE.jar:4.3.10.RELEASE]//省略部分堆棧信息2)既然是Patch請求方法,我們從org.springframework.data.rest.webmvc.config.JsonPatchHandler.apply(JsonPatchHandler.java:83)入手分析。
//片段a:public <T> T apply(IncomingRequest request, T target) throws Exception {Assert.notNull(request, "Request must not be null!");Assert.isTrue(request.isPatchRequest(), "Cannot handle non-PATCH request!");Assert.notNull(target, "Target must not be null!");if (request.isJsonPatchRequest()) {//return applyPatch(request.getBody(), target);} else {return applyMergePatch(request.getBody(), target);}}片段a中的if判斷決定了請求Content-Type須指定application/json-patch+json。
//片段b: public boolean isJsonPatchRequest() {return isPatchRequest() //是否是PATCH請求方法//Content-Type是否與application/json-patch+json兼容&& RestMediaTypes.JSON_PATCH_JSON.isCompatibleWith(contentType);}片段a中的if判斷為true的話,進入applyPatch方法:
//片段c: @SuppressWarnings("unchecked") <T> T applyPatch(InputStream source, T target) throws Exception {return getPatchOperations(source).apply(target, (Class<T>) target.getClass()); }跟進getPatchOperations方法:
//片段d: private Patch getPatchOperations(InputStream source) {try {return new JsonPatchPatchConverter(mapper).convert(mapper.readTree(source));//通過Jackson 生成對應的對象實例} catch (Exception o_O) {throw new HttpMessageNotReadableException(String.format("Could not read PATCH operations! Expected %s!", RestMediaTypes.JSON_PATCH_JSON), o_O);} }片段d通過Jackson實例化對象,我們看看相關構造函數:
//片段e: public Patch(List<PatchOperation> operations) {this.operations = operations; } //片段f: public PatchOperation(String op, String path, Object value) {this.op = op;this.path = path;this.value = value;this.spelExpression = pathToExpression(path); }對于PatchOperation對象,成員spelExpression根據path轉化而來,這一點對于PoC構造非常重要,筆者一開始就坑在這里。 pathToExpression完整的調用鏈比較長,影響PoC的構造關鍵在于下面兩個方法。
//片段g: private static String pathToSpEL(String path) {return pathNodesToSpEL(path.split("\\/"));//跟據斜杠分割成字符數組 } //片段h: private static String pathNodesToSpEL(String[] pathNodes) {StringBuilder spelBuilder = new StringBuilder();for (int i = 0; i < pathNodes.length; i++) {String pathNode = pathNodes[i];if (pathNode.length() == 0) {continue;}if (APPEND_CHARACTERS.contains(pathNode)) {if (spelBuilder.length() > 0) {spelBuilder.append(".");}spelBuilder.append("$[true]");continue;}try {int index = Integer.parseInt(pathNode);spelBuilder.append('[').append(index).append(']');} catch (NumberFormatException e) {if (spelBuilder.length() > 0) {//使用.拼接字符數組//如筆者嘗試執行touch /tmp/file,spelBuilder.append('.'); //并未在/tmp中發現file文件,后來發現應用目錄中多了隱藏文件,} //原因就在此處spelBuilder.append(pathNode);}}String spel = spelBuilder.toString();if (spel.length() == 0) {spel = "#this";}return spel; }回到片段C,繼續看apply:
//片段i: public <T> T apply(T in, Class<T> type) throws PatchException {for (PatchOperation operation : operations) {operation.perform(in, type);}return in; }在RFC6902的標準中,一次PATCH請求允許多個操作,比如:
[{ "op": "test", "path": "/a/b/c", "value": "foo" },{ "op": "remove", "path": "/a/b/c" },{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } ]對于上面的請求數據,將會順序執行test、remove、add操作(當前操作的”文檔”為上一次操作更新后的”文檔”)。
因此,在代碼片段i中循環每一個”操作”。假設我們提交了一個PATCH請求op為replace,我們接著看PatchOperation子類ReplaceOperation的perform方法:
//片段j: <T> void perform(Object target, Class<T> type) {setValueOnTarget(target, evaluateValueFromTarget(target, type)); }調用父類PatchOperation的evaluateValueFromTarget方法:
//片段k: protected <T> Object evaluateValueFromTarget(Object targetObject, Class<T> entityType) {return value instanceof LateObjectEvaluator? ((LateObjectEvaluator) value).evaluate(spelExpression.getValueType(targetObject)) : value; }官方在evaluateValueFromTarget方法中打了補丁,補丁的修復邏輯是檢查路徑是否合法,如果不合法則會拋出PatchException。完整的補丁信息可以從GitHub看對應commit。
//片段l:protected <T> Object evaluateValueFromTarget(Object targetObject, Class<T> entityType) { - return value instanceof LateObjectEvaluator - ? ((LateObjectEvaluator) value).evaluate(spelExpression.getValueType(targetObject)) : value; + verifyPath(entityType); + + return evaluate(spelExpression.getValueType(targetObject)); + }++ protected final <T> Object evaluate(Class<T> type) { + return value instanceof LateObjectEvaluator ? ((LateObjectEvaluator) value).evaluate(type) : value; + }++ /** + * Verifies that the current path is available on the given type. + * + * @param type must not be {@literal null}. + * @return the {@link PropertyPath} representing the path. Empty if the path only consists of index lookups or append + * characters. + */+ protected final Optional<PropertyPath> verifyPath(Class<?> type) { + + String pathSource = Arrays.stream(path.split("/"))// + .filter(it -> !it.matches("\\d")) // no digits + .filter(it -> !it.equals("-")) // no "last element"s + .filter(it -> !it.isEmpty()) // + .collect(Collectors.joining(".")); + + if (pathSource.isEmpty()) { + return Optional.empty(); + } + + try { + return Optional.of(PropertyPath.from(pathSource, type)); //根據對象和路徑獲取PropertyPath + } catch (PropertyReferenceException o_O) { + throw new PatchException(String.format(INVALID_PATH_REFERENCE, pathSource, type, path), o_O); + }}回過頭看代碼片段j,setValueOnTarget再往后走就是SpEL解析了。由于SpEL非該漏洞核心,本文不再深入。
漏洞復現
明白了漏洞原理之后,復現就非常簡單了。注入表達式沒有太多限制。
漏洞修復
漏洞在9月21日披露,雖然定位為嚴重。但是筆者持續跟蹤,并未發現國內哪些站點在跟進,不排除攻擊者利用此漏洞攻擊未打補丁的受影響應用。
漏洞信息來源于官方公告。
值得注意的是,本次漏洞問題出現在 spring-data-rest-webmvc中。由于Spring 提供內建的依賴解決,因此可能并不會在依賴配置文件(如Maven的pom.xml)顯式看到 spring-data-rest-webmv的依賴配置,這就是為什么官方公告還提及Spring Boot和Spring Data的緣故。
漏洞觸發條件:網站使用Spring Data REST提供REST Web服務,版本在受影響范圍內。
修復建議:及時升級。
參考鏈接
總結
以上是生活随笔為你收集整理的Spring Data REST 远程代码执行漏洞(CVE-2017-8046)分析与复现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 前端遇上Go: 静态资源增量更新的新实践
- 下一篇: 阿里P8架构师谈:java架构师面试技能