Struts2 S2-001漏洞深入研究
Struts2工作原理
Suruts2的工作原理如下图
在该图中,一共给出了四种颜色的标识,其对应的意义如下。
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框架中的处理大概分为以下几个步骤
1、客户端初始化一个指向Servlet容器(例如Tomcat)的请求
2、这个请求经过一系列的过滤器(Filter)(这些过滤器中有一个叫做ActionContextCleanUp的可选过滤器,这个过滤器对于Struts2和其他框架的集成很有帮助,例如:SiteMesh
Plugin)
3、接着FilterDispatcher被调用,FilterDispatcher询问ActionMapper来决定这个请是否需要调用某个Action
漏洞分析
首先写一个很简单的Demo
我们先从struts2接收我们表单中提交的参数开始分析,代码位于com/opensymphony/xwork2/interceptor/ParametersInterceptor.java
程序获得我们表单中提交的username以及password
在获取表单提交参数后,进行set操作,为类中变量赋值
Set执行完之后,执行execute方法,此时username为%{1+1},password为123
跳过一部分的中间过程,execute方法最终会调用/org/apache/struts2/views/jsp/ComponentTagSupport.java对jsp标签进行逐一解析以构建返回页面。我们直接从解析传入payload的username标签开始
当解析到”<” 标签开始符合时,进入doStartTag方法
Struts2自定义标签类重写主要就是重写doStartTag()和doEndTag()方法。
doStartTag()方法是遇到标签开始时会呼叫的方法,doEndTag()方法是在遇到标签结束时呼叫的方法
当doStartTag()方法执行结束后,程序重新回到index.jsp <s:textfield name=”username” label=”username”/>这一行,此时解析到”/>”符号,接着进入doEndTag方法
在doEndTag方法中调用componet.end方法解析<s:textfield name=”username” label=”username” />,我们跟入这个方法
在end 方法中,可见调用evaluateParams方法,我们继续跟入该方法
在evaluateParams方法中,当this.name 不为null时,进入上图if分支
由于我们解析的为index.jsp中这一<s:textfield name=”username” label=”username”/>
此时的this.name值为”username”
在if (this.name != null)这一分支中调用this.findString方法,对传入findString方法中的this.name进行处理,并将返回的值赋值给name变量
由于这个name变量值以及findString方法对后文漏洞都有重要的影响,因此我们跟一下findString方法即name值获取的过程
跟入findString方法,此时传入findString方法的expr参数值为”username”
在findString方法中,调用translateVariables方法,此时传入translateVariables方法的expr变量值为”username”
跟入translateVariables方法
在translateVariables方法中存在一处while循环
在循环中,会判断expression是否以”%{”开头且以”}”结尾
当满足这个条件,程序将认为其是表达式,并进入如下if分支进行表达式计算处理
但是很显然,我们本次的expression为字符串”username”,并不满足条件,因此没有办法进入上图119行处的if分支,直接返回,见下图
在return后,程序继续回到evaluateParams方法中,此时if (this.name !=
null)已经执行结束,经过上文的分析,findString方法的原理以及name值的得来已经很清楚了。evaluateParams方法中除了if(this.name != null),还有一连串很多if分支
程序会继续执行后续的if,如下图
我们跳过中间这一长串if,直接看上图断点处
在altSyntax开启时,进入此分支
此时的name为”username”,即从上文if (this.name !=null)分支那个我们上文分析的那个if分支内获取来
上图红框中的Expr变量,是由name拼接”%{}”得来,因此值为”%{“username”}”
紧接着这个expr又在下一行代码中传入findValue,见下图
FindValue方法经过上文if (this.name != null)分支分析,大家一定已经很熟悉
FindValue方法最终会将expr即”%{“username”}”传入translateVariables方法
不同于上文那次,此时的expr可是以“%{“开头并且以”}”结尾,满足进入if ((start != -1) && (end != -1) && (count == 0))分支的条件
Expr经过下图120的substring取值操作,最后将”%{}”脱掉,将”username”赋值给var
接着var被传入Stack.findValue,我们跟入Stack.findValue方法
Stack.findValue如下图
stack.findValue对Ognl表达式进行getValue处理,在经过stack.findValue操作后将表单中username的值进行返回
程序找到我们提交的表单中username的值为”%{1+1}”,见下图
上图这个值就是我们构造的payload
O为stack.findValue的返回值,因此o为”%{1+1}”,见下图
接着o在字符拼接处理后,赋值给expression变量,见下图
由上图可见, expression 被拼接成 “%{1+1}”
注意,这时候我们还在while循环中,while循环仍没有结束
在这一次的循环中expression 为 “%{1+1}”,见下图
expression 为 “%{1+1}”
与上文流程一致,此时 “1+1”被取出赋值给var,见下图
Var被传递给stack.findValue,findValue中对”1+1”表达式进行getValue操作,见下图
stack.findValue将处理后的结果2赋值给o,见下图
expression由o拼接而来,拼接后的expression 为 2,见下图
我们仍然在while循环中,在这一次的循环中expression 为 2,见下图
此时expression已经不能满足”%{}”的格式,自然也不会进入后续if分支继续计算
因此break跳出while循环,将值进行return
此时return的username值为计算后的2
其实在弄清程序处理的步骤后,不难发现,其实struts2是这样执行的
struts2获得要处理的表单字段名,这里对应的是”username”
将字段名拼接成”%{}”形式,即”%{“username”}”
进入while循环,在while循环中,只要满足”%{}”形式,就将其中值取出进行Ognl
getValue操作,这里对应的就是取出username的value值,即我们构造的payload”%{1+1}”。
这个操作可以理解为用来取出表单字段对应的value操作由于取出的username值”%{1+1}”仍满足”%{}”形式,1+1即被取出传入进行Ongl
getValue操作,1+1最后返回值为2由于2不满足”%{}”形式,while循环停止,将2进行返回
程序构造返回页面,进行返回
一些想法
S2-001的官方描述如下
WebWork 2.1+和Struts 2的“
altSyntax”功能允许将OGNL表达式插入文本字符串并进行递归处理。这允许恶意用户通常通过HTML文本字段提交包含OGNL表达式的字符串,如果表单验证失败,该字符串将由服务器执行。
但是只有表单验证失败才会触发漏洞吗?
接下来我们做个实验验证
首先修改了下后台代码
结果表明Success与否,都返回index.jsp
表达式仍然可以执行,可见与success无关