报告编号: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 发布分析