漏洞成因
正常情况下访问/article并输入数字型id即可获取文章内容,但如果传入了spel表达式,则会导致转到错误页面同时对spel表达式内容进行解析并反应在错误页面中。
debug过程
同样从DispatcherServlet.doDispatch()函数开始,很快就定位到了关键类:即PropertyPlaceholderHelper类。
在DispatcherServlet中由于以下判定报错
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
...
}
报错内容为NumberFormatException,即将输入的值转化为数字错误。
先看PropertyPlaceholderHelper.parseStringValue()方法
rotected String parseStringValue(String strVal, PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {
StringBuilder result = new StringBuilder(strVal);
int startIndex = strVal.indexOf(this.placeholderPrefix);
while(startIndex != -1) {
int endIndex = this.findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(placeholder)) {
throw new IllegalArgumentException("Circular placeholder reference '" + placeholder + "' in property definitions");
}
placeholder = this.parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
//这里传入
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;
}
}
}
......
}
return result.toString();
}
之后就走会返回view,而view中包含了 ${timestamp}、${error}、${status}、${message},view会通过循环遍历判断值是否以 “${” 开头
只要是 “${” 开头,就会进入spel表达式执行阶段。但当我们把message的value设置为payload,也是 ‘${’ 开头的,则也会执行payload,他没有对可控参数message的值进行校验。
之后通过ErrorMvcAutoConfiguration.resolvePlaceholder()方法来对spel表达式解析并获取值。
这一步即获取status的值。而当到达payload时,则会解析成Runtime类并执行exec方法,这一步为反射获取到Runtime实例
总结
由于传入的值会判断是否为数字,如果不为数字则会报错,走入报错页面,而报错页面是包含了一些spel表达式的,所以会对报错页面内的spel表达式进行循环查找并解析出来,最终渲染给页面。但出问题的原因是,${messgae}的值如果也是一个spel表达式,那么它会继续循环解析这个表达式,从而达到了命令注入,也即未对用户可控参数进行校验。
修复建议
跟其原因,是未对id进行spel方向的过滤。因此我总结建议为以下几点:
- 对id进行过滤,设置黑白名单,过滤如 ${ 等的值
- 升级框架版本
- 打补丁
- 自定义报错页面