作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/yntuSGAggpY89DwtXxaWpg

1.SPEL簡介

SPEL(Spring Expression Language),即Spring表達式語言,是比JSP的EL更強大的一種表達式語言。從Spring 3開始引入了Spring表達式語言,它能夠以一種強大而簡潔的方式將值裝配到Bean屬性和構造器參數中,在這個過程中所使用的表達式會在運行時計算得到值。使用SPEL你可以實現超乎想象的裝配效果,這是其他裝配技術很難做到的。

2.SPEL使用

SPEL的使用可以分為兩種方式,第一種是在注解中進行使用,另一種是通過SPEL組件提供的接口來進行解析。

在注解中使用的情況

//@Value能修飾成員變量和方法形參
//#{}內就是SPEL表達式的語法
//Spring會根據SPEL表達式語法,為變量arg賦值
@Value("#{表達式}")
public String arg;
//將"hello"字符串賦值給word變量
@Value("hello")
private String word; 
//從網址"http://www.baidu.com"獲取資源
@Value("http://www.baidu.com")
private Resource url; 

通過接口的使用情況

   public static void main(String[] args) {
     //實例化表達式解析對象
        ExpressionParser parser = new SPELExpressionParser();
     //調用該對象的parseExpression方法來執行表達式
        Expression expres = parser.parseExpression("3*3");
     //獲取表達式的執行結果,想要返回的結果類型可以以這種Type.class的形式傳入
        String message = expres.getValue(String.class);
        System.out.println(message);
    }

這段代碼是執行一段簡單的SPEL表達式“3*3”,最終執行結果如下所示

11

SPEL表達式還能執行一些更復雜的命令,例如對一個對象進行操作,代碼如下所示,首先是一個pojo類

public class User {
    public String userName;
    public User() {
    }
    public User(String userName) {
        this.userName = userName;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String sayHi(String name){
        return name+" say: Hi";
    }
      public static String sayBye(String userName){
        return userName+"say: Bye";
    }
}

然后是通過SPEL表達式操作user對象的屬性

User user = new User();
//實例化表達式解析對象
ExpressionParser parser = new SPELExpressionParser();
//實例化上下文,將user對象作為參數傳入,這樣就可以操作user對象的屬性了
StandardEvaluationContext context = new StandardEvaluationContext(user);
/**
如果不想在實例化上下文的時候就傳入對象的話就可以使用下面的代碼進行等價替換

StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(user);

之所以可以這么替換是因為StandardEvaluationContext在構造方法中還是通過調用了setRootObject方法
通過setRootObject方法傳入的參數會被放入StandardEvaluationContext.rootObject屬性中

*/

//向上下文中添加元素
context.setVariable("newUserName","Jone");
//這里的userName就是user.userName屬性,#newUserName就是上一步中添加的,newUserName為key,而value為Jone,所以這一步是將newUserName的值賦值給user.userName屬性
parser.parseExpression("userName=#newUserName").getValue(context);
System.out.println(user.getUserName());

//這一步是通過SPEL表達式直接給user.userName屬性賦值
parser.parseExpression("userName='Tom'").getValue(context);
System.out.println(user.getUserName());

//通過setVariable傳入上下文中的參數會被放入StandardEvaluationContext.variables屬性中,該屬性為HashMap類型,傳入的字符串“user”,就是他的key值,value就是user這個對象
context.setVariable("user",user);

//通過setVariable方法存放入上下文中的對象,就可以通過 #+key+屬性的方式進行調用
String name = (String)parser.parseExpression("#user.userName").getValue(context);
//通過setVariable方法傳入的對象和通過setRootObject方法傳入的對象是不一樣的,通過setRootObject傳入的對象可以直接通過“屬性名稱”來進行調用,而通過setVariable方法傳入的對象,只能通過“#+key+屬性的方式進行調用”

可以操作對象的屬性SPEL同樣也可以操作對象的方法,例如我們的pojo類User中就有一個成員方法sayHi,和一個靜態方法sayBye,我們使用SPEL表達式來分別調用一下

首先是調用成員方法,也就是動態方法

User user = new User();
ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(user);
context.setVariable("user",user);
//如下可以使用 #+Key+MethodName的形式進行調用
//這種方法不僅可以調用動態方法,也可以調用靜態方法
String result = (String) parser.parseExpression("#user.sayHi('jack')").getValue(context);
System.out.println(result);

運行結果如下

12

然后是調用靜態方法,代碼如下所示

ExpressionParser parser = new SPELExpressionParser();
//使用“T(Type)”來表示java.lang.Class類的實例,即如同java代碼中直接寫類名。此方法一般用來引用常量或靜態方法
String result = parser.parseExpression("T(com.SPEL.pojo.User).sayBye('Jack')").getValue(String.class);

System.out.println(result);

除了以上兩種方可以調用靜態方法以外還有一種方法

ExpressionParser parser = new SPELExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
//通過反射拿到User類的sayBye方法對象,
Method sayBye = User.class.getMethod("sayBye", String.class);
//將sayBye方法對象注冊進上下文中
context.registerFunction("sayBye",sayBye);
//然后就可以通過#+MehtondName的形式進行調用
String result = (String) parser.parseExpression("#sayBye('jack')").getValue(context);
        System.out.println(result);

3.CVE-2016-4977 漏洞分析

根據網上爆出得漏洞相關信息,POC如下所示

http://your-ip:8080/oauth/authorize?response_type=${233*233}&client_id=acme&scope=openid&redirect_uri=http://test

目前我們對漏洞的詳細情況一無所知,首先我們根據請求路徑的映射,找到后來用來接收該請求的方法,經過一番搜索我門找到了“/oauth/authorize”這個路徑映射的是AuthorizationEndpoint.authorize方法。

1

我們在該方法中打上斷點,然后發送poc即可看到程序執行到斷點處,這里有一個需要注意的值,就是errorPage這個屬性的值,其值為“forward:/oauth/error”,這個值后續會使用到。

2

程序往下執行,來到一個if判斷,這里判斷的值就是我們poc中傳遞的response_type值,這里主要判斷response_type的值是不是“token”或者“code”,很明顯不是,這里傳遞的response_type的值是“${3*10}”,所以會拋出一個“Unsupported response types”,也就是“不支持的返回類型錯誤”。

3

然后就是一系列的異常操作,沒什么特別值得講的,接下來我們的斷點下在DispatherServlet.processDispatchResult方法里,由于之前在AuthorizationEndpoint.authorize方法中執行出現了異常,所以Spring Security會返回一個認證錯誤的執行頁面,而跳轉的方式和地址就是我們剛才看到的errorPage這個屬性的值,也就是“forward:/oauth/error”,這里指定了跳轉方式,和跳轉的路徑,跳轉方式為“forward”,也就是服務器內部跳轉,而跳轉的路徑就是“/oauth/error”,后續的執行就是Spring Security在內部。最終發起轉發的位置在哪呢?在InternalResourceView.renderMergedOutputModel方法中,

4

可以看到真正出發服務器內部轉發的代碼是最后一行的rd.forward(request, response);,rd變量是一個ApplicationDispatcher對象,ApplicationDispatcher.forward方法的作用就是處理服務器內部的請求轉發,而需要請求的路徑"/oauth/error" 在執行getRequestDispatcher方法中傳入了進去 并最終返回一個ApplicationDispatcher對象,然后調用了ApplicationDispatcher.forward方法進行服務器內部請求轉發,這個轉發的過程就不做過多贅述了,不是我們研究的重點。

現在我們已知轉發的路徑為"/oauth/error",那我們就去搜索這個路徑,經過搜索找到的該路徑對應的方法,為WhitelabelErrorEndpoint.handleError方法。

5

這里我們需要留意的就是這個error變量,可以看到就是之前在AuthorizationEndpoint.authorize方法中拋出的Unsupportedresponsetypes異常,其中有一個detailMessage屬性,其中封裝的是一段字符串,而該段字符串中的${3*10}就是SPEL表達式,也是我們在poc中傳遞的response_type的值,而ERROR中的${error.summary}同樣也是SPEL表達式,而ERROR屬性則被傳入了SPELVIew的構造方法中,進而生成了一個SPELView對象,該類從類型來分析很明顯是用于處理SPEL表達式的,我們跟進該類。

SPELVIew在構造方法中實例化了一個匿名內部類對象并賦值給了resolver屬性,這個對象就是SPEL代碼執行的核心.為什么說這個PlaceholderResolver.resolvePlaceholder方法是核心關鍵就在于用紅圈圈里來的這段代碼。即Expression expression = parser.parseExpression(name); 這段代碼的作用就是解析和執行SPEL表達式,至于parser屬性是什么類型也可以截圖看一下,從截圖中看到是SPELExpressionParser類型。

6

7

在該處下斷點,看下執行結果

8

這里看到parser屬性是SPELExpressionParser類型,結合之前的SPEL使用介紹,可知這里就是要解析SPEL表達式了,而傳入的name變量就是要解析的SPEL表達式,這個SPEL表達式就是“error.summary”,那么這個error是什么呢?在WhitelabelErrorEndpoint.handleError方法中,可以看到error就是封裝進去的UnSupportedResponseTypesExpection對象,而UnSupportedResponseTypesExpection的父類OAuth2Exception有一個名為getSummary的方法,而在之前的截圖中看到在SPELView.render方法中,調用了StandardEvaluationContext.setRootObject,傳入的參數是一個Hashmap對象, 當map對象像以setRootObject方法傳入SPEL上下文中的時候,就可以以key.valueProperty/valueMethod的形式進行反射調用,也就是反射調用屬性或者調用對應的getter方法,注意這里能通過反射調用的方法只有getter方法,測試代碼如下所示

public class ErrorImpl {

    public String summary = "hello world1";

    public String getSummary() {
        return "hello world";
    }

//    public String setSummary() {
////        this.summary = summary;
//        return "hello world3";
//    }

    public String sayHello(){
        return "say world";
    }
}
public class SPELTest2 {

    public static void main(String[] args) {
        ExpressionParser parser = new SPELExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        ErrorImpl error = new ErrorImpl();
        Map<String, Object> model = new HashMap<String, Object>();
        model.put("error", error);
        context.setRootObject(model);
        context.addPropertyAccessor(new MapAccessor());
        Expression expression = parser.parseExpression("error.summary");
        Object value = expression.getValue(context);
        System.out.println(value.toString());
    }
}

所以解析“error.summary”這個SPEL表達式最終就會調用到OAuth2Exception.getSummary方法,最終得到的值如下所示

9

最終的到的value是一串字符串,而在這段字符串中,屬于SPEL表達式的是“${3*10}”,如此以來就到達了代碼執行的位置,執行結果如下圖所示10

最終執行的結果會返回至前端頁面,至此spring-security-oauth2 SPEL表達式注入漏洞分析完畢

4.總結

其實經過以上分析,大家不難發現,可以執行代碼和對類進行操作是SPEL表達式模塊所提供的正常功能,但是問題出在哪呢?就出在了Spring-oauth2這個模塊對response_type這個參數校驗的不嚴格,在后續的操作中,僅僅只是將外部的“$"符號和“{}”進行了刪除,除此以外就沒有進行任何有效的過濾了,所以,表達式注入漏洞就產生了。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1694/