Struts2漏洞笔记之S2-001

前置知识

Struts2是一个基于MVC设计模式的Web应用框架,它本质上相当于一个servlet,在MVC设计模式中,Struts2作为控制器(Controller)来建立模型与视图的数据交互。 Struts 2是Struts的下一代产品,是在struts 1和WebWork的技术基础上进行了合并的全新的Struts 2框架。

1.OGNL(Object Graph Navigation Language)对象导航图语言

  Struts2框架使用OGNL作为默认的表达式语言,OGNL(Object Graph Navigation Language),是一种表达式语言,目的是为了在不能写Java代码的地方执行java代码;主要作用是用来存数据和取数据的。

2.关于Xwork、ActionContext、OgnlValueStack相关知识可以参考链接:https://milkfr.github.io/java/2019/02/04/java-struts2-4/

版本影响

2.0.1 ~ 2.0.8

漏洞原理

处理登陆问题上,验证失败返回原界面,在处理回显时,框架解析JSP页面标签时会对用户输入的Value值获取,在获取对应的Value值中递归解析%{、}造成了二次解析,最终触发表达式注入漏洞,执行任意代码。

程序入口

struts 2.0.8中Web.xml配置org.apache.struts2.dispatcher.FilterDispatcher为程序入口点,执行doFilter方法.在其中较关键创建OgnlValueStack,并添加相应的数据.

OgnlValueStack创建和数据载入

在Ognl解析表达式中存在关键的三要素expr、root、Context,在expr为可解析的表达式需要符合相关语法。接着需要关注root、Context如何载入到对象中。根据框架的分析可知Struts2中利用OgnlValueStack存储数据栈,而在创建之后将相关参数插入进root与Context.

跟进程序入口点FilterDispatcher.doFilter->this.dispatcher.serviceAction(....)中会先调用this.createContextMap

Dispatcher.createContextMap会获取当前请求的参数并以Map形式保存,最后载入extracontext中.

image-20201209215203978

接着会获取当前访问的命名空间、文件名、方法名

在初始化ActionProxy时会创建一个OgnlValueStack实例(DefaultActionInvocation.createContextMap())

接着会将extraContext通过putAll存放进stack.Context中.

image-20201209214207476

调用push将当前访问生成的实例化Action存入stack.root中.但是这时生成的Action并没有设置上usernamepassword

image-20201209214609324

ParametersInterceptor载入参数

ParametersInterceptor拦截器又继承自MethodFilterInterceptor,其主要功能是把ActionContext中的请求参数设置到ValueStack中,如果栈顶是当前Action则把请求参数设置到了Action中,如果栈顶是一个model(Action实现了ModelDriven接口)则把参数设置到了model中。

跟进代码看下究竟

ParametersInterceptor.doIntercept会从ActionContext上下文中取出parameters

image-20201210104747537

跟进ParametersInterceptor.setParameters一路跟进在OgnlRuntime.setMethodValue中根菌propertyName获取该属性的set方法.接着执行OgnlRuntime.callAppropriateMethod反射执行setPassword方法

image-20201210105247323

执行Action

在一系列拦截器执行完毕后,调用DefaulActionInvocation.invokeActionOnly()执行Action操作.继续跟进DefaulActionInvocation.invokeAction会先获取需要执行该Action实例的方法,该方法在创建ActionProxy获取,没有制定方法时,会默认调用execute方法,接着会反射执行LoginAction.execute()

image-20201210110754593

Result处理

当执行execute返回”error”作为ResultCode返回,(可以看作账号验证失败仍然停留在登陆界面),执行StrutsResultSupport.doExcute()后框架将会开始处理页面回显,而其中会调用中间件tomcat调度器ApplicationDispatcher由于访问jsp文件,会调用JspServlet处理请求。接着Struts2会利用doStartTag、doEndTag解析标签.

通过向页面请求

username=1&password=%25{%40java.lang.System%40getProperty("user.dir")}

进入doEndTag解析标签

<s:textfield name="password" label="password" />

进入UIBean解析公共标签,满足IF语句后会对password拼接%{字符串为%{password}.

image-20201210193258205

之后会进入TextParseUtil.translateVariables递归判断当前返回字符串是否含有%{}字符串,满足的话会剔除掉%{},执行findValue方法,从当前值栈中找到password获得对应的值%{@java.lang.System@getProperty("user.dir")}

image-20201210194132579

由于TextParseUtil.translateVariables的递归判断,会再一次执行获得的值%{@java.lang.System@getProperty("user.dir")}造成二次解析,最后将结果保存值parameters.nameValue.在解析模版时会获取parameters.nameValue值,将执行代码的结果输出到浏览器上.

image-20201210194753753

漏洞复现

image-20201210200224422

修复

TextParseUtil.translateVariables限制递归深度,仅解析一次表达式

image-20201210203428795

总结

其实这是第二遍分析s2-001漏洞,在调试一遍学到很多,主要从框架出发来看待这个问题.

1.ThreadLocal设计模式,保证线程安全,使得每次拿到的ActionContext不受影响.

2.二次解析漏洞挖掘思路,分析至此,究其原因在于递归调用,最后在调用stack.findValue时会解析表达式.(或许写个全局搜findValue有惊喜呢)

3.过一遍文档和框架的生命周期在搭环境和理解代码也会有帮助.

该篇文章没有对Ognl如何解析表达式进一步分析,感觉有点麻烦,后续单独切一个知识点来学习.


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!