Struts2 S2-001漏洞深入研究

Struts2工作原理

Suruts2的工作原理如下图

lARuod.png

在该图中,一共给出了四种颜色的标识,其对应的意义如下。

  • 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

lAtluV.png

lAtAHS.png

lAtJN4.png

lAt3HU.png

我们先从struts2接收我们表单中提交的参数开始分析,代码位于com/opensymphony/xwork2/interceptor/ParametersInterceptor.java

lAtDHO.png

程序获得我们表单中提交的username以及password

在获取表单提交参数后,进行set操作,为类中变量赋值

lAYzhd.png

Set执行完之后,执行execute方法,此时username为%{1+1},password为123

lAtp9A.png

跳过一部分的中间过程,execute方法最终会调用/org/apache/struts2/views/jsp/ComponentTagSupport.java对jsp标签进行逐一解析以构建返回页面。我们直接从解析传入payload的username标签开始

lAtKcq.png

当解析到”<” 标签开始符合时,进入doStartTag方法

lAR0Wq.png

Struts2自定义标签类重写主要就是重写doStartTag()和doEndTag()方法。

doStartTag()方法是遇到标签开始时会呼叫的方法,doEndTag()方法是在遇到标签结束时呼叫的方法

当doStartTag()方法执行结束后,程序重新回到index.jsp <s:textfield name=”username” label=”username”/>这一行,此时解析到”/>”符号,接着进入doEndTag方法

lARZLD.png

在doEndTag方法中调用componet.end方法解析<s:textfield name=”username” label=”username” />,我们跟入这个方法

lAt28A.png

在end 方法中,可见调用evaluateParams方法,我们继续跟入该方法

lARNwQ.png

在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”

lARJOS.png

在findString方法中,调用translateVariables方法,此时传入translateVariables方法的expr变量值为”username”

跟入translateVariables方法

lAR8Qf.png

在translateVariables方法中存在一处while循环

lAtu3n.png

在循环中,会判断expression是否以”%{”开头且以”}”结尾

当满足这个条件,程序将认为其是表达式,并进入如下if分支进行表达式计算处理

lARrlV.png

但是很显然,我们本次的expression为字符串”username”,并不满足条件,因此没有办法进入上图119行处的if分支,直接返回,见下图

lAtag1.png

在return后,程序继续回到evaluateParams方法中,此时if (this.name !=
null)已经执行结束,经过上文的分析,findString方法的原理以及name值的得来已经很清楚了。evaluateParams方法中除了if(this.name != null),还有一连串很多if分支

程序会继续执行后续的if,如下图

lAtRgI.png

lAtCct.png

我们跳过中间这一长串if,直接看上图断点处

在altSyntax开启时,进入此分支

lARndH.png

此时的name为”username”,即从上文if (this.name !=null)分支那个我们上文分析的那个if分支内获取来

上图红框中的Expr变量,是由name拼接”%{}”得来,因此值为”%{“username”}”

紧接着这个expr又在下一行代码中传入findValue,见下图

lAtgCd.png

FindValue方法经过上文if (this.name != null)分支分析,大家一定已经很熟悉

FindValue方法最终会将expr即”%{“username”}”传入translateVariables方法

lAtBDK.png

不同于上文那次,此时的expr可是以“%{“开头并且以”}”结尾,满足进入if ((start != -1) && (end != -1) && (count == 0))分支的条件

lAt64H.png

Expr经过下图120的substring取值操作,最后将”%{}”脱掉,将”username”赋值给var

lAt91I.png

接着var被传入Stack.findValue,我们跟入Stack.findValue方法

Stack.findValue如下图

lAtn9s.png

stack.findValue对Ognl表达式进行getValue处理,在经过stack.findValue操作后将表单中username的值进行返回

程序找到我们提交的表单中username的值为”%{1+1}”,见下图

lAt0u6.png

上图这个值就是我们构造的payload

O为stack.findValue的返回值,因此o为”%{1+1}”,见下图

lAtZNQ.png

接着o在字符拼接处理后,赋值给expression变量,见下图

lAtVAg.png

由上图可见, expression 被拼接成 “%{1+1}”

注意,这时候我们还在while循环中,while循环仍没有结束

在这一次的循环中expression 为 “%{1+1}”,见下图

lARdFs.png

expression 为 “%{1+1}”

与上文流程一致,此时 “1+1”被取出赋值给var,见下图

lAtMj0.png

Var被传递给stack.findValue,findValue中对”1+1”表达式进行getValue操作,见下图

lARQJI.png

stack.findValue将处理后的结果2赋值给o,见下图

lAtdjx.png

expression由o拼接而来,拼接后的expression 为 2,见下图

lAtkB8.png

我们仍然在while循环中,在这一次的循环中expression 为 2,见下图

lAt1BT.png

此时expression已经不能满足”%{}”的格式,自然也不会进入后续if分支继续计算

lAtFnf.png

因此break跳出while循环,将值进行return

lAtsED.png

此时return的username值为计算后的2

其实在弄清程序处理的步骤后,不难发现,其实struts2是这样执行的

  1. struts2获得要处理的表单字段名,这里对应的是”username”

  2. 将字段名拼接成”%{}”形式,即”%{“username”}”

  3. 进入while循环,在while循环中,只要满足”%{}”形式,就将其中值取出进行Ognl
    getValue操作,这里对应的就是取出username的value值,即我们构造的payload”%{1+1}”。
    这个操作可以理解为用来取出表单字段对应的value操作

  4. 由于取出的username值”%{1+1}”仍满足”%{}”形式,1+1即被取出传入进行Ongl
    getValue操作,1+1最后返回值为2

  5. 由于2不满足”%{}”形式,while循环停止,将2进行返回

  6. 程序构造返回页面,进行返回

一些想法

S2-001的官方描述如下

WebWork 2.1+和Struts 2的“
altSyntax”功能允许将OGNL表达式插入文本字符串并进行递归处理。这允许恶意用户通常通过HTML文本字段提交包含OGNL表达式的字符串,如果表单验证失败,该字符串将由服务器执行。

但是只有表单验证失败才会触发漏洞吗?

接下来我们做个实验验证

首先修改了下后台代码

lAtehj.png

结果表明Success与否,都返回index.jsp

lAYxtH.png

lAtGEF.png

表达式仍然可以执行,可见与success无关