Struts2-s01漏洞复现分析
漏洞描述
该漏洞因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用 OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。例如注册或登录页面,提交失败后端一般会默认返回之前提交的数据,由于后端使用 %{value} 对提交的数据执行了一次 OGNL 表达式解析,所以可以直接构造 Payload 进行命令执行。
受影响版本
WebWork 2.1 (with altSyntax enabled), WebWork 2.2.0 - WebWork 2.2.5, Struts 2.0.0 - Struts 2.0.8
复现环境
maven(struts-2.0.8)+tomcat7
漏洞缺陷位置
TextParseUtil.translateVariables
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
// deal with the "pure" expressions first!
//expression = expression.trim();
Object result = expression;
//漏洞根本原因,未设置合理退出条件,导致ognl表达式递归解析
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条件是唯一退出循环条件
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 {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
} else {
break;
}
}
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
审计分析
流程分析
该ognl流程图对应漏洞缺陷行位置贴图代码块
代码分析及漏洞复现
漏洞核心
- struts2存在ognl递归解析缺陷
- ognl能执行代码
这个是ognl简单实用的测试代码
OgnlContext context = new OgnlContext();
Object obj = Ognl.getValue("'helloworld'.length()",context);
System.out.println(obj);
Object obj1 = Ognl.getValue("@java.lang.String@format('foo %s','bar')",context);
System.out.println(obj1);
Object obj2 = Ognl.getValue("@java.lang.Runtime@getRuntime().exec('open /Applications/Calculator.app/')",context);
该调用栈实现逻辑和上面的测试代码逻辑相同
断点设置(重要)
com.opensymphony.xwork2.DefaultActionProxy
resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);
result.execute(this);
com.opensymphony.xwork2.util.TextParseUtil
Object result = expression;
Object o = stack.findValue(var, asType);
org.apache.struts2.dispatcher.ServletDispatcherResult
dispatcher.forward(request, response);
org.apache.struts2.components.UIBean
expr = "%{" + name + "}";
断点设置好后开始观察数据流
- 使用poc进行测试
- 可以看到数据流首先经过很多过滤器
- 经过层层过滤器后进入到我们自己编写的代码位置,可以看到%{1+1}已经传递过来了
- 根据execute方法逻辑可知返回ERROR,进入下一个断点,可以看到resultCode=error,再根据我们自己配置的struts.xml可以看到返回error则转发到index.jsp页面
- 继续执行下一个断点,等到expression等于username,username是前端传递给后端的值的key,ongl引擎对其使用%{}包裹然后对其解析
- 在跟踪到下一个断点,进入到ognl解析逻辑,可以看到根据前端传递的username字段解析获取到传递过来的值%{1+1}
- 继续执行,发现%{1+1}被ognl继续解析,造成ognl表达式注入
- 最后可以看到最终返回的数据是2