作者:Hu3sky@360CERT
0x01 漏洞简述
2020年08月13日, 360CERT监测发现Apache官方发布了Struts2远程代码执行漏洞的风险通告,该漏洞编号为CVE-2019-0230,漏洞等级:高危。漏洞评分:8.5。
攻击者可以通过构造恶意的OGNL表达式,并将其设置到可被外部输入进行修改,且会执行OGNL表达式的Struts2标签的属性值,引发OGNL表达式解析,最终造成远程代码执行的影响。
对此,360CERT建议广大用户及时将Apache Struts2进行升级完成漏洞修复。与此同时,请做好资产自查以及预防工作,以免遭受黑客攻击。
0x02 风险等级
360CERT对该漏洞的评定结果如下
| 评定方式 | 等级 |
|---|---|
| 威胁等级 | 中危 |
| 影响面 | 一般 |
| 360CERT评分 | 8.5分 |
0x03 影响版本
- Apache Struts2:2.0.0-2.5.20
0x04 漏洞详情
根据官方发布的信息来看,漏洞产生的主要原因是因为Apache Struts框架在强制执行时,会对分配给某些标签属性(如id)的属性值执行二次ognl解析,对于精心设计的请求,这可能导致远程代码执行(RCE)。
官方给出的利用场景如下:
<s:url var="url" namespace="/employee" action="list"/><s:a id="%{skillName}" href="%{url}">List available Employees</s:a>
这里的id属性里的值使用了ognl表达式进行包裹,同时该值如果可控,那么就会因为两次的解析而造成ognl表达式执行。
该分析在较低版本中进行测试,仅对漏洞产生原理进行分析,如在高版本中进行执行命令,需绕过沙箱。
拦截器处理请求值
首先,我们需要明确jsp是如何进行取值的。 在com.opensymphony.xwork2.interceptor.ParametersInterceptor这个拦截器里,会获取我们传入的值。 ActionContext存储着当前上下文的请求信息。 
ParametersInterceptor里,会把请求的值存入ValueStack类型的栈里,这里是OgnlValueStack,具体在setParameters里。 
stack的setValue方法进行赋值。 
action指向的jsp。然后开始处理对应的jsp标签。
id标签解析
在org.apache.struts2.views.jsp.ComponentTagSupport#doStartTag方法中。 
valueStack里获取相关的请求值,然后调用populateParams方法,该方法对标签进行处理,其中包括id标签。
于是跟进org.apache.struts2.components.Component#setId方法。 id属性不为null,于是继续跟进findString方法。 
findValue,由于altSyntax默认为true(这个功能是将标签内的内容当作OGNL表达式解析,关闭了之后标签内的内容就不会当作OGNL表达式解析了),所以最终进入translateVariables方法(高版本对应的是evaluate方法)。 
id,也就是%{skillName}进行ognl表达式执行,执行完剩余部分就是skillName。translateVariables代码如下:
public static Object translateVariables(char[] openChars, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
Object result = expression;
char[] arr$ = openChars;
int len$ = openChars.length;
for(int i$ = 0; i$ < len$; ++i$) {
char open = arr$[i$];
int loopCount = 1;
int pos = 0;
String lookupChars = open + "{";
while(true) {
int start = expression.indexOf(lookupChars, pos);
if (start == -1) {
int pos = false;
++loopCount;
start = expression.indexOf(lookupChars);
}
//防止递归解析
if (loopCount > maxLoopCount) {
break;
}
int length = expression.length();
int x = start + 2;
int count = 1;
while(start != -1 && x < length && count != 0) {
char c = expression.charAt(x++);
if (c == '{') {
++count;
} else if (c == '}') {
--count;
}
}
int end = x - 1;
if (start == -1 || end == -1 || count != 0) {
break;
}
String var = expression.substring(start + 2, end);
Object o = stack.findValue(var, asType);
if (evaluator != null) {
o = evaluator.evaluate(o);
}
String left = expression.substring(0, start);
String right = expression.substring(end + 1);
String middle = null;
if (o != null) {
middle = o.toString();
if (StringUtils.isEmpty(left)) {
result = o;
} else {
result = left.concat(middle);
}
if (StringUtils.isNotEmpty(right)) {
result = result.toString().concat(right);
}
expression = left.concat(middle).concat(right);
} else {
expression = left.concat(right);
result = expression;
}
pos = (left != null && left.length() > 0 ? left.length() - 1 : 0) + (middle != null && middle.length() > 0 ? middle.length() - 1 : 0) + 1;
pos = Math.max(pos, 1);
}
}
XWorkConverter conv = (XWorkConverter)((Container)stack.getContext().get("com.opensymphony.xwork2.ActionContext.container")).getInstance(XWorkConverter.class);
return conv.convertValue(stack.getContext(), result, asType);
}
然后调用stack.findValue方法从stackValue的context上下文中获取之前传入的该参数的具体值。 
expression值进行重新赋值,赋值为request请求传入的skillName参数的值,比如%{1+1},这里由于有如下判断:
if (loopCount > maxLoopCount) {
break;
}
此段代码用来防止递归解析ognl,所以最终将%{1+1}赋值给result后,会跳出循环,不再继续解析传入的值,接着往下执行:
XWorkConverter conv = (XWorkConverter)((Container)stack.getContext().get("com.opensymphony.xwork2.ActionContext.container")).getInstance(XWorkConverter.class);
return conv.convertValue(stack.getContext(), result, asType);
会获取上下文中的ContainerImpl,并实例化XWorkConverter: 
convertValue,该方法里也没有做ognl解析,判断value不为null并且类型和一开始预定的类型一致,就返回value。
id值二次解析
回退到setId,执行完findString后,将Ancohr.id重新赋值。 
return到doStartTag,执行完populateParams方法。 
Anchor.start。 
evaluateParams,这里会跟据标签做进一步操作。 在判断完一系列标签的值为null之后,会调用populateComponentHtmlId方法。 
id值,而这里的id值已经是之前经过populateParams方法处理过后的值。
findStringIfAltSyntax,传入id值。 根据altSyntax值,判断是否执行findString方法,进入findString之后的步骤就和前面对标签的ognl表达式执行一样,在translateVariables方法执行最终的ognl表达式。
总结
该漏洞主要是id标签的二次ognl解析产生的,第一次是在解析id标签属性的时候,这时候id的值已经被替换为用户输入,而第二次再次获取id值,此时的id值已经是用户可控的值,这时候就会解析用户输入的ognl表达式。
该漏洞限制条件较多:
-
Struts2标签的属性值可执行OGNL表达式(比如id)。 -
Struts2标签的属性值可被外部输入修改。 -
Struts2标签的属性值未经安全验证。 - 高版本需绕过沙箱执行命令。
-
useAltSyntax为true。
0x05 时间线
2020-08-13 Apache Struts2官方发布安全通告
2020-08-13 360CERT发布通告
2020-09-01 360-CERT 发布分析












发表评论
您还未登录,请先登录。
登录