S2-001
S2-001
漏洞介绍
Struts2对OGNL表达式解析使用了开源组件 opensymphony.xwork
组件,OGNL解析代码实际上位于XWork中,而不是 WebWork2 或Struts2 中
该漏洞是由于WebWork 2.1+ 和 Struts 2 的"altSyntax"特性引起的。altSyntax特性允许将OGNL表达式插入文本字符串并进行递归处理。这允许恶意用户通过HTML文本字段提交一个包含OGNL表达式的字符串,如果表单验证失败,服务器将执行该表达式。
影响版本
WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
环境搭建
这里学习 nivia 师傅的环境搭建方法,下载 vulhub 靶场,然后参考 war 包运行:Tomcat 服务搭建,
但是这个教程只是能运行 war 包,还不能调试,如果直接搜索调试的话是类似 IDEA 调试 jar 包这个这种调试,没什么用,这里可以直接把解压后的文件配置依次复制过来,然后选择部署 facet,这样就行了。
还可以直接配置远程调试。
漏洞复现
访问漏洞环境,
POST:username=%{1+1}&password=1
漏洞分析
调用栈,
在相应的漏洞版本下,核心Filter为org.apache.struts2.dispatcher.FilterDispatcher
,从web.xml中也可以看出
将 Action 类的相关属性放到 ValueStack
那么在 org.apache.struts2.dispatcher.FilterDispatcher#dofilter
处下断点,先获得 Actionmapping,
然后 mapping
不为 null,调用 dispatcher#serviceAction
方法,
该方法始化extraContext
,封装了session、HttpServletRequest、HttpServletResponse等信息。获取ValueStack,调试时为null。
初始化ActionProxy。proxy.getInvocation().getStack()
初始化 ValueStack
,通过 setAttribute
方法封装进 request
,
调用 ActionProxy#execute
,这里对应会调用到 ActionInvocation#invoke
,这里开始执行拦截器
调用拦截链的拦截器
其中到 ParametersInterceptor
拦截器时跟进 doIntercept
方法,是获取对应Action,获取ActionInvocation
和parameters
,parameters对应请求中的参数。由于parameters不为null,这里会获取StackValue,调用setParameters方法
在 ParametersInterceptor#setParameters
方法中,会遍历这个 parameters
,将键值对存进Stack中,对应 OgnlValueStack#setValue
方法,这个过滤器就是单纯存个参数值,(这里也可以注意一下就是后面的 s2-003
漏洞)
然后调用到 DefaultWorkflowInterceptor#doIntercept
方法,在这个方法最后调用了ActionInvocation#invoke
调用完所有拦截器后调用 Action 方法
跟进然后一路调用到了 invokeActionOnly()
方法,这里开始调用对应Action#execute
方法了,
来到了 LoginAction#execute
,也就是自己写的逻辑,这里会执行失败返回 error
表单执行失败处理
看到 struts.xml
配置中如果返回 error 就会回到 index.jsp
页面
返回结果以后就开始调用 ActionInvocation#executeResult
方法,调用Result实现类里的execute方法开始处理请求结果,
然后一路调用,处理结果
如果返回结果是一个 jsp 文件,则会调用 JspServlet
来处理请求,调用栈如下(用一下nivia师傅的图)
最后交由 Struts 来处理解析相关的标签,
例如在解析Struts标签如<s:textfield name="username" label="username" />
,在标签的开始和结束位置,会分别调用对应实现类中的doStartTag
和doEndTag
方法:
- doStartTag:获取一些组件信息和属性赋值,总之是些初始化的工作
- doEndTag:在标签解析结束后需要做的事,如调用组件的end
这里会调用到ComponentTagSupport
这个实现类,漏洞触发点位于ComponentTagSupport#doEndTag
当解析 <s:textfield name="username" label="username" />
,doEndTag
方法会getBean方法获取到TextField对象赋值给 component
,调用其end方法
跟进 evaluateParams
方法,判断是否开启altSyntax
,若开启会套上一层%{}
跟进 findValue
方法,满足条件调用 TextParseUtil.translateVariables
方法
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
Object result = expression;
while (true) {
int start = expression.indexOf(open + "{");
int length = expression.length();
int x = start + 2;
int end;
char c;
int count = 1;
while (start != -1 && x < length && count != 0) {
c = expression.charAt(x++);
if (c == '{') {
count++;
} else if (c == '}') {
count--;
}
}
end = x - 1;
if ((start != -1) && (end != -1) && (count == 0)) {
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);
if (o != null) {
if (TextUtils.stringSet(left)) {
result = left + o;
} else {
result = o;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + o + right;
} else {
result = left + right;
expression = left + right;
}
} else {
break;
}
}
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
方法使用while true的方式递归执行ognl表达式,执行逻辑位于 Object o = stack.findValue(var, asType);
,解析过后将解析结果替换回原来的表达式中,继续第一步,直到不出现 %{}
,
比如第一次 %{username}
,
解析得到,
然后回替换回原来表达式,因为还有 %{}
,所以会继续解析,造成二次解析,再次调用 stack.findValue(var, asType)
方法,
最后执行命令。