报告编号:B6-2020-090103
报告来源:360CERT
报告作者:Hu3sky
更新日期:2020-09-01
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 发布分析